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