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 elif path == '/configuration/test/exit': 233 # This is not part of the standard DM server protocol. 234 # This extension is added to make the test server exit gracefully 235 # when the test is complete. 236 self.server.stop = True 237 http_response = 200 238 raw_reply = 'OK' 239 else: 240 http_response = 404 241 raw_reply = 'Invalid path' 242 self.send_response(http_response) 243 self.end_headers() 244 self.wfile.write(raw_reply) 245 246 def do_POST(self): 247 http_response, raw_reply = self.HandleRequest() 248 self.send_response(http_response) 249 if (http_response == 200): 250 self.send_header('Content-Type', 'application/x-protobuffer') 251 self.end_headers() 252 self.wfile.write(raw_reply) 253 254 def HandleExternalPolicyDataRequest(self): 255 """Handles a request to download policy data for a component.""" 256 policy_key = self.GetUniqueParam('key') 257 if not policy_key: 258 return (400, 'Missing key parameter') 259 data = self.server.ReadPolicyDataFromDataDir(policy_key) 260 if data is None: 261 return (404, 'Policy not found for ' + policy_key) 262 return (200, data) 263 264 def HandleRequest(self): 265 """Handles a request. 266 267 Parses the data supplied at construction time and returns a pair indicating 268 http status code and response data to be sent back to the client. 269 270 Returns: 271 A tuple of HTTP status code and response data to send to the client. 272 """ 273 rmsg = dm.DeviceManagementRequest() 274 length = int(self.headers.getheader('content-length')) 275 rmsg.ParseFromString(self.rfile.read(length)) 276 277 logging.debug('gaia auth token -> ' + 278 self.headers.getheader('Authorization', '')) 279 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token'))) 280 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid'))) 281 self.DumpMessage('Request', rmsg) 282 283 request_type = self.GetUniqueParam('request') 284 # Check server side requirements, as defined in 285 # device_management_backend.proto. 286 if (self.GetUniqueParam('devicetype') != '2' or 287 self.GetUniqueParam('apptype') != 'Chrome' or 288 len(self.GetUniqueParam('deviceid')) >= 64): 289 return (400, 'Invalid request parameter') 290 if request_type == 'register': 291 response = self.ProcessRegister(rmsg.register_request) 292 elif request_type == 'api_authorization': 293 response = self.ProcessApiAuthorization(rmsg.service_api_access_request) 294 elif request_type == 'unregister': 295 response = self.ProcessUnregister(rmsg.unregister_request) 296 elif request_type == 'policy': 297 response = self.ProcessPolicy(rmsg, request_type) 298 elif request_type == 'enterprise_check': 299 response = self.ProcessAutoEnrollment(rmsg.auto_enrollment_request) 300 elif request_type == 'device_state_retrieval': 301 response = self.ProcessDeviceStateRetrievalRequest( 302 rmsg.device_state_retrieval_request) 303 else: 304 return (400, 'Invalid request parameter') 305 306 self.DumpMessage('Response', response[1]) 307 return (response[0], response[1].SerializeToString()) 308 309 def CreatePolicyForExternalPolicyData(self, policy_key): 310 """Returns an ExternalPolicyData protobuf for policy_key. 311 312 If there is policy data for policy_key then the download url will be 313 set so that it points to that data, and the appropriate hash is also set. 314 Otherwise, the protobuf will be empty. 315 316 Args: 317 policy_key: The policy type and settings entity id, joined by '/'. 318 319 Returns: 320 A serialized ExternalPolicyData. 321 """ 322 settings = ep.ExternalPolicyData() 323 data = self.server.ReadPolicyDataFromDataDir(policy_key) 324 if data: 325 settings.download_url = urlparse.urljoin( 326 self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key) 327 settings.secure_hash = hashlib.sha256(data).digest() 328 return settings.SerializeToString() 329 330 def CheckGoogleLogin(self): 331 """Extracts the auth token from the request and returns it. The token may 332 either be a GoogleLogin token from an Authorization header, or an OAuth V2 333 token from the oauth_token query parameter. Returns None if no token is 334 present. 335 """ 336 oauth_token = self.GetUniqueParam('oauth_token') 337 if oauth_token: 338 return oauth_token 339 340 match = re.match('GoogleLogin auth=(\\w+)', 341 self.headers.getheader('Authorization', '')) 342 if match: 343 return match.group(1) 344 345 return None 346 347 def ProcessRegister(self, msg): 348 """Handles a register request. 349 350 Checks the query for authorization and device identifier, registers the 351 device with the server and constructs a response. 352 353 Args: 354 msg: The DeviceRegisterRequest message received from the client. 355 356 Returns: 357 A tuple of HTTP status code and response data to send to the client. 358 """ 359 # Check the auth token and device ID. 360 auth = self.CheckGoogleLogin() 361 if not auth: 362 return (403, 'No authorization') 363 364 policy = self.server.GetPolicies() 365 if ('*' not in policy['managed_users'] and 366 auth not in policy['managed_users']): 367 return (403, 'Unmanaged') 368 369 device_id = self.GetUniqueParam('deviceid') 370 if not device_id: 371 return (400, 'Missing device identifier') 372 373 token_info = self.server.RegisterDevice(device_id, 374 msg.machine_id, 375 msg.type) 376 377 # Send back the reply. 378 response = dm.DeviceManagementResponse() 379 response.register_response.device_management_token = ( 380 token_info['device_token']) 381 response.register_response.machine_name = token_info['machine_name'] 382 response.register_response.enrollment_type = token_info['enrollment_mode'] 383 384 return (200, response) 385 386 def ProcessApiAuthorization(self, msg): 387 """Handles an API authorization request. 388 389 Args: 390 msg: The DeviceServiceApiAccessRequest message received from the client. 391 392 Returns: 393 A tuple of HTTP status code and response data to send to the client. 394 """ 395 policy = self.server.GetPolicies() 396 397 # Return the auth code from the config file if it's defined, 398 # else return a descriptive default value. 399 response = dm.DeviceManagementResponse() 400 response.service_api_access_response.auth_code = policy.get( 401 'robot_api_auth_code', 'policy_testserver.py-auth_code') 402 403 return (200, response) 404 405 def ProcessUnregister(self, msg): 406 """Handles a register request. 407 408 Checks for authorization, unregisters the device and constructs the 409 response. 410 411 Args: 412 msg: The DeviceUnregisterRequest message received from the client. 413 414 Returns: 415 A tuple of HTTP status code and response data to send to the client. 416 """ 417 # Check the management token. 418 token, response = self.CheckToken() 419 if not token: 420 return response 421 422 # Unregister the device. 423 self.server.UnregisterDevice(token['device_token']) 424 425 # Prepare and send the response. 426 response = dm.DeviceManagementResponse() 427 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) 428 429 return (200, response) 430 431 def ProcessPolicy(self, msg, request_type): 432 """Handles a policy request. 433 434 Checks for authorization, encodes the policy into protobuf representation 435 and constructs the response. 436 437 Args: 438 msg: The DeviceManagementRequest message received from the client. 439 440 Returns: 441 A tuple of HTTP status code and response data to send to the client. 442 """ 443 token_info, error = self.CheckToken() 444 if not token_info: 445 return error 446 447 key_update_request = msg.device_state_key_update_request 448 if len(key_update_request.server_backed_state_key) > 0: 449 self.server.UpdateStateKeys(token_info['device_token'], 450 key_update_request.server_backed_state_key) 451 452 # If this is a |publicaccount| request, get the |username| now and use 453 # it in every PolicyFetchResponse produced. This is required to validate 454 # policy for extensions in device-local accounts. 455 # Unfortunately, the |username| can't be obtained from |msg| because that 456 # requires interacting with GAIA. 457 username = None 458 for request in msg.policy_request.request: 459 if request.policy_type == 'google/chromeos/publicaccount': 460 username = request.settings_entity_id 461 462 response = dm.DeviceManagementResponse() 463 for request in msg.policy_request.request: 464 if (request.policy_type in 465 ('google/android/user', 466 'google/chromeos/device', 467 'google/chromeos/publicaccount', 468 'google/chromeos/user', 469 'google/chrome/user', 470 'google/ios/user')): 471 fetch_response = response.policy_response.response.add() 472 self.ProcessCloudPolicy(request, token_info, fetch_response, username) 473 elif request.policy_type == 'google/chrome/extension': 474 self.ProcessCloudPolicyForExtensions( 475 request, response.policy_response, token_info, username) 476 else: 477 fetch_response.error_code = 400 478 fetch_response.error_message = 'Invalid policy_type' 479 480 return (200, response) 481 482 def ProcessAutoEnrollment(self, msg): 483 """Handles an auto-enrollment check request. 484 485 The reply depends on the value of the modulus: 486 1: replies with no new modulus and the sha256 hash of "0" 487 2: replies with a new modulus, 4. 488 4: replies with a new modulus, 2. 489 8: fails with error 400. 490 16: replies with a new modulus, 16. 491 32: replies with a new modulus, 1. 492 anything else: replies with no new modulus and an empty list of hashes 493 494 These allow the client to pick the testing scenario its wants to simulate. 495 496 Args: 497 msg: The DeviceAutoEnrollmentRequest message received from the client. 498 499 Returns: 500 A tuple of HTTP status code and response data to send to the client. 501 """ 502 auto_enrollment_response = dm.DeviceAutoEnrollmentResponse() 503 504 if msg.modulus == 1: 505 auto_enrollment_response.hash.extend( 506 self.server.GetMatchingStateKeyHashes(msg.modulus, msg.remainder)) 507 elif msg.modulus == 2: 508 auto_enrollment_response.expected_modulus = 4 509 elif msg.modulus == 4: 510 auto_enrollment_response.expected_modulus = 2 511 elif msg.modulus == 8: 512 return (400, 'Server error') 513 elif msg.modulus == 16: 514 auto_enrollment_response.expected_modulus = 16 515 elif msg.modulus == 32: 516 auto_enrollment_response.expected_modulus = 1 517 518 response = dm.DeviceManagementResponse() 519 response.auto_enrollment_response.CopyFrom(auto_enrollment_response) 520 return (200, response) 521 522 def ProcessDeviceStateRetrievalRequest(self, msg): 523 """Handles a device state retrieval request. 524 525 Response data is taken from server configuration. 526 527 Returns: 528 A tuple of HTTP status code and response data to send to the client. 529 """ 530 device_state_retrieval_response = dm.DeviceStateRetrievalResponse() 531 532 client = self.server.LookupByStateKey(msg.server_backed_state_key) 533 if client is not None: 534 state = self.server.GetPolicies().get('device_state', {}) 535 FIELDS = [ 536 'management_domain', 537 'restore_mode', 538 ] 539 for field in FIELDS: 540 if field in state: 541 setattr(device_state_retrieval_response, field, state[field]) 542 543 response = dm.DeviceManagementResponse() 544 response.device_state_retrieval_response.CopyFrom( 545 device_state_retrieval_response) 546 return (200, response) 547 548 def SetProtobufMessageField(self, group_message, field, field_value): 549 """Sets a field in a protobuf message. 550 551 Args: 552 group_message: The protobuf message. 553 field: The field of the message to set, it should be a member of 554 group_message.DESCRIPTOR.fields. 555 field_value: The value to set. 556 """ 557 if field.label == field.LABEL_REPEATED: 558 assert type(field_value) == list 559 entries = group_message.__getattribute__(field.name) 560 if field.message_type is None: 561 for list_item in field_value: 562 entries.append(list_item) 563 else: 564 # This field is itself a protobuf. 565 sub_type = field.message_type 566 for sub_value in field_value: 567 assert type(sub_value) == dict 568 # Add a new sub-protobuf per list entry. 569 sub_message = entries.add() 570 # Now iterate over its fields and recursively add them. 571 for sub_field in sub_message.DESCRIPTOR.fields: 572 if sub_field.name in sub_value: 573 value = sub_value[sub_field.name] 574 self.SetProtobufMessageField(sub_message, sub_field, value) 575 return 576 elif field.type == field.TYPE_BOOL: 577 assert type(field_value) == bool 578 elif field.type == field.TYPE_STRING: 579 assert type(field_value) == str or type(field_value) == unicode 580 elif field.type == field.TYPE_INT64: 581 assert type(field_value) == int 582 elif (field.type == field.TYPE_MESSAGE and 583 field.message_type.name == 'StringList'): 584 assert type(field_value) == list 585 entries = group_message.__getattribute__(field.name).entries 586 for list_item in field_value: 587 entries.append(list_item) 588 return 589 else: 590 raise Exception('Unknown field type %s' % field.type) 591 group_message.__setattr__(field.name, field_value) 592 593 def GatherDevicePolicySettings(self, settings, policies): 594 """Copies all the policies from a dictionary into a protobuf of type 595 CloudDeviceSettingsProto. 596 597 Args: 598 settings: The destination ChromeDeviceSettingsProto protobuf. 599 policies: The source dictionary containing policies in JSON format. 600 """ 601 for group in settings.DESCRIPTOR.fields: 602 # Create protobuf message for group. 603 group_message = eval('dp.' + group.message_type.name + '()') 604 # Indicates if at least one field was set in |group_message|. 605 got_fields = False 606 # Iterate over fields of the message and feed them from the 607 # policy config file. 608 for field in group_message.DESCRIPTOR.fields: 609 field_value = None 610 if field.name in policies: 611 got_fields = True 612 field_value = policies[field.name] 613 self.SetProtobufMessageField(group_message, field, field_value) 614 if got_fields: 615 settings.__getattribute__(group.name).CopyFrom(group_message) 616 617 def GatherUserPolicySettings(self, settings, policies): 618 """Copies all the policies from a dictionary into a protobuf of type 619 CloudPolicySettings. 620 621 Args: 622 settings: The destination: a CloudPolicySettings protobuf. 623 policies: The source: a dictionary containing policies under keys 624 'recommended' and 'mandatory'. 625 """ 626 for field in settings.DESCRIPTOR.fields: 627 # |field| is the entry for a specific policy in the top-level 628 # CloudPolicySettings proto. 629 630 # Look for this policy's value in the mandatory or recommended dicts. 631 if field.name in policies.get('mandatory', {}): 632 mode = cp.PolicyOptions.MANDATORY 633 value = policies['mandatory'][field.name] 634 elif field.name in policies.get('recommended', {}): 635 mode = cp.PolicyOptions.RECOMMENDED 636 value = policies['recommended'][field.name] 637 else: 638 continue 639 640 # Create protobuf message for this policy. 641 policy_message = eval('cp.' + field.message_type.name + '()') 642 policy_message.policy_options.mode = mode 643 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value'] 644 self.SetProtobufMessageField(policy_message, field_descriptor, value) 645 settings.__getattribute__(field.name).CopyFrom(policy_message) 646 647 def ProcessCloudPolicyForExtensions(self, request, response, token_info, 648 username=None): 649 """Handles a request for policy for extensions. 650 651 A request for policy for extensions is slightly different from the other 652 cloud policy requests, because it can trigger 0, one or many 653 PolicyFetchResponse messages in the response. 654 655 Args: 656 request: The PolicyFetchRequest that triggered this handler. 657 response: The DevicePolicyResponse message for the response. Multiple 658 PolicyFetchResponses will be appended to this message. 659 token_info: The token extracted from the request. 660 username: The username for the response. May be None. 661 """ 662 # Send one PolicyFetchResponse for each extension that has 663 # configuration data at the server. 664 ids = self.server.ListMatchingComponents('google/chrome/extension') 665 for settings_entity_id in ids: 666 # Reuse the extension policy request, to trigger the same signature 667 # type in the response. 668 request.settings_entity_id = settings_entity_id 669 fetch_response = response.response.add() 670 self.ProcessCloudPolicy(request, token_info, fetch_response, username) 671 # Don't do key rotations for these messages. 672 fetch_response.ClearField('new_public_key') 673 fetch_response.ClearField('new_public_key_signature') 674 fetch_response.ClearField('new_public_key_verification_signature') 675 676 def ProcessCloudPolicy(self, msg, token_info, response, username=None): 677 """Handles a cloud policy request. (New protocol for policy requests.) 678 679 Encodes the policy into protobuf representation, signs it and constructs 680 the response. 681 682 Args: 683 msg: The CloudPolicyRequest message received from the client. 684 token_info: The token extracted from the request. 685 response: A PolicyFetchResponse message that should be filled with the 686 response data. 687 username: The username for the response. May be None. 688 """ 689 690 if msg.machine_id: 691 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id) 692 693 # Response is only given if the scope is specified in the config file. 694 # Normally 'google/chromeos/device', 'google/chromeos/user' and 695 # 'google/chromeos/publicaccount' should be accepted. 696 policy = self.server.GetPolicies() 697 policy_value = '' 698 policy_key = msg.policy_type 699 if msg.settings_entity_id: 700 policy_key += '/' + msg.settings_entity_id 701 if msg.policy_type in token_info['allowed_policy_types']: 702 if msg.policy_type in ('google/android/user', 703 'google/chromeos/publicaccount', 704 'google/chromeos/user', 705 'google/chrome/user', 706 'google/ios/user'): 707 settings = cp.CloudPolicySettings() 708 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 709 if payload is None: 710 self.GatherUserPolicySettings(settings, policy.get(policy_key, {})) 711 payload = settings.SerializeToString() 712 elif dp is not None and msg.policy_type == 'google/chromeos/device': 713 settings = dp.ChromeDeviceSettingsProto() 714 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 715 if payload is None: 716 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {})) 717 payload = settings.SerializeToString() 718 elif msg.policy_type == 'google/chrome/extension': 719 settings = ep.ExternalPolicyData() 720 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 721 if payload is None: 722 payload = self.CreatePolicyForExternalPolicyData(policy_key) 723 else: 724 response.error_code = 400 725 response.error_message = 'Invalid policy type' 726 return 727 else: 728 response.error_code = 400 729 response.error_message = 'Request not allowed for the token used' 730 return 731 732 # Sign with 'current_key_index', defaulting to key 0. 733 signing_key = None 734 req_key = None 735 current_key_index = policy.get('current_key_index', 0) 736 nkeys = len(self.server.keys) 737 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and 738 current_key_index in range(nkeys)): 739 signing_key = self.server.keys[current_key_index] 740 if msg.public_key_version in range(1, nkeys + 1): 741 # requested key exists, use for signing and rotate. 742 req_key = self.server.keys[msg.public_key_version - 1]['private_key'] 743 744 # Fill the policy data protobuf. 745 policy_data = dm.PolicyData() 746 policy_data.policy_type = msg.policy_type 747 policy_data.timestamp = int(time.time() * 1000) 748 policy_data.request_token = token_info['device_token'] 749 policy_data.policy_value = payload 750 policy_data.machine_name = token_info['machine_name'] 751 policy_data.valid_serial_number_missing = ( 752 token_info['machine_id'] in BAD_MACHINE_IDS) 753 policy_data.settings_entity_id = msg.settings_entity_id 754 policy_data.service_account_identity = policy.get( 755 'service_account_identity', 756 'policy_testserver.py-service_account_identity') 757 invalidation_source = policy.get('invalidation_source') 758 if invalidation_source is not None: 759 policy_data.invalidation_source = invalidation_source 760 # Since invalidation_name is type bytes in the proto, the Unicode name 761 # provided needs to be encoded as ASCII to set the correct byte pattern. 762 invalidation_name = policy.get('invalidation_name') 763 if invalidation_name is not None: 764 policy_data.invalidation_name = invalidation_name.encode('ascii') 765 766 if signing_key: 767 policy_data.public_key_version = current_key_index + 1 768 769 if username: 770 policy_data.username = username 771 else: 772 # For regular user/device policy, there is no way for the testserver to 773 # know the user name belonging to the GAIA auth token we received (short 774 # of actually talking to GAIA). To address this, we read the username from 775 # the policy configuration dictionary, or use a default. 776 policy_data.username = policy.get('policy_user', 'user (at] example.com') 777 policy_data.device_id = token_info['device_id'] 778 signed_data = policy_data.SerializeToString() 779 780 response.policy_data = signed_data 781 if signing_key: 782 response.policy_data_signature = ( 783 bytes(signing_key['private_key'].hashAndSign(signed_data))) 784 if msg.public_key_version != current_key_index + 1: 785 response.new_public_key = signing_key['public_key'] 786 787 # Set the verification signature appropriate for the policy domain. 788 # TODO(atwilson): Use the enrollment domain for public accounts when 789 # we add key validation for ChromeOS (http://crbug.com/328038). 790 if 'signatures' in signing_key: 791 verification_sig = self.GetSignatureForDomain( 792 signing_key['signatures'], policy_data.username) 793 794 if verification_sig: 795 assert len(verification_sig) == 256, \ 796 'bad signature size: %d' % len(verification_sig) 797 response.new_public_key_verification_signature = verification_sig 798 799 if req_key: 800 response.new_public_key_signature = ( 801 bytes(req_key.hashAndSign(response.new_public_key))) 802 803 return (200, response.SerializeToString()) 804 805 def GetSignatureForDomain(self, signatures, username): 806 parsed_username = username.split("@", 1) 807 if len(parsed_username) != 2: 808 logging.error('Could not extract domain from username: %s' % username) 809 return None 810 domain = parsed_username[1] 811 812 # Lookup the domain's signature in the passed dictionary. If none is found, 813 # fallback to a wildcard signature. 814 if domain in signatures: 815 return signatures[domain] 816 if '*' in signatures: 817 return signatures['*'] 818 819 # No key matching this domain. 820 logging.error('No verification signature matching domain: %s' % domain) 821 return None 822 823 def CheckToken(self): 824 """Helper for checking whether the client supplied a valid DM token. 825 826 Extracts the token from the request and passed to the server in order to 827 look up the client. 828 829 Returns: 830 A pair of token information record and error response. If the first 831 element is None, then the second contains an error code to send back to 832 the client. Otherwise the first element is the same structure that is 833 returned by LookupToken(). 834 """ 835 error = 500 836 dmtoken = None 837 request_device_id = self.GetUniqueParam('deviceid') 838 match = re.match('GoogleDMToken token=(\\w+)', 839 self.headers.getheader('Authorization', '')) 840 if match: 841 dmtoken = match.group(1) 842 if not dmtoken: 843 error = 401 844 else: 845 token_info = self.server.LookupToken(dmtoken) 846 if (not token_info or 847 not request_device_id or 848 token_info['device_id'] != request_device_id): 849 error = 410 850 else: 851 return (token_info, None) 852 853 logging.debug('Token check failed with error %d' % error) 854 855 return (None, (error, 'Server error %d' % error)) 856 857 def DumpMessage(self, label, msg): 858 """Helper for logging an ASCII dump of a protobuf message.""" 859 logging.debug('%s\n%s' % (label, str(msg))) 860 861 862 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn, 863 testserver_base.StoppableHTTPServer): 864 """Handles requests and keeps global service state.""" 865 866 def __init__(self, server_address, data_dir, policy_path, client_state_file, 867 private_key_paths, server_base_url): 868 """Initializes the server. 869 870 Args: 871 server_address: Server host and port. 872 policy_path: Names the file to read JSON-formatted policy from. 873 private_key_paths: List of paths to read private keys from. 874 """ 875 testserver_base.StoppableHTTPServer.__init__(self, server_address, 876 PolicyRequestHandler) 877 self._registered_tokens = {} 878 self.data_dir = data_dir 879 self.policy_path = policy_path 880 self.client_state_file = client_state_file 881 self.server_base_url = server_base_url 882 883 self.keys = [] 884 if private_key_paths: 885 # Load specified keys from the filesystem. 886 for key_path in private_key_paths: 887 try: 888 key_str = open(key_path).read() 889 except IOError: 890 print 'Failed to load private key from %s' % key_path 891 continue 892 try: 893 key = tlslite.api.parsePEMKey(key_str, private=True) 894 except SyntaxError: 895 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8( 896 bytearray(key_str)) 897 898 assert key is not None 899 key_info = { 'private_key' : key } 900 901 # Now try to read in a signature, if one exists. 902 try: 903 key_sig = open(key_path + '.sig').read() 904 # Create a dictionary with the wildcard domain + signature 905 key_info['signatures'] = {'*': key_sig} 906 except IOError: 907 print 'Failed to read validation signature from %s.sig' % key_path 908 self.keys.append(key_info) 909 else: 910 # Use the canned private keys if none were passed from the command line. 911 for signing_key in SIGNING_KEYS: 912 decoded_key = base64.b64decode(signing_key['key']); 913 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8( 914 bytearray(decoded_key)) 915 assert key is not None 916 # Grab the signature dictionary for this key and decode all of the 917 # signatures. 918 signature_dict = signing_key['signatures'] 919 decoded_signatures = {} 920 for domain in signature_dict: 921 decoded_signatures[domain] = base64.b64decode(signature_dict[domain]) 922 self.keys.append({'private_key': key, 923 'signatures': decoded_signatures}) 924 925 # Derive the public keys from the private keys. 926 for entry in self.keys: 927 key = entry['private_key'] 928 929 algorithm = asn1der.Sequence( 930 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID), 931 asn1der.Data(asn1der.NULL, '') ]) 932 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n), 933 asn1der.Integer(key.e) ]) 934 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ]) 935 entry['public_key'] = pubkey 936 937 # Load client state. 938 if self.client_state_file is not None: 939 try: 940 file_contents = open(self.client_state_file).read() 941 self._registered_tokens = json.loads(file_contents, strict=False) 942 except IOError: 943 pass 944 945 def GetPolicies(self): 946 """Returns the policies to be used, reloaded form the backend file every 947 time this is called. 948 """ 949 policy = {} 950 if json is None: 951 print 'No JSON module, cannot parse policy information' 952 else : 953 try: 954 policy = json.loads(open(self.policy_path).read(), strict=False) 955 except IOError: 956 print 'Failed to load policy from %s' % self.policy_path 957 return policy 958 959 def RegisterDevice(self, device_id, machine_id, type): 960 """Registers a device or user and generates a DM token for it. 961 962 Args: 963 device_id: The device identifier provided by the client. 964 965 Returns: 966 The newly generated device token for the device. 967 """ 968 dmtoken_chars = [] 969 while len(dmtoken_chars) < 32: 970 dmtoken_chars.append(random.choice('0123456789abcdef')) 971 dmtoken = ''.join(dmtoken_chars) 972 allowed_policy_types = { 973 dm.DeviceRegisterRequest.BROWSER: [ 974 'google/chrome/user', 975 'google/chrome/extension' 976 ], 977 dm.DeviceRegisterRequest.USER: [ 978 'google/chromeos/user', 979 'google/chrome/extension' 980 ], 981 dm.DeviceRegisterRequest.DEVICE: [ 982 'google/chromeos/device', 983 'google/chromeos/publicaccount', 984 'google/chrome/extension' 985 ], 986 dm.DeviceRegisterRequest.ANDROID_BROWSER: [ 987 'google/android/user' 988 ], 989 dm.DeviceRegisterRequest.IOS_BROWSER: [ 990 'google/ios/user' 991 ], 992 dm.DeviceRegisterRequest.TT: ['google/chromeos/user', 993 'google/chrome/user'], 994 } 995 if machine_id in KIOSK_MACHINE_IDS: 996 enrollment_mode = dm.DeviceRegisterResponse.RETAIL 997 else: 998 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE 999 self._registered_tokens[dmtoken] = { 1000 'device_id': device_id, 1001 'device_token': dmtoken, 1002 'allowed_policy_types': allowed_policy_types[type], 1003 'machine_name': 'chromeos-' + machine_id, 1004 'machine_id': machine_id, 1005 'enrollment_mode': enrollment_mode, 1006 } 1007 self.WriteClientState() 1008 return self._registered_tokens[dmtoken] 1009 1010 def UpdateMachineId(self, dmtoken, machine_id): 1011 """Updates the machine identifier for a registered device. 1012 1013 Args: 1014 dmtoken: The device management token provided by the client. 1015 machine_id: Updated hardware identifier value. 1016 """ 1017 if dmtoken in self._registered_tokens: 1018 self._registered_tokens[dmtoken]['machine_id'] = machine_id 1019 self.WriteClientState() 1020 1021 def UpdateStateKeys(self, dmtoken, state_keys): 1022 """Updates the state keys for a given client. 1023 1024 Args: 1025 dmtoken: The device management token provided by the client. 1026 state_keys: The state keys to set. 1027 """ 1028 if dmtoken in self._registered_tokens: 1029 self._registered_tokens[dmtoken]['state_keys'] = map( 1030 lambda key : key.encode('hex'), state_keys) 1031 self.WriteClientState() 1032 1033 def LookupToken(self, dmtoken): 1034 """Looks up a device or a user by DM token. 1035 1036 Args: 1037 dmtoken: The device management token provided by the client. 1038 1039 Returns: 1040 A dictionary with information about a device or user that is registered by 1041 dmtoken, or None if the token is not found. 1042 """ 1043 return self._registered_tokens.get(dmtoken, None) 1044 1045 def LookupByStateKey(self, state_key): 1046 """Looks up a device or a user by a state key. 1047 1048 Args: 1049 state_key: The state key provided by the client. 1050 1051 Returns: 1052 A dictionary with information about a device or user or None if there is 1053 no matching record. 1054 """ 1055 for client in self._registered_tokens.values(): 1056 if state_key.encode('hex') in client.get('state_keys', []): 1057 return client 1058 1059 return None 1060 1061 def GetMatchingStateKeyHashes(self, modulus, remainder): 1062 """Returns all clients registered with the server. 1063 1064 Returns: 1065 The list of registered clients. 1066 """ 1067 state_keys = sum([ c.get('state_keys', []) 1068 for c in self._registered_tokens.values() ], []) 1069 hashed_keys = map(lambda key: hashlib.sha256(key.decode('hex')).digest(), 1070 set(state_keys)) 1071 return filter( 1072 lambda hash : int(hash.encode('hex'), 16) % modulus == remainder, 1073 hashed_keys) 1074 1075 def UnregisterDevice(self, dmtoken): 1076 """Unregisters a device identified by the given DM token. 1077 1078 Args: 1079 dmtoken: The device management token provided by the client. 1080 """ 1081 if dmtoken in self._registered_tokens.keys(): 1082 del self._registered_tokens[dmtoken] 1083 self.WriteClientState() 1084 1085 def WriteClientState(self): 1086 """Writes the client state back to the file.""" 1087 if self.client_state_file is not None: 1088 json_data = json.dumps(self._registered_tokens) 1089 open(self.client_state_file, 'w').write(json_data) 1090 1091 def GetBaseFilename(self, policy_selector): 1092 """Returns the base filename for the given policy_selector. 1093 1094 Args: 1095 policy_selector: The policy type and settings entity id, joined by '/'. 1096 1097 Returns: 1098 The filename corresponding to the policy_selector, without a file 1099 extension. 1100 """ 1101 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector) 1102 return os.path.join(self.data_dir or '', 1103 'policy_%s' % sanitized_policy_selector) 1104 1105 def ListMatchingComponents(self, policy_type): 1106 """Returns a list of settings entity IDs that have a configuration file. 1107 1108 Args: 1109 policy_type: The policy type to look for. Only settings entity IDs for 1110 file selectors That match this policy_type will be returned. 1111 1112 Returns: 1113 A list of settings entity IDs for the given |policy_type| that have a 1114 configuration file in this server (either as a .bin, .txt or .data file). 1115 """ 1116 base_name = self.GetBaseFilename(policy_type) 1117 files = glob.glob('%s_*.*' % base_name) 1118 len_base_name = len(base_name) + 1 1119 return [ file[len_base_name:file.rfind('.')] for file in files ] 1120 1121 def ReadPolicyFromDataDir(self, policy_selector, proto_message): 1122 """Tries to read policy payload from a file in the data directory. 1123 1124 First checks for a binary rendition of the policy protobuf in 1125 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns 1126 it. If that file doesn't exist, tries 1127 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a 1128 protobuf using proto_message. If that fails as well, returns None. 1129 1130 Args: 1131 policy_selector: Selects which policy to read. 1132 proto_message: Optional protobuf message object used for decoding the 1133 proto text format. 1134 1135 Returns: 1136 The binary payload message, or None if not found. 1137 """ 1138 base_filename = self.GetBaseFilename(policy_selector) 1139 1140 # Try the binary payload file first. 1141 try: 1142 return open(base_filename + '.bin').read() 1143 except IOError: 1144 pass 1145 1146 # If that fails, try the text version instead. 1147 if proto_message is None: 1148 return None 1149 1150 try: 1151 text = open(base_filename + '.txt').read() 1152 google.protobuf.text_format.Merge(text, proto_message) 1153 return proto_message.SerializeToString() 1154 except IOError: 1155 return None 1156 except google.protobuf.text_format.ParseError: 1157 return None 1158 1159 def ReadPolicyDataFromDataDir(self, policy_selector): 1160 """Returns the external policy data for |policy_selector| if found. 1161 1162 Args: 1163 policy_selector: Selects which policy to read. 1164 1165 Returns: 1166 The data for the corresponding policy type and entity id, if found. 1167 """ 1168 base_filename = self.GetBaseFilename(policy_selector) 1169 try: 1170 return open(base_filename + '.data').read() 1171 except IOError: 1172 return None 1173 1174 def GetBaseURL(self): 1175 """Returns the server base URL. 1176 1177 Respects the |server_base_url| configuration parameter, if present. Falls 1178 back to construct the URL from the server hostname and port otherwise. 1179 1180 Returns: 1181 The URL to use for constructing URLs that get returned to clients. 1182 """ 1183 base_url = self.server_base_url 1184 if base_url is None: 1185 base_url = 'http://%s:%s' % self.server_address[:2] 1186 1187 return base_url 1188 1189 1190 class PolicyServerRunner(testserver_base.TestServerRunner): 1191 1192 def __init__(self): 1193 super(PolicyServerRunner, self).__init__() 1194 1195 def create_server(self, server_data): 1196 data_dir = self.options.data_dir or '' 1197 config_file = (self.options.config_file or 1198 os.path.join(data_dir, 'device_management')) 1199 server = PolicyTestServer((self.options.host, self.options.port), 1200 data_dir, config_file, 1201 self.options.client_state_file, 1202 self.options.policy_keys, 1203 self.options.server_base_url) 1204 server_data['port'] = server.server_port 1205 return server 1206 1207 def add_options(self): 1208 testserver_base.TestServerRunner.add_options(self) 1209 self.option_parser.add_option('--client-state', dest='client_state_file', 1210 help='File that client state should be ' 1211 'persisted to. This allows the server to be ' 1212 'seeded by a list of pre-registered clients ' 1213 'and restarts without abandoning registered ' 1214 'clients.') 1215 self.option_parser.add_option('--policy-key', action='append', 1216 dest='policy_keys', 1217 help='Specify a path to a PEM-encoded ' 1218 'private key to use for policy signing. May ' 1219 'be specified multiple times in order to ' 1220 'load multiple keys into the server. If the ' 1221 'server has multiple keys, it will rotate ' 1222 'through them in at each request in a ' 1223 'round-robin fashion. The server will ' 1224 'use a canned key if none is specified ' 1225 'on the command line. The test server will ' 1226 'also look for a verification signature file ' 1227 'in the same location: <filename>.sig and if ' 1228 'present will add the signature to the ' 1229 'policy blob as appropriate via the ' 1230 'new_public_key_verification_signature ' 1231 'field.') 1232 self.option_parser.add_option('--log-level', dest='log_level', 1233 default='WARN', 1234 help='Log level threshold to use.') 1235 self.option_parser.add_option('--config-file', dest='config_file', 1236 help='Specify a configuration file to use ' 1237 'instead of the default ' 1238 '<data_dir>/device_management') 1239 self.option_parser.add_option('--server-base-url', dest='server_base_url', 1240 help='The server base URL to use when ' 1241 'constructing URLs to return to the client.') 1242 1243 def run_server(self): 1244 logger = logging.getLogger() 1245 logger.setLevel(getattr(logging, str(self.options.log_level).upper())) 1246 if (self.options.log_to_console): 1247 logger.addHandler(logging.StreamHandler()) 1248 if (self.options.log_file): 1249 logger.addHandler(logging.FileHandler(self.options.log_file)) 1250 1251 testserver_base.TestServerRunner.run_server(self) 1252 1253 1254 if __name__ == '__main__': 1255 sys.exit(PolicyServerRunner().main()) 1256