1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """A bare-bones test server for testing cloud policy support. 6 7 This implements a simple cloud policy test server that can be used to test 8 chrome's device management service client. The policy information is read from 9 the file named device_management in the server's data directory. It contains 10 enforced and recommended policies for the device and user scope, and a list 11 of managed users. 12 13 The format of the file is JSON. The root dictionary contains a list under the 14 key "managed_users". It contains auth tokens for which the server will claim 15 that the user is managed. The token string "*" indicates that all users are 16 claimed to be managed. Other keys in the root dictionary identify request 17 scopes. The user-request scope is described by a dictionary that holds two 18 sub-dictionaries: "mandatory" and "recommended". Both these hold the policy 19 definitions as key/value stores, their format is identical to what the Linux 20 implementation reads from /etc. 21 The device-scope holds the policy-definition directly as key/value stores in the 22 protobuf-format. 23 24 Example: 25 26 { 27 "google/chromeos/device" : { 28 "guest_mode_enabled" : false 29 }, 30 "google/chromeos/user" : { 31 "mandatory" : { 32 "HomepageLocation" : "http://www.chromium.org", 33 "IncognitoEnabled" : false 34 }, 35 "recommended" : { 36 "JavascriptEnabled": false 37 } 38 }, 39 "google/chromeos/publicaccount/user@example.com" : { 40 "mandatory" : { 41 "HomepageLocation" : "http://www.chromium.org" 42 }, 43 "recommended" : { 44 } 45 }, 46 "managed_users" : [ 47 "secret123456" 48 ], 49 "current_key_index": 0, 50 "robot_api_auth_code": "fake_auth_code", 51 "invalidation_source": 1025, 52 "invalidation_name": "UENUPOL" 53 } 54 55 """ 56 57 import base64 58 import BaseHTTPServer 59 import cgi 60 import glob 61 import google.protobuf.text_format 62 import hashlib 63 import logging 64 import os 65 import random 66 import re 67 import sys 68 import time 69 import tlslite 70 import tlslite.api 71 import tlslite.utils 72 import tlslite.utils.cryptomath 73 import urlparse 74 75 # The name and availability of the json module varies in python versions. 76 try: 77 import simplejson as json 78 except ImportError: 79 try: 80 import json 81 except ImportError: 82 json = None 83 84 import asn1der 85 import testserver_base 86 87 import device_management_backend_pb2 as dm 88 import cloud_policy_pb2 as cp 89 import chrome_extension_policy_pb2 as ep 90 91 # Device policy is only available on Chrome OS builds. 92 try: 93 import chrome_device_policy_pb2 as dp 94 except ImportError: 95 dp = None 96 97 # ASN.1 object identifier for PKCS#1/RSA. 98 PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01' 99 100 # List of bad machine identifiers that trigger the |valid_serial_number_missing| 101 # flag to be set set in the policy fetch response. 102 BAD_MACHINE_IDS = [ '123490EN400015' ] 103 104 # List of machines that trigger the server to send kiosk enrollment response 105 # for the register request. 106 KIOSK_MACHINE_IDS = [ 'KIOSK' ] 107 108 # Dictionary containing base64-encoded policy signing keys plus per-domain 109 # signatures. Format is: 110 # { 111 # 'key': <base64-encoded PKCS8-format private key>, 112 # 'signatures': { 113 # <domain1>: <base64-encdoded SHA256 signature for key + domain1> 114 # <domain2>: <base64-encdoded SHA256 signature for key + domain2> 115 # ... 116 # } 117 # } 118 SIGNING_KEYS = [ 119 # Key1 120 {'key': 121 'MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2c3KzcPqvnJ5HCk3OZkf1' 122 'LMO8Ht4dw4FO2U0EmKvpo0zznj4RwUdmKobH1AFWzwZP4CDY2M67MsukE/1Jnbx1QIDAQ' 123 'ABAkBkKcLZa/75hHVz4PR3tZaw34PATlfxEG6RiRIwXlf/FFlfGIZOSxdW/I1A3XRl0/9' 124 'nZMuctBSKBrcTRZQWfT/hAiEA9g8xbQbMO6BEH/XCRSsQbPlvj4c9wDtVEzeAzZ/ht9kC' 125 'IQDiml+/lXS1emqml711jJcYJNYJzdy1lL/ieKogR59oXQIhAK+Pl4xa1U2VxAWpq7r+R' 126 'vH55wdZT03hB4p2h4gvEzXBAiAkw9kvE0eZPiBZoRrrHIFTOH7FnnHlwBmV2+/2RsiVPQ' 127 'IhAKqx/4qisivvmoM/xbzUagfoxwsu1A/4mGjhBKiS0BCq', 128 'signatures': 129 {'example.com': 130 'l+sT5mziei/GbmiP7VtRCCfwpZcg7uKbW2OlnK5B/TTELutjEIAMdHduNBwbO44qOn' 131 '/5c7YrtkXbBehaaDYFPGI6bGTbDmG9KRxhS+DaB7opgfCQWLi79Gn/jytKLZhRN/VS' 132 'y+PEbezqMi3d1/xDxlThwWZDNwnhv9ER/Nu/32ZTjzgtqonSn2CQtwXCIILm4FdV/1' 133 '/BdmZG+Ge4i4FTqYtInir5YFe611KXU/AveGhQGBIAXo4qYg1IqbVrvKBSU9dlI6Sl' 134 '9TJJLbJ3LGaXuljgFhyMAl3gcy7ftC9MohEmwa+sc7y2mOAgYQ5SSmyAtQwQgAkX9J' 135 '3+tfxjmoA/dg==', 136 'chromepolicytest.com': 137 'TzBiigZKwBdr6lyP6tUDsw+Q9wYO1Yepyxm0O4JZ4RID32L27sWzC1/hwC51fRcCvP' 138 'luEVIW6mH+BFODXMrteUFWfbbG7jgV+Wg+QdzMqgJjxhNKFXPTsZ7/286LAd1vBY/A' 139 'nGd8Wog6AhzfrgMbLNsH794GD0xIUwRvXUWFNP8pClj5VPgQnJrIA9aZwW8FNGbteA' 140 'HacFB0T/oqP5s7XT4Qvkj14RLmCgTwEM8Vcpqy5teJaF8yN17wniveddoOQGH6s0HC' 141 'ocprEccrH5fP/WVAPxCfx4vVYQY5q4CZ4K3f6dTC2FV4IDelM6dugEkvSS02YCzDaO' 142 'N+Z7IwElzTKg==', 143 'managedchrome.com': 144 'T0wXC5w3GXyovA09pyOLX7ui/NI603UfbZXYyTbHI7xtzCIaHVPH35Nx4zdqVrdsej' 145 'ErQ12yVLDDIJokY4Yl+/fj/zrkAPxThI+TNQ+jo0i+al05PuopfpzvCzIXiZBbkbyW' 146 '3XfedxXP3IPN2XU2/3vX+ZXUNG6pxeETem64kGezkjkUraqnHw3JVzwJYHhpMcwdLP' 147 'PYK6V23BbEHEVBtQZd/ledXacz7gOzm1zGni4e+vxA2roAdJWyhbjU0dTKNNUsZmMv' 148 'ryQH9Af1Jw+dqs0RAbhcJXm2i8EUWIgNv6aMn1Z2DzZwKKjXsKgcYSRo8pdYa8RZAo' 149 'UExd9roA9a5w==', 150 } 151 }, 152 # Key2 153 {'key': 154 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmZhreV04M3knCi6wibr49' 155 'oDesHny1G33PKOX9ko8pcxAiu9ZqsKCj7wNW2PGqnLi81fddACwQtYn5xdhCtzB9wIDAQ' 156 'ABAkA0z8m0cy8N08xundspoFZWO71WJLgv/peSDBYGI0RzJR1l9Np355EukQUQwRs5XrL' 157 '3vRQZy2vDqeiR96epkAhRAiEAzJ4DVI8k3pAl7CGv5icqFkJ02viExIwehhIEXBcB6p0C' 158 'IQDAKmzpoRpBEZRQ9xrTvPOi+Ea8Jnd478BU7CI/LFfgowIgMfLIoVWoDGRnvXKju60Hy' 159 'xNB70oHLut9cADp64j6QMkCIDrgxN4QbmrhaAAmtiGKE1wrlgCwCIsVamiasSOKAqLhAi' 160 'EAo/ItVcFtQPod97qG71CY/O4JzOciuU6AMhprs181vfM=', 161 'signatures': 162 # Key2 signatures 163 {'example.com': 164 'cO0nQjRptkeefKDw5QpJSQDavHABxUvbR9Wvoa235OG9Whw1RFqq2ye6pKnI3ezW6/' 165 '7b4ANcpi5a7HV5uF8K7gWyYdxY8NHLeyrbwXxg5j6HAmHmkP1UZcf/dAnWqo7cW8g4' 166 'DIQOhC43KkveMYJ2HnelwdXt/7zqkbe8/3Yj4nhjAUeARx86Sb8Nzydwkrvqs5Jw/x' 167 '5LG+BODExrXXcGu/ubDlW4ivJFqfNUPQysqBXSMY2XCHPJDx3eECLGVVN/fFAWWgjM' 168 'HFObAriAt0b18cc9Nr0mAt4Qq1oDzWcAHCPHE+5dr8Uf46BUrMLJRNRKCY7rrsoIin' 169 '9Be9gs3W+Aww==', 170 'chromepolicytest.com': 171 'mr+9CCYvR0cTvPwlzkxqlpGYy55gY7cPiIkPAPoql51yHK1tkMTOSFru8Dy/nMt+0o' 172 '4z7WO60F1wnIBGkQxnTj/DsO6QpCYi7oHqtLmZ2jsLQFlMyvPGUtpJEFvRwjr/TNbh' 173 '6RqUtz1LQFuJQ848kBrx7nkte1L8SuPDExgx+Q3LtbNj4SuTdvMUBMvEERXiLuwfFL' 174 'BefGjtsqfWETQVlJTCW7xcqOLedIX8UYgEDBpDOZ23A3GzCShuBsIut5m87R5mODht' 175 'EUmKNDK1+OMc6SyDpf+r48Wph4Db1bVaKy8fcpSNJOwEgsrmH7/+owKPGcN7I5jYAF' 176 'Z2PGxHTQ9JNA==', 177 'managedchrome.com': 178 'o5MVSo4bRwIJ/aooGyXpRXsEsWPG8fNA2UTG8hgwnLYhNeJCCnLs/vW2vdp0URE8jn' 179 'qiG4N8KjbuiGw0rJtO1EygdLfpnMEtqYlFjrOie38sy92l/AwohXj6luYzMWL+FqDu' 180 'WQeXasjgyY4s9BOLQVDEnEj3pvqhrk/mXvMwUeXGpbxTNbWAd0C8BTZrGOwU/kIXxo' 181 'vAMGg8L+rQaDwBTEnMsMZcvlrIyqSg5v4BxCWuL3Yd2xvUqZEUWRp1aKetsHRnz5hw' 182 'H7WK7DzvKepDn06XjPG9lchi448U3HB3PRKtCzfO3nD9YXMKTuqRpKPF8PeK11CWh1' 183 'DBvBYwi20vbQ==', 184 }, 185 }, 186 ] 187 188 class PolicyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 189 """Decodes and handles device management requests from clients. 190 191 The handler implements all the request parsing and protobuf message decoding 192 and encoding. It calls back into the server to lookup, register, and 193 unregister clients. 194 """ 195 196 def __init__(self, request, client_address, server): 197 """Initialize the handler. 198 199 Args: 200 request: The request data received from the client as a string. 201 client_address: The client address. 202 server: The TestServer object to use for (un)registering clients. 203 """ 204 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, 205 client_address, server) 206 207 def GetUniqueParam(self, name): 208 """Extracts a unique query parameter from the request. 209 210 Args: 211 name: Names the parameter to fetch. 212 Returns: 213 The parameter value or None if the parameter doesn't exist or is not 214 unique. 215 """ 216 if not hasattr(self, '_params'): 217 self._params = cgi.parse_qs(self.path[self.path.find('?') + 1:]) 218 219 param_list = self._params.get(name, []) 220 if len(param_list) == 1: 221 return param_list[0] 222 return None 223 224 def do_GET(self): 225 """Handles GET requests. 226 227 Currently this is only used to serve external policy data.""" 228 sep = self.path.find('?') 229 path = self.path if sep == -1 else self.path[:sep] 230 if path == '/externalpolicydata': 231 http_response, raw_reply = self.HandleExternalPolicyDataRequest() 232 else: 233 http_response = 404 234 raw_reply = 'Invalid path' 235 self.send_response(http_response) 236 self.end_headers() 237 self.wfile.write(raw_reply) 238 239 def do_POST(self): 240 http_response, raw_reply = self.HandleRequest() 241 self.send_response(http_response) 242 if (http_response == 200): 243 self.send_header('Content-Type', 'application/x-protobuffer') 244 self.end_headers() 245 self.wfile.write(raw_reply) 246 247 def HandleExternalPolicyDataRequest(self): 248 """Handles a request to download policy data for a component.""" 249 policy_key = self.GetUniqueParam('key') 250 if not policy_key: 251 return (400, 'Missing key parameter') 252 data = self.server.ReadPolicyDataFromDataDir(policy_key) 253 if data is None: 254 return (404, 'Policy not found for ' + policy_key) 255 return (200, data) 256 257 def HandleRequest(self): 258 """Handles a request. 259 260 Parses the data supplied at construction time and returns a pair indicating 261 http status code and response data to be sent back to the client. 262 263 Returns: 264 A tuple of HTTP status code and response data to send to the client. 265 """ 266 rmsg = dm.DeviceManagementRequest() 267 length = int(self.headers.getheader('content-length')) 268 rmsg.ParseFromString(self.rfile.read(length)) 269 270 logging.debug('gaia auth token -> ' + 271 self.headers.getheader('Authorization', '')) 272 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token'))) 273 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid'))) 274 self.DumpMessage('Request', rmsg) 275 276 request_type = self.GetUniqueParam('request') 277 # Check server side requirements, as defined in 278 # device_management_backend.proto. 279 if (self.GetUniqueParam('devicetype') != '2' or 280 self.GetUniqueParam('apptype') != 'Chrome' or 281 len(self.GetUniqueParam('deviceid')) >= 64 or 282 len(self.GetUniqueParam('agent')) >= 64): 283 return (400, 'Invalid request parameter') 284 if request_type == 'register': 285 response = self.ProcessRegister(rmsg.register_request) 286 elif request_type == 'api_authorization': 287 response = self.ProcessApiAuthorization(rmsg.service_api_access_request) 288 elif request_type == 'unregister': 289 response = self.ProcessUnregister(rmsg.unregister_request) 290 elif request_type == 'policy': 291 response = self.ProcessPolicy(rmsg, request_type) 292 elif request_type == 'enterprise_check': 293 response = self.ProcessAutoEnrollment(rmsg.auto_enrollment_request) 294 elif request_type == 'device_state_retrieval': 295 response = self.ProcessDeviceStateRetrievalRequest( 296 rmsg.device_state_retrieval_request) 297 else: 298 return (400, 'Invalid request parameter') 299 300 self.DumpMessage('Response', response[1]) 301 return (response[0], response[1].SerializeToString()) 302 303 def CreatePolicyForExternalPolicyData(self, policy_key): 304 """Returns an ExternalPolicyData protobuf for policy_key. 305 306 If there is policy data for policy_key then the download url will be 307 set so that it points to that data, and the appropriate hash is also set. 308 Otherwise, the protobuf will be empty. 309 310 Args: 311 policy_key: The policy type and settings entity id, joined by '/'. 312 313 Returns: 314 A serialized ExternalPolicyData. 315 """ 316 settings = ep.ExternalPolicyData() 317 data = self.server.ReadPolicyDataFromDataDir(policy_key) 318 if data: 319 settings.download_url = urlparse.urljoin( 320 self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key) 321 settings.secure_hash = hashlib.sha256(data).digest() 322 return settings.SerializeToString() 323 324 def CheckGoogleLogin(self): 325 """Extracts the auth token from the request and returns it. The token may 326 either be a GoogleLogin token from an Authorization header, or an OAuth V2 327 token from the oauth_token query parameter. Returns None if no token is 328 present. 329 """ 330 oauth_token = self.GetUniqueParam('oauth_token') 331 if oauth_token: 332 return oauth_token 333 334 match = re.match('GoogleLogin auth=(\\w+)', 335 self.headers.getheader('Authorization', '')) 336 if match: 337 return match.group(1) 338 339 return None 340 341 def ProcessRegister(self, msg): 342 """Handles a register request. 343 344 Checks the query for authorization and device identifier, registers the 345 device with the server and constructs a response. 346 347 Args: 348 msg: The DeviceRegisterRequest message received from the client. 349 350 Returns: 351 A tuple of HTTP status code and response data to send to the client. 352 """ 353 # Check the auth token and device ID. 354 auth = self.CheckGoogleLogin() 355 if not auth: 356 return (403, 'No authorization') 357 358 policy = self.server.GetPolicies() 359 if ('*' not in policy['managed_users'] and 360 auth not in policy['managed_users']): 361 return (403, 'Unmanaged') 362 363 device_id = self.GetUniqueParam('deviceid') 364 if not device_id: 365 return (400, 'Missing device identifier') 366 367 token_info = self.server.RegisterDevice(device_id, 368 msg.machine_id, 369 msg.type) 370 371 # Send back the reply. 372 response = dm.DeviceManagementResponse() 373 response.register_response.device_management_token = ( 374 token_info['device_token']) 375 response.register_response.machine_name = token_info['machine_name'] 376 response.register_response.enrollment_type = token_info['enrollment_mode'] 377 378 return (200, response) 379 380 def ProcessApiAuthorization(self, msg): 381 """Handles an API authorization request. 382 383 Args: 384 msg: The DeviceServiceApiAccessRequest message received from the client. 385 386 Returns: 387 A tuple of HTTP status code and response data to send to the client. 388 """ 389 policy = self.server.GetPolicies() 390 391 # Return the auth code from the config file if it's defined, 392 # else return a descriptive default value. 393 response = dm.DeviceManagementResponse() 394 response.service_api_access_response.auth_code = policy.get( 395 'robot_api_auth_code', 'policy_testserver.py-auth_code') 396 397 return (200, response) 398 399 def ProcessUnregister(self, msg): 400 """Handles a register request. 401 402 Checks for authorization, unregisters the device and constructs the 403 response. 404 405 Args: 406 msg: The DeviceUnregisterRequest message received from the client. 407 408 Returns: 409 A tuple of HTTP status code and response data to send to the client. 410 """ 411 # Check the management token. 412 token, response = self.CheckToken() 413 if not token: 414 return response 415 416 # Unregister the device. 417 self.server.UnregisterDevice(token['device_token']) 418 419 # Prepare and send the response. 420 response = dm.DeviceManagementResponse() 421 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) 422 423 return (200, response) 424 425 def ProcessPolicy(self, msg, request_type): 426 """Handles a policy request. 427 428 Checks for authorization, encodes the policy into protobuf representation 429 and constructs the response. 430 431 Args: 432 msg: The DeviceManagementRequest message received from the client. 433 434 Returns: 435 A tuple of HTTP status code and response data to send to the client. 436 """ 437 token_info, error = self.CheckToken() 438 if not token_info: 439 return error 440 441 key_update_request = msg.device_state_key_update_request 442 if len(key_update_request.server_backed_state_key) > 0: 443 self.server.UpdateStateKeys(token_info['device_token'], 444 key_update_request.server_backed_state_key) 445 446 # If this is a |publicaccount| request, get the |username| now and use 447 # it in every PolicyFetchResponse produced. This is required to validate 448 # policy for extensions in device-local accounts. 449 # Unfortunately, the |username| can't be obtained from |msg| because that 450 # requires interacting with GAIA. 451 username = None 452 for request in msg.policy_request.request: 453 if request.policy_type == 'google/chromeos/publicaccount': 454 username = request.settings_entity_id 455 456 response = dm.DeviceManagementResponse() 457 for request in msg.policy_request.request: 458 if (request.policy_type in 459 ('google/android/user', 460 'google/chromeos/device', 461 'google/chromeos/publicaccount', 462 'google/chromeos/user', 463 'google/chrome/user', 464 'google/ios/user')): 465 fetch_response = response.policy_response.response.add() 466 self.ProcessCloudPolicy(request, token_info, fetch_response, username) 467 elif request.policy_type == 'google/chrome/extension': 468 self.ProcessCloudPolicyForExtensions( 469 request, response.policy_response, token_info, username) 470 else: 471 fetch_response.error_code = 400 472 fetch_response.error_message = 'Invalid policy_type' 473 474 return (200, response) 475 476 def ProcessAutoEnrollment(self, msg): 477 """Handles an auto-enrollment check request. 478 479 The reply depends on the value of the modulus: 480 1: replies with no new modulus and the sha256 hash of "0" 481 2: replies with a new modulus, 4. 482 4: replies with a new modulus, 2. 483 8: fails with error 400. 484 16: replies with a new modulus, 16. 485 32: replies with a new modulus, 1. 486 anything else: replies with no new modulus and an empty list of hashes 487 488 These allow the client to pick the testing scenario its wants to simulate. 489 490 Args: 491 msg: The DeviceAutoEnrollmentRequest message received from the client. 492 493 Returns: 494 A tuple of HTTP status code and response data to send to the client. 495 """ 496 auto_enrollment_response = dm.DeviceAutoEnrollmentResponse() 497 498 if msg.modulus == 1: 499 auto_enrollment_response.hash.extend( 500 self.server.GetMatchingStateKeyHashes(msg.modulus, msg.remainder)) 501 elif msg.modulus == 2: 502 auto_enrollment_response.expected_modulus = 4 503 elif msg.modulus == 4: 504 auto_enrollment_response.expected_modulus = 2 505 elif msg.modulus == 8: 506 return (400, 'Server error') 507 elif msg.modulus == 16: 508 auto_enrollment_response.expected_modulus = 16 509 elif msg.modulus == 32: 510 auto_enrollment_response.expected_modulus = 1 511 512 response = dm.DeviceManagementResponse() 513 response.auto_enrollment_response.CopyFrom(auto_enrollment_response) 514 return (200, response) 515 516 def ProcessDeviceStateRetrievalRequest(self, msg): 517 """Handles a device state retrieval request. 518 519 Response data is taken from server configuration. 520 521 Returns: 522 A tuple of HTTP status code and response data to send to the client. 523 """ 524 device_state_retrieval_response = dm.DeviceStateRetrievalResponse() 525 526 client = self.server.LookupByStateKey(msg.server_backed_state_key) 527 if client is not None: 528 state = self.server.GetPolicies().get('device_state', {}) 529 FIELDS = [ 530 'management_domain', 531 'restore_mode', 532 ] 533 for field in FIELDS: 534 if field in state: 535 setattr(device_state_retrieval_response, field, state[field]) 536 537 response = dm.DeviceManagementResponse() 538 response.device_state_retrieval_response.CopyFrom( 539 device_state_retrieval_response) 540 return (200, response) 541 542 def SetProtobufMessageField(self, group_message, field, field_value): 543 """Sets a field in a protobuf message. 544 545 Args: 546 group_message: The protobuf message. 547 field: The field of the message to set, it should be a member of 548 group_message.DESCRIPTOR.fields. 549 field_value: The value to set. 550 """ 551 if field.label == field.LABEL_REPEATED: 552 assert type(field_value) == list 553 entries = group_message.__getattribute__(field.name) 554 if field.message_type is None: 555 for list_item in field_value: 556 entries.append(list_item) 557 else: 558 # This field is itself a protobuf. 559 sub_type = field.message_type 560 for sub_value in field_value: 561 assert type(sub_value) == dict 562 # Add a new sub-protobuf per list entry. 563 sub_message = entries.add() 564 # Now iterate over its fields and recursively add them. 565 for sub_field in sub_message.DESCRIPTOR.fields: 566 if sub_field.name in sub_value: 567 value = sub_value[sub_field.name] 568 self.SetProtobufMessageField(sub_message, sub_field, value) 569 return 570 elif field.type == field.TYPE_BOOL: 571 assert type(field_value) == bool 572 elif field.type == field.TYPE_STRING: 573 assert type(field_value) == str or type(field_value) == unicode 574 elif field.type == field.TYPE_INT64: 575 assert type(field_value) == int 576 elif (field.type == field.TYPE_MESSAGE and 577 field.message_type.name == 'StringList'): 578 assert type(field_value) == list 579 entries = group_message.__getattribute__(field.name).entries 580 for list_item in field_value: 581 entries.append(list_item) 582 return 583 else: 584 raise Exception('Unknown field type %s' % field.type) 585 group_message.__setattr__(field.name, field_value) 586 587 def GatherDevicePolicySettings(self, settings, policies): 588 """Copies all the policies from a dictionary into a protobuf of type 589 CloudDeviceSettingsProto. 590 591 Args: 592 settings: The destination ChromeDeviceSettingsProto protobuf. 593 policies: The source dictionary containing policies in JSON format. 594 """ 595 for group in settings.DESCRIPTOR.fields: 596 # Create protobuf message for group. 597 group_message = eval('dp.' + group.message_type.name + '()') 598 # Indicates if at least one field was set in |group_message|. 599 got_fields = False 600 # Iterate over fields of the message and feed them from the 601 # policy config file. 602 for field in group_message.DESCRIPTOR.fields: 603 field_value = None 604 if field.name in policies: 605 got_fields = True 606 field_value = policies[field.name] 607 self.SetProtobufMessageField(group_message, field, field_value) 608 if got_fields: 609 settings.__getattribute__(group.name).CopyFrom(group_message) 610 611 def GatherUserPolicySettings(self, settings, policies): 612 """Copies all the policies from a dictionary into a protobuf of type 613 CloudPolicySettings. 614 615 Args: 616 settings: The destination: a CloudPolicySettings protobuf. 617 policies: The source: a dictionary containing policies under keys 618 'recommended' and 'mandatory'. 619 """ 620 for field in settings.DESCRIPTOR.fields: 621 # |field| is the entry for a specific policy in the top-level 622 # CloudPolicySettings proto. 623 624 # Look for this policy's value in the mandatory or recommended dicts. 625 if field.name in policies.get('mandatory', {}): 626 mode = cp.PolicyOptions.MANDATORY 627 value = policies['mandatory'][field.name] 628 elif field.name in policies.get('recommended', {}): 629 mode = cp.PolicyOptions.RECOMMENDED 630 value = policies['recommended'][field.name] 631 else: 632 continue 633 634 # Create protobuf message for this policy. 635 policy_message = eval('cp.' + field.message_type.name + '()') 636 policy_message.policy_options.mode = mode 637 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value'] 638 self.SetProtobufMessageField(policy_message, field_descriptor, value) 639 settings.__getattribute__(field.name).CopyFrom(policy_message) 640 641 def ProcessCloudPolicyForExtensions(self, request, response, token_info, 642 username=None): 643 """Handles a request for policy for extensions. 644 645 A request for policy for extensions is slightly different from the other 646 cloud policy requests, because it can trigger 0, one or many 647 PolicyFetchResponse messages in the response. 648 649 Args: 650 request: The PolicyFetchRequest that triggered this handler. 651 response: The DevicePolicyResponse message for the response. Multiple 652 PolicyFetchResponses will be appended to this message. 653 token_info: The token extracted from the request. 654 username: The username for the response. May be None. 655 """ 656 # Send one PolicyFetchResponse for each extension that has 657 # configuration data at the server. 658 ids = self.server.ListMatchingComponents('google/chrome/extension') 659 for settings_entity_id in ids: 660 # Reuse the extension policy request, to trigger the same signature 661 # type in the response. 662 request.settings_entity_id = settings_entity_id 663 fetch_response = response.response.add() 664 self.ProcessCloudPolicy(request, token_info, fetch_response, username) 665 # Don't do key rotations for these messages. 666 fetch_response.ClearField('new_public_key') 667 fetch_response.ClearField('new_public_key_signature') 668 fetch_response.ClearField('new_public_key_verification_signature') 669 670 def ProcessCloudPolicy(self, msg, token_info, response, username=None): 671 """Handles a cloud policy request. (New protocol for policy requests.) 672 673 Encodes the policy into protobuf representation, signs it and constructs 674 the response. 675 676 Args: 677 msg: The CloudPolicyRequest message received from the client. 678 token_info: The token extracted from the request. 679 response: A PolicyFetchResponse message that should be filled with the 680 response data. 681 username: The username for the response. May be None. 682 """ 683 684 if msg.machine_id: 685 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id) 686 687 # Response is only given if the scope is specified in the config file. 688 # Normally 'google/chromeos/device', 'google/chromeos/user' and 689 # 'google/chromeos/publicaccount' should be accepted. 690 policy = self.server.GetPolicies() 691 policy_value = '' 692 policy_key = msg.policy_type 693 if msg.settings_entity_id: 694 policy_key += '/' + msg.settings_entity_id 695 if msg.policy_type in token_info['allowed_policy_types']: 696 if msg.policy_type in ('google/android/user', 697 'google/chromeos/publicaccount', 698 'google/chromeos/user', 699 'google/chrome/user', 700 'google/ios/user'): 701 settings = cp.CloudPolicySettings() 702 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 703 if payload is None: 704 self.GatherUserPolicySettings(settings, policy.get(policy_key, {})) 705 payload = settings.SerializeToString() 706 elif dp is not None and msg.policy_type == 'google/chromeos/device': 707 settings = dp.ChromeDeviceSettingsProto() 708 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 709 if payload is None: 710 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {})) 711 payload = settings.SerializeToString() 712 elif msg.policy_type == 'google/chrome/extension': 713 settings = ep.ExternalPolicyData() 714 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 715 if payload is None: 716 payload = self.CreatePolicyForExternalPolicyData(policy_key) 717 else: 718 response.error_code = 400 719 response.error_message = 'Invalid policy type' 720 return 721 else: 722 response.error_code = 400 723 response.error_message = 'Request not allowed for the token used' 724 return 725 726 # Sign with 'current_key_index', defaulting to key 0. 727 signing_key = None 728 req_key = None 729 current_key_index = policy.get('current_key_index', 0) 730 nkeys = len(self.server.keys) 731 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and 732 current_key_index in range(nkeys)): 733 signing_key = self.server.keys[current_key_index] 734 if msg.public_key_version in range(1, nkeys + 1): 735 # requested key exists, use for signing and rotate. 736 req_key = self.server.keys[msg.public_key_version - 1]['private_key'] 737 738 # Fill the policy data protobuf. 739 policy_data = dm.PolicyData() 740 policy_data.policy_type = msg.policy_type 741 policy_data.timestamp = int(time.time() * 1000) 742 policy_data.request_token = token_info['device_token'] 743 policy_data.policy_value = payload 744 policy_data.machine_name = token_info['machine_name'] 745 policy_data.valid_serial_number_missing = ( 746 token_info['machine_id'] in BAD_MACHINE_IDS) 747 policy_data.settings_entity_id = msg.settings_entity_id 748 policy_data.service_account_identity = policy.get( 749 'service_account_identity', 750 'policy_testserver.py-service_account_identity') 751 invalidation_source = policy.get('invalidation_source') 752 if invalidation_source is not None: 753 policy_data.invalidation_source = invalidation_source 754 # Since invalidation_name is type bytes in the proto, the Unicode name 755 # provided needs to be encoded as ASCII to set the correct byte pattern. 756 invalidation_name = policy.get('invalidation_name') 757 if invalidation_name is not None: 758 policy_data.invalidation_name = invalidation_name.encode('ascii') 759 760 if signing_key: 761 policy_data.public_key_version = current_key_index + 1 762 763 if username: 764 policy_data.username = username 765 else: 766 # For regular user/device policy, there is no way for the testserver to 767 # know the user name belonging to the GAIA auth token we received (short 768 # of actually talking to GAIA). To address this, we read the username from 769 # the policy configuration dictionary, or use a default. 770 policy_data.username = policy.get('policy_user', 'user (at] example.com') 771 policy_data.device_id = token_info['device_id'] 772 signed_data = policy_data.SerializeToString() 773 774 response.policy_data = signed_data 775 if signing_key: 776 response.policy_data_signature = ( 777 bytes(signing_key['private_key'].hashAndSign(signed_data))) 778 if msg.public_key_version != current_key_index + 1: 779 response.new_public_key = signing_key['public_key'] 780 781 # Set the verification signature appropriate for the policy domain. 782 # TODO(atwilson): Use the enrollment domain for public accounts when 783 # we add key validation for ChromeOS (http://crbug.com/328038). 784 if 'signatures' in signing_key: 785 verification_sig = self.GetSignatureForDomain( 786 signing_key['signatures'], policy_data.username) 787 788 if verification_sig: 789 assert len(verification_sig) == 256, \ 790 'bad signature size: %d' % len(verification_sig) 791 response.new_public_key_verification_signature = verification_sig 792 793 if req_key: 794 response.new_public_key_signature = ( 795 bytes(req_key.hashAndSign(response.new_public_key))) 796 797 return (200, response.SerializeToString()) 798 799 def GetSignatureForDomain(self, signatures, username): 800 parsed_username = username.split("@", 1) 801 if len(parsed_username) != 2: 802 logging.error('Could not extract domain from username: %s' % username) 803 return None 804 domain = parsed_username[1] 805 806 # Lookup the domain's signature in the passed dictionary. If none is found, 807 # fallback to a wildcard signature. 808 if domain in signatures: 809 return signatures[domain] 810 if '*' in signatures: 811 return signatures['*'] 812 813 # No key matching this domain. 814 logging.error('No verification signature matching domain: %s' % domain) 815 return None 816 817 def CheckToken(self): 818 """Helper for checking whether the client supplied a valid DM token. 819 820 Extracts the token from the request and passed to the server in order to 821 look up the client. 822 823 Returns: 824 A pair of token information record and error response. If the first 825 element is None, then the second contains an error code to send back to 826 the client. Otherwise the first element is the same structure that is 827 returned by LookupToken(). 828 """ 829 error = 500 830 dmtoken = None 831 request_device_id = self.GetUniqueParam('deviceid') 832 match = re.match('GoogleDMToken token=(\\w+)', 833 self.headers.getheader('Authorization', '')) 834 if match: 835 dmtoken = match.group(1) 836 if not dmtoken: 837 error = 401 838 else: 839 token_info = self.server.LookupToken(dmtoken) 840 if (not token_info or 841 not request_device_id or 842 token_info['device_id'] != request_device_id): 843 error = 410 844 else: 845 return (token_info, None) 846 847 logging.debug('Token check failed with error %d' % error) 848 849 return (None, (error, 'Server error %d' % error)) 850 851 def DumpMessage(self, label, msg): 852 """Helper for logging an ASCII dump of a protobuf message.""" 853 logging.debug('%s\n%s' % (label, str(msg))) 854 855 856 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn, 857 testserver_base.StoppableHTTPServer): 858 """Handles requests and keeps global service state.""" 859 860 def __init__(self, server_address, data_dir, policy_path, client_state_file, 861 private_key_paths, server_base_url): 862 """Initializes the server. 863 864 Args: 865 server_address: Server host and port. 866 policy_path: Names the file to read JSON-formatted policy from. 867 private_key_paths: List of paths to read private keys from. 868 """ 869 testserver_base.StoppableHTTPServer.__init__(self, server_address, 870 PolicyRequestHandler) 871 self._registered_tokens = {} 872 self.data_dir = data_dir 873 self.policy_path = policy_path 874 self.client_state_file = client_state_file 875 self.server_base_url = server_base_url 876 877 self.keys = [] 878 if private_key_paths: 879 # Load specified keys from the filesystem. 880 for key_path in private_key_paths: 881 try: 882 key_str = open(key_path).read() 883 except IOError: 884 print 'Failed to load private key from %s' % key_path 885 continue 886 try: 887 key = tlslite.api.parsePEMKey(key_str, private=True) 888 except SyntaxError: 889 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8( 890 bytearray(key_str)) 891 892 assert key is not None 893 key_info = { 'private_key' : key } 894 895 # Now try to read in a signature, if one exists. 896 try: 897 key_sig = open(key_path + '.sig').read() 898 # Create a dictionary with the wildcard domain + signature 899 key_info['signatures'] = {'*': key_sig} 900 except IOError: 901 print 'Failed to read validation signature from %s.sig' % key_path 902 self.keys.append(key_info) 903 else: 904 # Use the canned private keys if none were passed from the command line. 905 for signing_key in SIGNING_KEYS: 906 decoded_key = base64.b64decode(signing_key['key']); 907 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8( 908 bytearray(decoded_key)) 909 assert key is not None 910 # Grab the signature dictionary for this key and decode all of the 911 # signatures. 912 signature_dict = signing_key['signatures'] 913 decoded_signatures = {} 914 for domain in signature_dict: 915 decoded_signatures[domain] = base64.b64decode(signature_dict[domain]) 916 self.keys.append({'private_key': key, 917 'signatures': decoded_signatures}) 918 919 # Derive the public keys from the private keys. 920 for entry in self.keys: 921 key = entry['private_key'] 922 923 algorithm = asn1der.Sequence( 924 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID), 925 asn1der.Data(asn1der.NULL, '') ]) 926 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n), 927 asn1der.Integer(key.e) ]) 928 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ]) 929 entry['public_key'] = pubkey 930 931 # Load client state. 932 if self.client_state_file is not None: 933 try: 934 file_contents = open(self.client_state_file).read() 935 self._registered_tokens = json.loads(file_contents, strict=False) 936 except IOError: 937 pass 938 939 def GetPolicies(self): 940 """Returns the policies to be used, reloaded form the backend file every 941 time this is called. 942 """ 943 policy = {} 944 if json is None: 945 print 'No JSON module, cannot parse policy information' 946 else : 947 try: 948 policy = json.loads(open(self.policy_path).read(), strict=False) 949 except IOError: 950 print 'Failed to load policy from %s' % self.policy_path 951 return policy 952 953 def RegisterDevice(self, device_id, machine_id, type): 954 """Registers a device or user and generates a DM token for it. 955 956 Args: 957 device_id: The device identifier provided by the client. 958 959 Returns: 960 The newly generated device token for the device. 961 """ 962 dmtoken_chars = [] 963 while len(dmtoken_chars) < 32: 964 dmtoken_chars.append(random.choice('0123456789abcdef')) 965 dmtoken = ''.join(dmtoken_chars) 966 allowed_policy_types = { 967 dm.DeviceRegisterRequest.BROWSER: [ 968 'google/chrome/user', 969 'google/chrome/extension' 970 ], 971 dm.DeviceRegisterRequest.USER: [ 972 'google/chromeos/user', 973 'google/chrome/extension' 974 ], 975 dm.DeviceRegisterRequest.DEVICE: [ 976 'google/chromeos/device', 977 'google/chromeos/publicaccount', 978 'google/chrome/extension' 979 ], 980 dm.DeviceRegisterRequest.ANDROID_BROWSER: [ 981 'google/android/user' 982 ], 983 dm.DeviceRegisterRequest.IOS_BROWSER: [ 984 'google/ios/user' 985 ], 986 dm.DeviceRegisterRequest.TT: ['google/chromeos/user', 987 'google/chrome/user'], 988 } 989 if machine_id in KIOSK_MACHINE_IDS: 990 enrollment_mode = dm.DeviceRegisterResponse.RETAIL 991 else: 992 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE 993 self._registered_tokens[dmtoken] = { 994 'device_id': device_id, 995 'device_token': dmtoken, 996 'allowed_policy_types': allowed_policy_types[type], 997 'machine_name': 'chromeos-' + machine_id, 998 'machine_id': machine_id, 999 'enrollment_mode': enrollment_mode, 1000 } 1001 self.WriteClientState() 1002 return self._registered_tokens[dmtoken] 1003 1004 def UpdateMachineId(self, dmtoken, machine_id): 1005 """Updates the machine identifier for a registered device. 1006 1007 Args: 1008 dmtoken: The device management token provided by the client. 1009 machine_id: Updated hardware identifier value. 1010 """ 1011 if dmtoken in self._registered_tokens: 1012 self._registered_tokens[dmtoken]['machine_id'] = machine_id 1013 self.WriteClientState() 1014 1015 def UpdateStateKeys(self, dmtoken, state_keys): 1016 """Updates the state keys for a given client. 1017 1018 Args: 1019 dmtoken: The device management token provided by the client. 1020 state_keys: The state keys to set. 1021 """ 1022 if dmtoken in self._registered_tokens: 1023 self._registered_tokens[dmtoken]['state_keys'] = map( 1024 lambda key : key.encode('hex'), state_keys) 1025 self.WriteClientState() 1026 1027 def LookupToken(self, dmtoken): 1028 """Looks up a device or a user by DM token. 1029 1030 Args: 1031 dmtoken: The device management token provided by the client. 1032 1033 Returns: 1034 A dictionary with information about a device or user that is registered by 1035 dmtoken, or None if the token is not found. 1036 """ 1037 return self._registered_tokens.get(dmtoken, None) 1038 1039 def LookupByStateKey(self, state_key): 1040 """Looks up a device or a user by a state key. 1041 1042 Args: 1043 state_key: The state key provided by the client. 1044 1045 Returns: 1046 A dictionary with information about a device or user or None if there is 1047 no matching record. 1048 """ 1049 for client in self._registered_tokens.values(): 1050 if state_key.encode('hex') in client.get('state_keys', []): 1051 return client 1052 1053 return None 1054 1055 def GetMatchingStateKeyHashes(self, modulus, remainder): 1056 """Returns all clients registered with the server. 1057 1058 Returns: 1059 The list of registered clients. 1060 """ 1061 state_keys = sum([ c.get('state_keys', []) 1062 for c in self._registered_tokens.values() ], []) 1063 hashed_keys = map(lambda key: hashlib.sha256(key.decode('hex')).digest(), 1064 set(state_keys)) 1065 return filter( 1066 lambda hash : int(hash.encode('hex'), 16) % modulus == remainder, 1067 hashed_keys) 1068 1069 def UnregisterDevice(self, dmtoken): 1070 """Unregisters a device identified by the given DM token. 1071 1072 Args: 1073 dmtoken: The device management token provided by the client. 1074 """ 1075 if dmtoken in self._registered_tokens.keys(): 1076 del self._registered_tokens[dmtoken] 1077 self.WriteClientState() 1078 1079 def WriteClientState(self): 1080 """Writes the client state back to the file.""" 1081 if self.client_state_file is not None: 1082 json_data = json.dumps(self._registered_tokens) 1083 open(self.client_state_file, 'w').write(json_data) 1084 1085 def GetBaseFilename(self, policy_selector): 1086 """Returns the base filename for the given policy_selector. 1087 1088 Args: 1089 policy_selector: The policy type and settings entity id, joined by '/'. 1090 1091 Returns: 1092 The filename corresponding to the policy_selector, without a file 1093 extension. 1094 """ 1095 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector) 1096 return os.path.join(self.data_dir or '', 1097 'policy_%s' % sanitized_policy_selector) 1098 1099 def ListMatchingComponents(self, policy_type): 1100 """Returns a list of settings entity IDs that have a configuration file. 1101 1102 Args: 1103 policy_type: The policy type to look for. Only settings entity IDs for 1104 file selectors That match this policy_type will be returned. 1105 1106 Returns: 1107 A list of settings entity IDs for the given |policy_type| that have a 1108 configuration file in this server (either as a .bin, .txt or .data file). 1109 """ 1110 base_name = self.GetBaseFilename(policy_type) 1111 files = glob.glob('%s_*.*' % base_name) 1112 len_base_name = len(base_name) + 1 1113 return [ file[len_base_name:file.rfind('.')] for file in files ] 1114 1115 def ReadPolicyFromDataDir(self, policy_selector, proto_message): 1116 """Tries to read policy payload from a file in the data directory. 1117 1118 First checks for a binary rendition of the policy protobuf in 1119 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns 1120 it. If that file doesn't exist, tries 1121 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a 1122 protobuf using proto_message. If that fails as well, returns None. 1123 1124 Args: 1125 policy_selector: Selects which policy to read. 1126 proto_message: Optional protobuf message object used for decoding the 1127 proto text format. 1128 1129 Returns: 1130 The binary payload message, or None if not found. 1131 """ 1132 base_filename = self.GetBaseFilename(policy_selector) 1133 1134 # Try the binary payload file first. 1135 try: 1136 return open(base_filename + '.bin').read() 1137 except IOError: 1138 pass 1139 1140 # If that fails, try the text version instead. 1141 if proto_message is None: 1142 return None 1143 1144 try: 1145 text = open(base_filename + '.txt').read() 1146 google.protobuf.text_format.Merge(text, proto_message) 1147 return proto_message.SerializeToString() 1148 except IOError: 1149 return None 1150 except google.protobuf.text_format.ParseError: 1151 return None 1152 1153 def ReadPolicyDataFromDataDir(self, policy_selector): 1154 """Returns the external policy data for |policy_selector| if found. 1155 1156 Args: 1157 policy_selector: Selects which policy to read. 1158 1159 Returns: 1160 The data for the corresponding policy type and entity id, if found. 1161 """ 1162 base_filename = self.GetBaseFilename(policy_selector) 1163 try: 1164 return open(base_filename + '.data').read() 1165 except IOError: 1166 return None 1167 1168 def GetBaseURL(self): 1169 """Returns the server base URL. 1170 1171 Respects the |server_base_url| configuration parameter, if present. Falls 1172 back to construct the URL from the server hostname and port otherwise. 1173 1174 Returns: 1175 The URL to use for constructing URLs that get returned to clients. 1176 """ 1177 base_url = self.server_base_url 1178 if base_url is None: 1179 base_url = 'http://%s:%s' % self.server_address[:2] 1180 1181 return base_url 1182 1183 1184 class PolicyServerRunner(testserver_base.TestServerRunner): 1185 1186 def __init__(self): 1187 super(PolicyServerRunner, self).__init__() 1188 1189 def create_server(self, server_data): 1190 data_dir = self.options.data_dir or '' 1191 config_file = (self.options.config_file or 1192 os.path.join(data_dir, 'device_management')) 1193 server = PolicyTestServer((self.options.host, self.options.port), 1194 data_dir, config_file, 1195 self.options.client_state_file, 1196 self.options.policy_keys, 1197 self.options.server_base_url) 1198 server_data['port'] = server.server_port 1199 return server 1200 1201 def add_options(self): 1202 testserver_base.TestServerRunner.add_options(self) 1203 self.option_parser.add_option('--client-state', dest='client_state_file', 1204 help='File that client state should be ' 1205 'persisted to. This allows the server to be ' 1206 'seeded by a list of pre-registered clients ' 1207 'and restarts without abandoning registered ' 1208 'clients.') 1209 self.option_parser.add_option('--policy-key', action='append', 1210 dest='policy_keys', 1211 help='Specify a path to a PEM-encoded ' 1212 'private key to use for policy signing. May ' 1213 'be specified multiple times in order to ' 1214 'load multiple keys into the server. If the ' 1215 'server has multiple keys, it will rotate ' 1216 'through them in at each request in a ' 1217 'round-robin fashion. The server will ' 1218 'use a canned key if none is specified ' 1219 'on the command line. The test server will ' 1220 'also look for a verification signature file ' 1221 'in the same location: <filename>.sig and if ' 1222 'present will add the signature to the ' 1223 'policy blob as appropriate via the ' 1224 'new_public_key_verification_signature ' 1225 'field.') 1226 self.option_parser.add_option('--log-level', dest='log_level', 1227 default='WARN', 1228 help='Log level threshold to use.') 1229 self.option_parser.add_option('--config-file', dest='config_file', 1230 help='Specify a configuration file to use ' 1231 'instead of the default ' 1232 '<data_dir>/device_management') 1233 self.option_parser.add_option('--server-base-url', dest='server_base_url', 1234 help='The server base URL to use when ' 1235 'constructing URLs to return to the client.') 1236 1237 def run_server(self): 1238 logger = logging.getLogger() 1239 logger.setLevel(getattr(logging, str(self.options.log_level).upper())) 1240 if (self.options.log_to_console): 1241 logger.addHandler(logging.StreamHandler()) 1242 if (self.options.log_file): 1243 logger.addHandler(logging.FileHandler(self.options.log_file)) 1244 1245 testserver_base.TestServerRunner.run_server(self) 1246 1247 1248 if __name__ == '__main__': 1249 sys.exit(PolicyServerRunner().main()) 1250