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 BaseHTTPServer
     58 import cgi
     59 import google.protobuf.text_format
     60 import hashlib
     61 import logging
     62 import os
     63 import random
     64 import re
     65 import sys
     66 import time
     67 import tlslite
     68 import tlslite.api
     69 import tlslite.utils
     70 import tlslite.utils.cryptomath
     71 import urlparse
     72 
     73 # The name and availability of the json module varies in python versions.
     74 try:
     75   import simplejson as json
     76 except ImportError:
     77   try:
     78     import json
     79   except ImportError:
     80     json = None
     81 
     82 import asn1der
     83 import testserver_base
     84 
     85 import device_management_backend_pb2 as dm
     86 import cloud_policy_pb2 as cp
     87 import chrome_extension_policy_pb2 as ep
     88 
     89 # Device policy is only available on Chrome OS builds.
     90 try:
     91   import chrome_device_policy_pb2 as dp
     92 except ImportError:
     93   dp = None
     94 
     95 # ASN.1 object identifier for PKCS#1/RSA.
     96 PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
     97 
     98 # SHA256 sum of "0".
     99 SHA256_0 = hashlib.sha256('0').digest()
    100 
    101 # List of bad machine identifiers that trigger the |valid_serial_number_missing|
    102 # flag to be set set in the policy fetch response.
    103 BAD_MACHINE_IDS = [ '123490EN400015' ]
    104 
    105 # List of machines that trigger the server to send kiosk enrollment response
    106 # for the register request.
    107 KIOSK_MACHINE_IDS = [ 'KIOSK' ]
    108 
    109 
    110 class PolicyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    111   """Decodes and handles device management requests from clients.
    112 
    113   The handler implements all the request parsing and protobuf message decoding
    114   and encoding. It calls back into the server to lookup, register, and
    115   unregister clients.
    116   """
    117 
    118   def __init__(self, request, client_address, server):
    119     """Initialize the handler.
    120 
    121     Args:
    122       request: The request data received from the client as a string.
    123       client_address: The client address.
    124       server: The TestServer object to use for (un)registering clients.
    125     """
    126     BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
    127                                                    client_address, server)
    128 
    129   def GetUniqueParam(self, name):
    130     """Extracts a unique query parameter from the request.
    131 
    132     Args:
    133       name: Names the parameter to fetch.
    134     Returns:
    135       The parameter value or None if the parameter doesn't exist or is not
    136       unique.
    137     """
    138     if not hasattr(self, '_params'):
    139       self._params = cgi.parse_qs(self.path[self.path.find('?') + 1:])
    140 
    141     param_list = self._params.get(name, [])
    142     if len(param_list) == 1:
    143       return param_list[0]
    144     return None
    145 
    146   def do_GET(self):
    147     """Handles GET requests.
    148 
    149     Currently this is only used to serve external policy data."""
    150     sep = self.path.find('?')
    151     path = self.path if sep == -1 else self.path[:sep]
    152     if path == '/externalpolicydata':
    153       http_response, raw_reply = self.HandleExternalPolicyDataRequest()
    154     else:
    155       http_response = 404
    156       raw_reply = 'Invalid path'
    157     self.send_response(http_response)
    158     self.end_headers()
    159     self.wfile.write(raw_reply)
    160 
    161   def do_POST(self):
    162     http_response, raw_reply = self.HandleRequest()
    163     self.send_response(http_response)
    164     if (http_response == 200):
    165       self.send_header('Content-Type', 'application/x-protobuffer')
    166     self.end_headers()
    167     self.wfile.write(raw_reply)
    168 
    169   def HandleExternalPolicyDataRequest(self):
    170     """Handles a request to download policy data for a component."""
    171     policy_key = self.GetUniqueParam('key')
    172     if not policy_key:
    173       return (400, 'Missing key parameter')
    174     data = self.server.ReadPolicyDataFromDataDir(policy_key)
    175     if data is None:
    176       return (404, 'Policy not found for ' + policy_key)
    177     return (200, data)
    178 
    179   def HandleRequest(self):
    180     """Handles a request.
    181 
    182     Parses the data supplied at construction time and returns a pair indicating
    183     http status code and response data to be sent back to the client.
    184 
    185     Returns:
    186       A tuple of HTTP status code and response data to send to the client.
    187     """
    188     rmsg = dm.DeviceManagementRequest()
    189     length = int(self.headers.getheader('content-length'))
    190     rmsg.ParseFromString(self.rfile.read(length))
    191 
    192     logging.debug('gaia auth token -> ' +
    193                   self.headers.getheader('Authorization', ''))
    194     logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token')))
    195     logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid')))
    196     self.DumpMessage('Request', rmsg)
    197 
    198     request_type = self.GetUniqueParam('request')
    199     # Check server side requirements, as defined in
    200     # device_management_backend.proto.
    201     if (self.GetUniqueParam('devicetype') != '2' or
    202         self.GetUniqueParam('apptype') != 'Chrome' or
    203         (request_type != 'ping' and
    204          len(self.GetUniqueParam('deviceid')) >= 64) or
    205         len(self.GetUniqueParam('agent')) >= 64):
    206       return (400, 'Invalid request parameter')
    207     if request_type == 'register':
    208       return self.ProcessRegister(rmsg.register_request)
    209     if request_type == 'api_authorization':
    210       return self.ProcessApiAuthorization(rmsg.service_api_access_request)
    211     elif request_type == 'unregister':
    212       return self.ProcessUnregister(rmsg.unregister_request)
    213     elif request_type == 'policy' or request_type == 'ping':
    214       return self.ProcessPolicy(rmsg.policy_request, request_type)
    215     elif request_type == 'enterprise_check':
    216       return self.ProcessAutoEnrollment(rmsg.auto_enrollment_request)
    217     else:
    218       return (400, 'Invalid request parameter')
    219 
    220   def CreatePolicyForExternalPolicyData(self, policy_key):
    221     """Returns an ExternalPolicyData protobuf for policy_key.
    222 
    223     If there is policy data for policy_key then the download url will be
    224     set so that it points to that data, and the appropriate hash is also set.
    225     Otherwise, the protobuf will be empty.
    226 
    227     Args:
    228       policy_key: the policy type and settings entity id, joined by '/'.
    229 
    230     Returns:
    231       A serialized ExternalPolicyData.
    232     """
    233     settings = ep.ExternalPolicyData()
    234     data = self.server.ReadPolicyDataFromDataDir(policy_key)
    235     if data:
    236       settings.download_url = urlparse.urljoin(
    237           self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key)
    238       settings.secure_hash = hashlib.sha1(data).digest()
    239     return settings.SerializeToString()
    240 
    241   def CheckGoogleLogin(self):
    242     """Extracts the auth token from the request and returns it. The token may
    243     either be a GoogleLogin token from an Authorization header, or an OAuth V2
    244     token from the oauth_token query parameter. Returns None if no token is
    245     present.
    246     """
    247     oauth_token = self.GetUniqueParam('oauth_token')
    248     if oauth_token:
    249       return oauth_token
    250 
    251     match = re.match('GoogleLogin auth=(\\w+)',
    252                      self.headers.getheader('Authorization', ''))
    253     if match:
    254       return match.group(1)
    255 
    256     return None
    257 
    258   def ProcessRegister(self, msg):
    259     """Handles a register request.
    260 
    261     Checks the query for authorization and device identifier, registers the
    262     device with the server and constructs a response.
    263 
    264     Args:
    265       msg: The DeviceRegisterRequest message received from the client.
    266 
    267     Returns:
    268       A tuple of HTTP status code and response data to send to the client.
    269     """
    270     # Check the auth token and device ID.
    271     auth = self.CheckGoogleLogin()
    272     if not auth:
    273       return (403, 'No authorization')
    274 
    275     policy = self.server.GetPolicies()
    276     if ('*' not in policy['managed_users'] and
    277         auth not in policy['managed_users']):
    278       return (403, 'Unmanaged')
    279 
    280     device_id = self.GetUniqueParam('deviceid')
    281     if not device_id:
    282       return (400, 'Missing device identifier')
    283 
    284     token_info = self.server.RegisterDevice(device_id,
    285                                              msg.machine_id,
    286                                              msg.type)
    287 
    288     # Send back the reply.
    289     response = dm.DeviceManagementResponse()
    290     response.register_response.device_management_token = (
    291         token_info['device_token'])
    292     response.register_response.machine_name = token_info['machine_name']
    293     response.register_response.enrollment_type = token_info['enrollment_mode']
    294 
    295     self.DumpMessage('Response', response)
    296 
    297     return (200, response.SerializeToString())
    298 
    299   def ProcessApiAuthorization(self, msg):
    300     """Handles an API authorization request.
    301 
    302     Args:
    303       msg: The DeviceServiceApiAccessRequest message received from the client.
    304 
    305     Returns:
    306       A tuple of HTTP status code and response data to send to the client.
    307     """
    308     policy = self.server.GetPolicies()
    309 
    310     # Return the auth code from the config file if it's defined,
    311     # else return a descriptive default value.
    312     response = dm.DeviceManagementResponse()
    313     response.service_api_access_response.auth_code = policy.get(
    314         'robot_api_auth_code', 'policy_testserver.py-auth_code')
    315     self.DumpMessage('Response', response)
    316 
    317     return (200, response.SerializeToString())
    318 
    319   def ProcessUnregister(self, msg):
    320     """Handles a register request.
    321 
    322     Checks for authorization, unregisters the device and constructs the
    323     response.
    324 
    325     Args:
    326       msg: The DeviceUnregisterRequest message received from the client.
    327 
    328     Returns:
    329       A tuple of HTTP status code and response data to send to the client.
    330     """
    331     # Check the management token.
    332     token, response = self.CheckToken()
    333     if not token:
    334       return response
    335 
    336     # Unregister the device.
    337     self.server.UnregisterDevice(token['device_token'])
    338 
    339     # Prepare and send the response.
    340     response = dm.DeviceManagementResponse()
    341     response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
    342 
    343     self.DumpMessage('Response', response)
    344 
    345     return (200, response.SerializeToString())
    346 
    347   def ProcessPolicy(self, msg, request_type):
    348     """Handles a policy request.
    349 
    350     Checks for authorization, encodes the policy into protobuf representation
    351     and constructs the response.
    352 
    353     Args:
    354       msg: The DevicePolicyRequest 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     token_info, error = self.CheckToken()
    360     if not token_info:
    361       return error
    362 
    363     response = dm.DeviceManagementResponse()
    364     for request in msg.request:
    365       fetch_response = response.policy_response.response.add()
    366       if (request.policy_type in
    367              ('google/chrome/user',
    368               'google/chromeos/user',
    369               'google/chromeos/device',
    370               'google/chromeos/publicaccount',
    371               'google/chrome/extension')):
    372         if request_type != 'policy':
    373           fetch_response.error_code = 400
    374           fetch_response.error_message = 'Invalid request type'
    375         else:
    376           self.ProcessCloudPolicy(request, token_info, fetch_response)
    377       else:
    378         fetch_response.error_code = 400
    379         fetch_response.error_message = 'Invalid policy_type'
    380 
    381     return (200, response.SerializeToString())
    382 
    383   def ProcessAutoEnrollment(self, msg):
    384     """Handles an auto-enrollment check request.
    385 
    386     The reply depends on the value of the modulus:
    387       1: replies with no new modulus and the sha256 hash of "0"
    388       2: replies with a new modulus, 4.
    389       4: replies with a new modulus, 2.
    390       8: fails with error 400.
    391       16: replies with a new modulus, 16.
    392       32: replies with a new modulus, 1.
    393       anything else: replies with no new modulus and an empty list of hashes
    394 
    395     These allow the client to pick the testing scenario its wants to simulate.
    396 
    397     Args:
    398       msg: The DeviceAutoEnrollmentRequest message received from the client.
    399 
    400     Returns:
    401       A tuple of HTTP status code and response data to send to the client.
    402     """
    403     auto_enrollment_response = dm.DeviceAutoEnrollmentResponse()
    404 
    405     if msg.modulus == 1:
    406       auto_enrollment_response.hash.append(SHA256_0)
    407     elif msg.modulus == 2:
    408       auto_enrollment_response.expected_modulus = 4
    409     elif msg.modulus == 4:
    410       auto_enrollment_response.expected_modulus = 2
    411     elif msg.modulus == 8:
    412       return (400, 'Server error')
    413     elif msg.modulus == 16:
    414       auto_enrollment_response.expected_modulus = 16
    415     elif msg.modulus == 32:
    416       auto_enrollment_response.expected_modulus = 1
    417 
    418     response = dm.DeviceManagementResponse()
    419     response.auto_enrollment_response.CopyFrom(auto_enrollment_response)
    420     return (200, response.SerializeToString())
    421 
    422   def SetProtobufMessageField(self, group_message, field, field_value):
    423     '''Sets a field in a protobuf message.
    424 
    425     Args:
    426       group_message: The protobuf message.
    427       field: The field of the message to set, it should be a member of
    428           group_message.DESCRIPTOR.fields.
    429       field_value: The value to set.
    430     '''
    431     if field.label == field.LABEL_REPEATED:
    432       assert type(field_value) == list
    433       entries = group_message.__getattribute__(field.name)
    434       if field.message_type is None:
    435         for list_item in field_value:
    436           entries.append(list_item)
    437       else:
    438         # This field is itself a protobuf.
    439         sub_type = field.message_type
    440         for sub_value in field_value:
    441           assert type(sub_value) == dict
    442           # Add a new sub-protobuf per list entry.
    443           sub_message = entries.add()
    444           # Now iterate over its fields and recursively add them.
    445           for sub_field in sub_message.DESCRIPTOR.fields:
    446             if sub_field.name in sub_value:
    447               value = sub_value[sub_field.name]
    448               self.SetProtobufMessageField(sub_message, sub_field, value)
    449       return
    450     elif field.type == field.TYPE_BOOL:
    451       assert type(field_value) == bool
    452     elif field.type == field.TYPE_STRING:
    453       assert type(field_value) == str or type(field_value) == unicode
    454     elif field.type == field.TYPE_INT64:
    455       assert type(field_value) == int
    456     elif (field.type == field.TYPE_MESSAGE and
    457           field.message_type.name == 'StringList'):
    458       assert type(field_value) == list
    459       entries = group_message.__getattribute__(field.name).entries
    460       for list_item in field_value:
    461         entries.append(list_item)
    462       return
    463     else:
    464       raise Exception('Unknown field type %s' % field.type)
    465     group_message.__setattr__(field.name, field_value)
    466 
    467   def GatherDevicePolicySettings(self, settings, policies):
    468     '''Copies all the policies from a dictionary into a protobuf of type
    469     CloudDeviceSettingsProto.
    470 
    471     Args:
    472       settings: The destination ChromeDeviceSettingsProto protobuf.
    473       policies: The source dictionary containing policies in JSON format.
    474     '''
    475     for group in settings.DESCRIPTOR.fields:
    476       # Create protobuf message for group.
    477       group_message = eval('dp.' + group.message_type.name + '()')
    478       # Indicates if at least one field was set in |group_message|.
    479       got_fields = False
    480       # Iterate over fields of the message and feed them from the
    481       # policy config file.
    482       for field in group_message.DESCRIPTOR.fields:
    483         field_value = None
    484         if field.name in policies:
    485           got_fields = True
    486           field_value = policies[field.name]
    487           self.SetProtobufMessageField(group_message, field, field_value)
    488       if got_fields:
    489         settings.__getattribute__(group.name).CopyFrom(group_message)
    490 
    491   def GatherUserPolicySettings(self, settings, policies):
    492     '''Copies all the policies from a dictionary into a protobuf of type
    493     CloudPolicySettings.
    494 
    495     Args:
    496       settings: The destination: a CloudPolicySettings protobuf.
    497       policies: The source: a dictionary containing policies under keys
    498           'recommended' and 'mandatory'.
    499     '''
    500     for field in settings.DESCRIPTOR.fields:
    501       # |field| is the entry for a specific policy in the top-level
    502       # CloudPolicySettings proto.
    503 
    504       # Look for this policy's value in the mandatory or recommended dicts.
    505       if field.name in policies.get('mandatory', {}):
    506         mode = cp.PolicyOptions.MANDATORY
    507         value = policies['mandatory'][field.name]
    508       elif field.name in policies.get('recommended', {}):
    509         mode = cp.PolicyOptions.RECOMMENDED
    510         value = policies['recommended'][field.name]
    511       else:
    512         continue
    513 
    514       # Create protobuf message for this policy.
    515       policy_message = eval('cp.' + field.message_type.name + '()')
    516       policy_message.policy_options.mode = mode
    517       field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value']
    518       self.SetProtobufMessageField(policy_message, field_descriptor, value)
    519       settings.__getattribute__(field.name).CopyFrom(policy_message)
    520 
    521   def ProcessCloudPolicy(self, msg, token_info, response):
    522     """Handles a cloud policy request. (New protocol for policy requests.)
    523 
    524     Encodes the policy into protobuf representation, signs it and constructs
    525     the response.
    526 
    527     Args:
    528       msg: The CloudPolicyRequest message received from the client.
    529       token_info: the token extracted from the request.
    530       response: A PolicyFetchResponse message that should be filled with the
    531                 response data.
    532     """
    533 
    534     if msg.machine_id:
    535       self.server.UpdateMachineId(token_info['device_token'], msg.machine_id)
    536 
    537     # Response is only given if the scope is specified in the config file.
    538     # Normally 'google/chromeos/device', 'google/chromeos/user' and
    539     # 'google/chromeos/publicaccount' should be accepted.
    540     policy = self.server.GetPolicies()
    541     policy_value = ''
    542     policy_key = msg.policy_type
    543     if msg.settings_entity_id:
    544       policy_key += '/' + msg.settings_entity_id
    545     if msg.policy_type in token_info['allowed_policy_types']:
    546       if (msg.policy_type == 'google/chromeos/user' or
    547           msg.policy_type == 'google/chrome/user' or
    548           msg.policy_type == 'google/chromeos/publicaccount'):
    549         settings = cp.CloudPolicySettings()
    550         payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
    551         if payload is None:
    552           self.GatherUserPolicySettings(settings, policy.get(policy_key, {}))
    553           payload = settings.SerializeToString()
    554       elif dp is not None and msg.policy_type == 'google/chromeos/device':
    555         settings = dp.ChromeDeviceSettingsProto()
    556         payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
    557         if payload is None:
    558           self.GatherDevicePolicySettings(settings, policy.get(policy_key, {}))
    559           payload = settings.SerializeToString()
    560       elif msg.policy_type == 'google/chrome/extension':
    561         settings = ep.ExternalPolicyData()
    562         payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
    563         if payload is None:
    564           payload = self.CreatePolicyForExternalPolicyData(policy_key)
    565       else:
    566         response.error_code = 400
    567         response.error_message = 'Invalid policy type'
    568         return
    569     else:
    570       response.error_code = 400
    571       response.error_message = 'Request not allowed for the token used'
    572       return
    573 
    574     # Sign with 'current_key_index', defaulting to key 0.
    575     signing_key = None
    576     req_key = None
    577     current_key_index = policy.get('current_key_index', 0)
    578     nkeys = len(self.server.keys)
    579     if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and
    580         current_key_index in range(nkeys)):
    581       signing_key = self.server.keys[current_key_index]
    582       if msg.public_key_version in range(1, nkeys + 1):
    583         # requested key exists, use for signing and rotate.
    584         req_key = self.server.keys[msg.public_key_version - 1]['private_key']
    585 
    586     # Fill the policy data protobuf.
    587     policy_data = dm.PolicyData()
    588     policy_data.policy_type = msg.policy_type
    589     policy_data.timestamp = int(time.time() * 1000)
    590     policy_data.request_token = token_info['device_token']
    591     policy_data.policy_value = payload
    592     policy_data.machine_name = token_info['machine_name']
    593     policy_data.valid_serial_number_missing = (
    594         token_info['machine_id'] in BAD_MACHINE_IDS)
    595     policy_data.settings_entity_id = msg.settings_entity_id
    596     policy_data.service_account_identity = policy.get(
    597         'service_account_identity',
    598         'policy_testserver.py-service_account_identity')
    599     invalidation_source = policy.get('invalidation_source')
    600     if invalidation_source is not None:
    601       policy_data.invalidation_source = invalidation_source
    602     # Since invalidation_name is type bytes in the proto, the Unicode name
    603     # provided needs to be encoded as ASCII to set the correct byte pattern.
    604     invalidation_name = policy.get('invalidation_name')
    605     if invalidation_name is not None:
    606       policy_data.invalidation_name = invalidation_name.encode('ascii')
    607 
    608     if signing_key:
    609       policy_data.public_key_version = current_key_index + 1
    610     if msg.policy_type == 'google/chromeos/publicaccount':
    611       policy_data.username = msg.settings_entity_id
    612     else:
    613       # For regular user/device policy, there is no way for the testserver to
    614       # know the user name belonging to the GAIA auth token we received (short
    615       # of actually talking to GAIA). To address this, we read the username from
    616       # the policy configuration dictionary, or use a default.
    617       policy_data.username = policy.get('policy_user', 'user (at] example.com')
    618     policy_data.device_id = token_info['device_id']
    619     signed_data = policy_data.SerializeToString()
    620 
    621     response.policy_data = signed_data
    622     if signing_key:
    623       response.policy_data_signature = (
    624           signing_key['private_key'].hashAndSign(signed_data).tostring())
    625       if msg.public_key_version != current_key_index + 1:
    626         response.new_public_key = signing_key['public_key']
    627         if req_key:
    628           response.new_public_key_signature = (
    629               req_key.hashAndSign(response.new_public_key).tostring())
    630 
    631     self.DumpMessage('Response', response)
    632 
    633     return (200, response.SerializeToString())
    634 
    635   def CheckToken(self):
    636     """Helper for checking whether the client supplied a valid DM token.
    637 
    638     Extracts the token from the request and passed to the server in order to
    639     look up the client.
    640 
    641     Returns:
    642       A pair of token information record and error response. If the first
    643       element is None, then the second contains an error code to send back to
    644       the client. Otherwise the first element is the same structure that is
    645       returned by LookupToken().
    646     """
    647     error = 500
    648     dmtoken = None
    649     request_device_id = self.GetUniqueParam('deviceid')
    650     match = re.match('GoogleDMToken token=(\\w+)',
    651                      self.headers.getheader('Authorization', ''))
    652     if match:
    653       dmtoken = match.group(1)
    654     if not dmtoken:
    655       error = 401
    656     else:
    657       token_info = self.server.LookupToken(dmtoken)
    658       if (not token_info or
    659           not request_device_id or
    660           token_info['device_id'] != request_device_id):
    661         error = 410
    662       else:
    663         return (token_info, None)
    664 
    665     logging.debug('Token check failed with error %d' % error)
    666 
    667     return (None, (error, 'Server error %d' % error))
    668 
    669   def DumpMessage(self, label, msg):
    670     """Helper for logging an ASCII dump of a protobuf message."""
    671     logging.debug('%s\n%s' % (label, str(msg)))
    672 
    673 
    674 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn,
    675                        testserver_base.StoppableHTTPServer):
    676   """Handles requests and keeps global service state."""
    677 
    678   def __init__(self, server_address, data_dir, policy_path, client_state_file,
    679                private_key_paths, server_base_url):
    680     """Initializes the server.
    681 
    682     Args:
    683       server_address: Server host and port.
    684       policy_path: Names the file to read JSON-formatted policy from.
    685       private_key_paths: List of paths to read private keys from.
    686     """
    687     testserver_base.StoppableHTTPServer.__init__(self, server_address,
    688                                                  PolicyRequestHandler)
    689     self._registered_tokens = {}
    690     self.data_dir = data_dir
    691     self.policy_path = policy_path
    692     self.client_state_file = client_state_file
    693     self.server_base_url = server_base_url
    694 
    695     self.keys = []
    696     if private_key_paths:
    697       # Load specified keys from the filesystem.
    698       for key_path in private_key_paths:
    699         try:
    700           key_str = open(key_path).read()
    701         except IOError:
    702           print 'Failed to load private key from %s' % key_path
    703           continue
    704 
    705         try:
    706           key = tlslite.api.parsePEMKey(key_str, private=True)
    707         except SyntaxError:
    708           key = tlslite.utils.Python_RSAKey.Python_RSAKey._parsePKCS8(
    709               tlslite.utils.cryptomath.stringToBytes(key_str))
    710 
    711         assert key is not None
    712         self.keys.append({ 'private_key' : key })
    713     else:
    714       # Generate 2 private keys if none were passed from the command line.
    715       for i in range(2):
    716         key = tlslite.api.generateRSAKey(512)
    717         assert key is not None
    718         self.keys.append({ 'private_key' : key })
    719 
    720     # Derive the public keys from the private keys.
    721     for entry in self.keys:
    722       key = entry['private_key']
    723 
    724       algorithm = asn1der.Sequence(
    725           [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
    726             asn1der.Data(asn1der.NULL, '') ])
    727       rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
    728                                       asn1der.Integer(key.e) ])
    729       pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
    730       entry['public_key'] = pubkey
    731 
    732     # Load client state.
    733     if self.client_state_file is not None:
    734       try:
    735         file_contents = open(self.client_state_file).read()
    736         self._registered_tokens = json.loads(file_contents, strict=False)
    737       except IOError:
    738         pass
    739 
    740   def GetPolicies(self):
    741     """Returns the policies to be used, reloaded form the backend file every
    742        time this is called.
    743     """
    744     policy = {}
    745     if json is None:
    746       print 'No JSON module, cannot parse policy information'
    747     else :
    748       try:
    749         policy = json.loads(open(self.policy_path).read(), strict=False)
    750       except IOError:
    751         print 'Failed to load policy from %s' % self.policy_path
    752     return policy
    753 
    754   def RegisterDevice(self, device_id, machine_id, type):
    755     """Registers a device or user and generates a DM token for it.
    756 
    757     Args:
    758       device_id: The device identifier provided by the client.
    759 
    760     Returns:
    761       The newly generated device token for the device.
    762     """
    763     dmtoken_chars = []
    764     while len(dmtoken_chars) < 32:
    765       dmtoken_chars.append(random.choice('0123456789abcdef'))
    766     dmtoken = ''.join(dmtoken_chars)
    767     allowed_policy_types = {
    768       dm.DeviceRegisterRequest.BROWSER: [
    769           'google/chrome/user',
    770           'google/chrome/extension'
    771       ],
    772       dm.DeviceRegisterRequest.USER: [
    773           'google/chromeos/user',
    774           'google/chrome/extension'
    775       ],
    776       dm.DeviceRegisterRequest.DEVICE: [
    777           'google/chromeos/device',
    778           'google/chromeos/publicaccount'
    779       ],
    780       dm.DeviceRegisterRequest.TT: ['google/chromeos/user',
    781                                     'google/chrome/user'],
    782     }
    783     if machine_id in KIOSK_MACHINE_IDS:
    784       enrollment_mode = dm.DeviceRegisterResponse.RETAIL
    785     else:
    786       enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE
    787     self._registered_tokens[dmtoken] = {
    788       'device_id': device_id,
    789       'device_token': dmtoken,
    790       'allowed_policy_types': allowed_policy_types[type],
    791       'machine_name': 'chromeos-' + machine_id,
    792       'machine_id': machine_id,
    793       'enrollment_mode': enrollment_mode,
    794     }
    795     self.WriteClientState()
    796     return self._registered_tokens[dmtoken]
    797 
    798   def UpdateMachineId(self, dmtoken, machine_id):
    799     """Updates the machine identifier for a registered device.
    800 
    801     Args:
    802       dmtoken: The device management token provided by the client.
    803       machine_id: Updated hardware identifier value.
    804     """
    805     if dmtoken in self._registered_tokens:
    806       self._registered_tokens[dmtoken]['machine_id'] = machine_id
    807       self.WriteClientState()
    808 
    809   def LookupToken(self, dmtoken):
    810     """Looks up a device or a user by DM token.
    811 
    812     Args:
    813       dmtoken: The device management token provided by the client.
    814 
    815     Returns:
    816       A dictionary with information about a device or user that is registered by
    817       dmtoken, or None if the token is not found.
    818     """
    819     return self._registered_tokens.get(dmtoken, None)
    820 
    821   def UnregisterDevice(self, dmtoken):
    822     """Unregisters a device identified by the given DM token.
    823 
    824     Args:
    825       dmtoken: The device management token provided by the client.
    826     """
    827     if dmtoken in self._registered_tokens.keys():
    828       del self._registered_tokens[dmtoken]
    829       self.WriteClientState()
    830 
    831   def WriteClientState(self):
    832     """Writes the client state back to the file."""
    833     if self.client_state_file is not None:
    834       json_data = json.dumps(self._registered_tokens)
    835       open(self.client_state_file, 'w').write(json_data)
    836 
    837   def GetBaseFilename(self, policy_selector):
    838     """Returns the base filename for the given policy_selector.
    839 
    840     Args:
    841       policy_selector: the policy type and settings entity id, joined by '/'.
    842 
    843     Returns:
    844       The filename corresponding to the policy_selector, without a file
    845       extension.
    846     """
    847     sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector)
    848     return os.path.join(self.data_dir or '',
    849                         'policy_%s' % sanitized_policy_selector)
    850 
    851   def ReadPolicyFromDataDir(self, policy_selector, proto_message):
    852     """Tries to read policy payload from a file in the data directory.
    853 
    854     First checks for a binary rendition of the policy protobuf in
    855     <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns
    856     it. If that file doesn't exist, tries
    857     <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a
    858     protobuf using proto_message. If that fails as well, returns None.
    859 
    860     Args:
    861       policy_selector: Selects which policy to read.
    862       proto_message: Optional protobuf message object used for decoding the
    863           proto text format.
    864 
    865     Returns:
    866       The binary payload message, or None if not found.
    867     """
    868     base_filename = self.GetBaseFilename(policy_selector)
    869 
    870     # Try the binary payload file first.
    871     try:
    872       return open(base_filename + '.bin').read()
    873     except IOError:
    874       pass
    875 
    876     # If that fails, try the text version instead.
    877     if proto_message is None:
    878       return None
    879 
    880     try:
    881       text = open(base_filename + '.txt').read()
    882       google.protobuf.text_format.Merge(text, proto_message)
    883       return proto_message.SerializeToString()
    884     except IOError:
    885       return None
    886     except google.protobuf.text_format.ParseError:
    887       return None
    888 
    889   def ReadPolicyDataFromDataDir(self, policy_selector):
    890     """Returns the external policy data for |policy_selector| if found.
    891 
    892     Args:
    893       policy_selector: Selects which policy to read.
    894 
    895     Returns:
    896       The data for the corresponding policy type and entity id, if found.
    897     """
    898     base_filename = self.GetBaseFilename(policy_selector)
    899     try:
    900       return open(base_filename + '.data').read()
    901     except IOError:
    902       return None
    903 
    904   def GetBaseURL(self):
    905     """Returns the server base URL.
    906 
    907     Respects the |server_base_url| configuration parameter, if present. Falls
    908     back to construct the URL from the server hostname and port otherwise.
    909 
    910     Returns:
    911       The URL to use for constructing URLs that get returned to clients.
    912     """
    913     base_url = self.server_base_url
    914     if base_url is None:
    915       base_url = 'http://%s:%s' % self.server_address[:2]
    916 
    917     return base_url
    918 
    919 
    920 class PolicyServerRunner(testserver_base.TestServerRunner):
    921 
    922   def __init__(self):
    923     super(PolicyServerRunner, self).__init__()
    924 
    925   def create_server(self, server_data):
    926     data_dir = self.options.data_dir or ''
    927     config_file = (self.options.config_file or
    928                    os.path.join(data_dir, 'device_management'))
    929     server = PolicyTestServer((self.options.host, self.options.port),
    930                               data_dir, config_file,
    931                               self.options.client_state_file,
    932                               self.options.policy_keys,
    933                               self.options.server_base_url)
    934     server_data['port'] = server.server_port
    935     return server
    936 
    937   def add_options(self):
    938     testserver_base.TestServerRunner.add_options(self)
    939     self.option_parser.add_option('--client-state', dest='client_state_file',
    940                                   help='File that client state should be '
    941                                   'persisted to. This allows the server to be '
    942                                   'seeded by a list of pre-registered clients '
    943                                   'and restarts without abandoning registered '
    944                                   'clients.')
    945     self.option_parser.add_option('--policy-key', action='append',
    946                                   dest='policy_keys',
    947                                   help='Specify a path to a PEM-encoded '
    948                                   'private key to use for policy signing. May '
    949                                   'be specified multiple times in order to '
    950                                   'load multipe keys into the server. If the '
    951                                   'server has multiple keys, it will rotate '
    952                                   'through them in at each request in a '
    953                                   'round-robin fashion. The server will '
    954                                   'generate a random key if none is specified '
    955                                   'on the command line.')
    956     self.option_parser.add_option('--log-level', dest='log_level',
    957                                   default='WARN',
    958                                   help='Log level threshold to use.')
    959     self.option_parser.add_option('--config-file', dest='config_file',
    960                                   help='Specify a configuration file to use '
    961                                   'instead of the default '
    962                                   '<data_dir>/device_management')
    963     self.option_parser.add_option('--server-base-url', dest='server_base_url',
    964                                   help='The server base URL to use when '
    965                                   'constructing URLs to return to the client.')
    966 
    967   def run_server(self):
    968     logger = logging.getLogger()
    969     logger.setLevel(getattr(logging, str(self.options.log_level).upper()))
    970     if (self.options.log_to_console):
    971       logger.addHandler(logging.StreamHandler())
    972     if (self.options.log_file):
    973       logger.addHandler(logging.FileHandler(self.options.log_file))
    974 
    975     testserver_base.TestServerRunner.run_server(self)
    976 
    977 
    978 if __name__ == '__main__':
    979   sys.exit(PolicyServerRunner().main())
    980