1 #!/usr/bin/python2.5 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """A bare-bones test server for testing cloud policy support. 7 8 This implements a simple cloud policy test server that can be used to test 9 chrome's device management service client. The policy information is read from 10 the file named device_management in the server's data directory. It contains 11 enforced and recommended policies for the device and user scope, and a list 12 of managed users. 13 14 The format of the file is JSON. The root dictionary contains a list under the 15 key "managed_users". It contains auth tokens for which the server will claim 16 that the user is managed. The token string "*" indicates that all users are 17 claimed to be managed. Other keys in the root dictionary identify request 18 scopes. Each request scope is described by a dictionary that holds two 19 sub-dictionaries: "mandatory" and "recommended". Both these hold the policy 20 definitions as key/value stores, their format is identical to what the Linux 21 implementation reads from /etc. 22 23 Example: 24 25 { 26 "chromeos/device": { 27 "mandatory": { 28 "HomepageLocation" : "http://www.chromium.org" 29 }, 30 "recommended": { 31 "JavascriptEnabled": false, 32 }, 33 }, 34 "managed_users": [ 35 "secret123456" 36 ] 37 } 38 39 40 """ 41 42 import cgi 43 import logging 44 import os 45 import random 46 import re 47 import sys 48 import time 49 import tlslite 50 import tlslite.api 51 import tlslite.utils 52 53 # The name and availability of the json module varies in python versions. 54 try: 55 import simplejson as json 56 except ImportError: 57 try: 58 import json 59 except ImportError: 60 json = None 61 62 import asn1der 63 import device_management_backend_pb2 as dm 64 import cloud_policy_pb2 as cp 65 import chrome_device_policy_pb2 as dp 66 67 # ASN.1 object identifier for PKCS#1/RSA. 68 PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01' 69 70 class RequestHandler(object): 71 """Decodes and handles device management requests from clients. 72 73 The handler implements all the request parsing and protobuf message decoding 74 and encoding. It calls back into the server to lookup, register, and 75 unregister clients. 76 """ 77 78 def __init__(self, server, path, headers, request): 79 """Initialize the handler. 80 81 Args: 82 server: The TestServer object to use for (un)registering clients. 83 path: A string containing the request path and query parameters. 84 headers: A rfc822.Message-like object containing HTTP headers. 85 request: The request data received from the client as a string. 86 """ 87 self._server = server 88 self._path = path 89 self._headers = headers 90 self._request = request 91 self._params = None 92 93 def GetUniqueParam(self, name): 94 """Extracts a unique query parameter from the request. 95 96 Args: 97 name: Names the parameter to fetch. 98 Returns: 99 The parameter value or None if the parameter doesn't exist or is not 100 unique. 101 """ 102 if not self._params: 103 self._params = cgi.parse_qs(self._path[self._path.find('?') + 1:]) 104 105 param_list = self._params.get(name, []) 106 if len(param_list) == 1: 107 return param_list[0] 108 return None; 109 110 def HandleRequest(self): 111 """Handles a request. 112 113 Parses the data supplied at construction time and returns a pair indicating 114 http status code and response data to be sent back to the client. 115 116 Returns: 117 A tuple of HTTP status code and response data to send to the client. 118 """ 119 rmsg = dm.DeviceManagementRequest() 120 rmsg.ParseFromString(self._request) 121 122 logging.debug('auth -> ' + self._headers.getheader('Authorization', '')) 123 logging.debug('deviceid -> ' + self.GetUniqueParam('deviceid')) 124 self.DumpMessage('Request', rmsg) 125 126 request_type = self.GetUniqueParam('request') 127 # Check server side requirements, as defined in 128 # device_management_backend.proto. 129 if (self.GetUniqueParam('devicetype') != '2' or 130 self.GetUniqueParam('apptype') != 'Chrome' or 131 (request_type != 'ping' and 132 len(self.GetUniqueParam('deviceid')) >= 64) or 133 len(self.GetUniqueParam('agent')) >= 64): 134 return (400, 'Invalid request parameter') 135 if request_type == 'register': 136 return self.ProcessRegister(rmsg.register_request) 137 elif request_type == 'unregister': 138 return self.ProcessUnregister(rmsg.unregister_request) 139 elif request_type == 'policy' or request_type == 'ping': 140 return self.ProcessPolicy(rmsg.policy_request, request_type) 141 else: 142 return (400, 'Invalid request parameter') 143 144 def CheckGoogleLogin(self): 145 """Extracts the GoogleLogin auth token from the HTTP request, and 146 returns it. Returns None if the token is not present. 147 """ 148 match = re.match('GoogleLogin auth=(\\w+)', 149 self._headers.getheader('Authorization', '')) 150 if not match: 151 return None 152 return match.group(1) 153 154 def ProcessRegister(self, msg): 155 """Handles a register request. 156 157 Checks the query for authorization and device identifier, registers the 158 device with the server and constructs a response. 159 160 Args: 161 msg: The DeviceRegisterRequest message received from the client. 162 163 Returns: 164 A tuple of HTTP status code and response data to send to the client. 165 """ 166 # Check the auth token and device ID. 167 if not self.CheckGoogleLogin(): 168 return (403, 'No authorization') 169 170 device_id = self.GetUniqueParam('deviceid') 171 if not device_id: 172 return (400, 'Missing device identifier') 173 174 token_info = self._server.RegisterDevice(device_id, 175 msg.machine_id, 176 msg.type) 177 178 # Send back the reply. 179 response = dm.DeviceManagementResponse() 180 response.register_response.device_management_token = ( 181 token_info['device_token']) 182 response.register_response.machine_name = token_info['machine_name'] 183 184 self.DumpMessage('Response', response) 185 186 return (200, response.SerializeToString()) 187 188 def ProcessUnregister(self, msg): 189 """Handles a register request. 190 191 Checks for authorization, unregisters the device and constructs the 192 response. 193 194 Args: 195 msg: The DeviceUnregisterRequest message received from the client. 196 197 Returns: 198 A tuple of HTTP status code and response data to send to the client. 199 """ 200 # Check the management token. 201 token, response = self.CheckToken(); 202 if not token: 203 return response 204 205 # Unregister the device. 206 self._server.UnregisterDevice(token); 207 208 # Prepare and send the response. 209 response = dm.DeviceManagementResponse() 210 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) 211 212 self.DumpMessage('Response', response) 213 214 return (200, response.SerializeToString()) 215 216 def ProcessInitialPolicy(self, msg): 217 """Handles a 'preregister policy' request. 218 219 Queries the list of managed users and responds the client if their user 220 is managed or not. 221 222 Args: 223 msg: The PolicyFetchRequest message received from the client. 224 225 Returns: 226 A tuple of HTTP status code and response data to send to the client. 227 """ 228 # Check the GAIA token. 229 auth = self.CheckGoogleLogin() 230 if not auth: 231 return (403, 'No authorization') 232 233 chrome_initial_settings = dm.ChromeInitialSettingsProto() 234 if ('*' in self._server.policy['managed_users'] or 235 auth in self._server.policy['managed_users']): 236 chrome_initial_settings.enrollment_provision = ( 237 dm.ChromeInitialSettingsProto.MANAGED); 238 else: 239 chrome_initial_settings.enrollment_provision = ( 240 dm.ChromeInitialSettingsProto.UNMANAGED); 241 242 policy_data = dm.PolicyData() 243 policy_data.policy_type = msg.policy_type 244 policy_data.policy_value = chrome_initial_settings.SerializeToString() 245 246 # Prepare and send the response. 247 response = dm.DeviceManagementResponse() 248 fetch_response = response.policy_response.response.add() 249 fetch_response.policy_data = ( 250 policy_data.SerializeToString()) 251 252 self.DumpMessage('Response', response) 253 254 return (200, response.SerializeToString()) 255 256 def ProcessDevicePolicy(self, msg): 257 """Handles a policy request that uses the deprecated protcol. 258 TODO(gfeher): Remove this when we certainly don't need it. 259 260 Checks for authorization, encodes the policy into protobuf representation 261 and constructs the response. 262 263 Args: 264 msg: The DevicePolicyRequest message received from the client. 265 266 Returns: 267 A tuple of HTTP status code and response data to send to the client. 268 """ 269 270 # Check the management token. 271 token, response = self.CheckToken() 272 if not token: 273 return response 274 275 # Stuff the policy dictionary into a response message and send it back. 276 response = dm.DeviceManagementResponse() 277 response.policy_response.CopyFrom(dm.DevicePolicyResponse()) 278 279 # Respond only if the client requested policy for the cros/device scope, 280 # since that's where chrome policy is supposed to live in. 281 if msg.policy_scope == 'chromeos/device': 282 policy = self._server.policy['google/chromeos/user']['mandatory'] 283 setting = response.policy_response.setting.add() 284 setting.policy_key = 'chrome-policy' 285 policy_value = dm.GenericSetting() 286 for (key, value) in policy.iteritems(): 287 entry = policy_value.named_value.add() 288 entry.name = key 289 entry_value = dm.GenericValue() 290 if isinstance(value, bool): 291 entry_value.value_type = dm.GenericValue.VALUE_TYPE_BOOL 292 entry_value.bool_value = value 293 elif isinstance(value, int): 294 entry_value.value_type = dm.GenericValue.VALUE_TYPE_INT64 295 entry_value.int64_value = value 296 elif isinstance(value, str) or isinstance(value, unicode): 297 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING 298 entry_value.string_value = value 299 elif isinstance(value, list): 300 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY 301 for list_entry in value: 302 entry_value.string_array.append(str(list_entry)) 303 entry.value.CopyFrom(entry_value) 304 setting.policy_value.CopyFrom(policy_value) 305 306 self.DumpMessage('Response', response) 307 308 return (200, response.SerializeToString()) 309 310 def ProcessPolicy(self, msg, request_type): 311 """Handles a policy request. 312 313 Checks for authorization, encodes the policy into protobuf representation 314 and constructs the response. 315 316 Args: 317 msg: The DevicePolicyRequest message received from the client. 318 319 Returns: 320 A tuple of HTTP status code and response data to send to the client. 321 """ 322 323 if msg.request: 324 for request in msg.request: 325 if request.policy_type == 'google/chromeos/unregistered_user': 326 if request_type != 'ping': 327 return (400, 'Invalid request type') 328 return self.ProcessInitialPolicy(request) 329 elif (request.policy_type in 330 ('google/chromeos/user', 'google/chromeos/device')): 331 if request_type != 'policy': 332 return (400, 'Invalid request type') 333 return self.ProcessCloudPolicy(request) 334 else: 335 return (400, 'Invalid policy_type') 336 else: 337 return self.ProcessDevicePolicy(msg) 338 339 def SetProtobufMessageField(self, group_message, field, field_value): 340 '''Sets a field in a protobuf message. 341 342 Args: 343 group_message: The protobuf message. 344 field: The field of the message to set, it shuold be a member of 345 group_message.DESCRIPTOR.fields. 346 field_value: The value to set. 347 ''' 348 if field.label == field.LABEL_REPEATED: 349 assert type(field_value) == list 350 entries = group_message.__getattribute__(field.name) 351 for list_item in field_value: 352 entries.append(list_item) 353 return 354 elif field.type == field.TYPE_BOOL: 355 assert type(field_value) == bool 356 elif field.type == field.TYPE_STRING: 357 assert type(field_value) == str or type(field_value) == unicode 358 elif field.type == field.TYPE_INT64: 359 assert type(field_value) == int 360 elif (field.type == field.TYPE_MESSAGE and 361 field.message_type.name == 'StringList'): 362 assert type(field_value) == list 363 entries = group_message.__getattribute__(field.name).entries 364 for list_item in field_value: 365 entries.append(list_item) 366 return 367 else: 368 raise Exception('Unknown field type %s' % field.type) 369 group_message.__setattr__(field.name, field_value) 370 371 def GatherDevicePolicySettings(self, settings, policies): 372 '''Copies all the policies from a dictionary into a protobuf of type 373 CloudDeviceSettingsProto. 374 375 Args: 376 settings: The destination ChromeDeviceSettingsProto protobuf. 377 policies: The source dictionary containing policies in JSON format. 378 ''' 379 for group in settings.DESCRIPTOR.fields: 380 # Create protobuf message for group. 381 group_message = eval('dp.' + group.message_type.name + '()') 382 # Indicates if at least one field was set in |group_message|. 383 got_fields = False 384 # Iterate over fields of the message and feed them from the 385 # policy config file. 386 for field in group_message.DESCRIPTOR.fields: 387 field_value = None 388 if field.name in policies: 389 got_fields = True 390 field_value = policies[field.name] 391 self.SetProtobufMessageField(group_message, field, field_value) 392 if got_fields: 393 settings.__getattribute__(group.name).CopyFrom(group_message) 394 395 def GatherUserPolicySettings(self, settings, policies): 396 '''Copies all the policies from a dictionary into a protobuf of type 397 CloudPolicySettings. 398 399 Args: 400 settings: The destination: a CloudPolicySettings protobuf. 401 policies: The source: a dictionary containing policies under keys 402 'recommended' and 'mandatory'. 403 ''' 404 for group in settings.DESCRIPTOR.fields: 405 # Create protobuf message for group. 406 group_message = eval('cp.' + group.message_type.name + '()') 407 # We assume that this policy group will be recommended, and only switch 408 # it to mandatory if at least one of its members is mandatory. 409 group_message.policy_options.mode = cp.PolicyOptions.RECOMMENDED 410 # Indicates if at least one field was set in |group_message|. 411 got_fields = False 412 # Iterate over fields of the message and feed them from the 413 # policy config file. 414 for field in group_message.DESCRIPTOR.fields: 415 field_value = None 416 if field.name in policies['mandatory']: 417 group_message.policy_options.mode = cp.PolicyOptions.MANDATORY 418 field_value = policies['mandatory'][field.name] 419 elif field.name in policies['recommended']: 420 field_value = policies['recommended'][field.name] 421 if field_value != None: 422 got_fields = True 423 self.SetProtobufMessageField(group_message, field, field_value) 424 if got_fields: 425 settings.__getattribute__(group.name).CopyFrom(group_message) 426 427 def ProcessCloudPolicy(self, msg): 428 """Handles a cloud policy request. (New protocol for policy requests.) 429 430 Checks for authorization, encodes the policy into protobuf representation, 431 signs it and constructs the repsonse. 432 433 Args: 434 msg: The CloudPolicyRequest message received from the client. 435 436 Returns: 437 A tuple of HTTP status code and response data to send to the client. 438 """ 439 440 token_info, error = self.CheckToken() 441 if not token_info: 442 return error 443 444 # Response is only given if the scope is specified in the config file. 445 # Normally 'google/chromeos/device' and 'google/chromeos/user' should be 446 # accepted. 447 policy_value = '' 448 if (msg.policy_type in token_info['allowed_policy_types'] and 449 msg.policy_type in self._server.policy): 450 if msg.policy_type == 'google/chromeos/user': 451 settings = cp.CloudPolicySettings() 452 self.GatherUserPolicySettings(settings, 453 self._server.policy[msg.policy_type]) 454 policy_value = settings.SerializeToString() 455 elif msg.policy_type == 'google/chromeos/device': 456 settings = dp.ChromeDeviceSettingsProto() 457 self.GatherDevicePolicySettings(settings, 458 self._server.policy[msg.policy_type]) 459 policy_value = settings.SerializeToString() 460 461 # Figure out the key we want to use. If multiple keys are configured, the 462 # server will rotate through them in a round-robin fashion. 463 signing_key = None 464 req_key = None 465 key_version = 1 466 nkeys = len(self._server.keys) 467 if msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and nkeys > 0: 468 if msg.public_key_version in range(1, nkeys + 1): 469 # requested key exists, use for signing and rotate. 470 req_key = self._server.keys[msg.public_key_version - 1]['private_key'] 471 key_version = (msg.public_key_version % nkeys) + 1 472 signing_key = self._server.keys[key_version - 1] 473 474 # Fill the policy data protobuf. 475 policy_data = dm.PolicyData() 476 policy_data.policy_type = msg.policy_type 477 policy_data.timestamp = int(time.time() * 1000) 478 policy_data.request_token = token_info['device_token']; 479 policy_data.policy_value = policy_value 480 policy_data.machine_name = token_info['machine_name'] 481 if signing_key: 482 policy_data.public_key_version = key_version 483 policy_data.username = self._server.username 484 policy_data.device_id = token_info['device_id'] 485 signed_data = policy_data.SerializeToString() 486 487 response = dm.DeviceManagementResponse() 488 fetch_response = response.policy_response.response.add() 489 fetch_response.policy_data = signed_data 490 if signing_key: 491 fetch_response.policy_data_signature = ( 492 signing_key['private_key'].hashAndSign(signed_data).tostring()) 493 if msg.public_key_version != key_version: 494 fetch_response.new_public_key = signing_key['public_key'] 495 if req_key: 496 fetch_response.new_public_key_signature = ( 497 req_key.hashAndSign(fetch_response.new_public_key).tostring()) 498 499 self.DumpMessage('Response', response) 500 501 return (200, response.SerializeToString()) 502 503 def CheckToken(self): 504 """Helper for checking whether the client supplied a valid DM token. 505 506 Extracts the token from the request and passed to the server in order to 507 look up the client. 508 509 Returns: 510 A pair of token information record and error response. If the first 511 element is None, then the second contains an error code to send back to 512 the client. Otherwise the first element is the same structure that is 513 returned by LookupToken(). 514 """ 515 error = None 516 dmtoken = None 517 request_device_id = self.GetUniqueParam('deviceid') 518 match = re.match('GoogleDMToken token=(\\w+)', 519 self._headers.getheader('Authorization', '')) 520 if match: 521 dmtoken = match.group(1) 522 if not dmtoken: 523 error = dm.DeviceManagementResponse.DEVICE_MANAGEMENT_TOKEN_INVALID 524 else: 525 token_info = self._server.LookupToken(dmtoken) 526 if (not token_info or 527 not request_device_id or 528 token_info['device_id'] != request_device_id): 529 error = dm.DeviceManagementResponse.DEVICE_NOT_FOUND 530 else: 531 return (token_info, None) 532 533 response = dm.DeviceManagementResponse() 534 response.error = error 535 536 self.DumpMessage('Response', response) 537 538 return (None, (200, response.SerializeToString())) 539 540 def DumpMessage(self, label, msg): 541 """Helper for logging an ASCII dump of a protobuf message.""" 542 logging.debug('%s\n%s' % (label, str(msg))) 543 544 class TestServer(object): 545 """Handles requests and keeps global service state.""" 546 547 def __init__(self, policy_path, private_key_paths, policy_user): 548 """Initializes the server. 549 550 Args: 551 policy_path: Names the file to read JSON-formatted policy from. 552 private_key_paths: List of paths to read private keys from. 553 """ 554 self._registered_tokens = {} 555 self.policy = {} 556 557 # There is no way to for the testserver to know the user name belonging to 558 # the GAIA auth token we received (short of actually talking to GAIA). To 559 # address this, we have a command line parameter to set the username that 560 # the server should report to the client. 561 self.username = policy_user 562 563 if json is None: 564 print 'No JSON module, cannot parse policy information' 565 else : 566 try: 567 self.policy = json.loads(open(policy_path).read()) 568 except IOError: 569 print 'Failed to load policy from %s' % policy_path 570 571 self.keys = [] 572 if private_key_paths: 573 # Load specified keys from the filesystem. 574 for key_path in private_key_paths: 575 try: 576 key = tlslite.api.parsePEMKey(open(key_path).read(), private=True) 577 except IOError: 578 print 'Failed to load private key from %s' % key_path 579 continue 580 581 assert key != None 582 self.keys.append({ 'private_key' : key }) 583 else: 584 # Generate a key if none were specified. 585 key = tlslite.api.generateRSAKey(1024) 586 assert key != None 587 self.keys.append({ 'private_key' : key }) 588 589 # Derive the public keys from the loaded private keys. 590 for entry in self.keys: 591 key = entry['private_key'] 592 593 algorithm = asn1der.Sequence( 594 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID), 595 asn1der.Data(asn1der.NULL, '') ]) 596 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n), 597 asn1der.Integer(key.e) ]) 598 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ]) 599 entry['public_key'] = pubkey; 600 601 def HandleRequest(self, path, headers, request): 602 """Handles a request. 603 604 Args: 605 path: The request path and query parameters received from the client. 606 headers: A rfc822.Message-like object containing HTTP headers. 607 request: The request data received from the client as a string. 608 Returns: 609 A pair of HTTP status code and response data to send to the client. 610 """ 611 handler = RequestHandler(self, path, headers, request) 612 return handler.HandleRequest() 613 614 def RegisterDevice(self, device_id, machine_id, type): 615 """Registers a device or user and generates a DM token for it. 616 617 Args: 618 device_id: The device identifier provided by the client. 619 620 Returns: 621 The newly generated device token for the device. 622 """ 623 dmtoken_chars = [] 624 while len(dmtoken_chars) < 32: 625 dmtoken_chars.append(random.choice('0123456789abcdef')) 626 dmtoken = ''.join(dmtoken_chars) 627 allowed_policy_types = { 628 dm.DeviceRegisterRequest.USER: ['google/chromeos/user'], 629 dm.DeviceRegisterRequest.DEVICE: ['google/chromeos/device'], 630 dm.DeviceRegisterRequest.TT: ['google/chromeos/user'], 631 } 632 self._registered_tokens[dmtoken] = { 633 'device_id': device_id, 634 'device_token': dmtoken, 635 'allowed_policy_types': allowed_policy_types[type], 636 'machine_name': 'chromeos-' + machine_id, 637 } 638 return self._registered_tokens[dmtoken] 639 640 def LookupToken(self, dmtoken): 641 """Looks up a device or a user by DM token. 642 643 Args: 644 dmtoken: The device management token provided by the client. 645 646 Returns: 647 A dictionary with information about a device or user that is registered by 648 dmtoken, or None if the token is not found. 649 """ 650 return self._registered_tokens.get(dmtoken, None) 651 652 def UnregisterDevice(self, dmtoken): 653 """Unregisters a device identified by the given DM token. 654 655 Args: 656 dmtoken: The device management token provided by the client. 657 """ 658 if dmtoken in self._registered_tokens.keys(): 659 del self._registered_tokens[dmtoken] 660