Home | History | Annotate | Download | only in test
      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