Home | History | Annotate | Download | only in testserver
      1 #!/usr/bin/python2.5
      2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """A bare-bones test server for testing cloud policy support.
      7 
      8 This implements a simple cloud policy test server that can be used to test
      9 chrome's device management service client. The policy information is read from
     10 the file named device_management in the server's data directory. It contains
     11 enforced and recommended policies for the device and user scope, and a list
     12 of managed users.
     13 
     14 The format of the file is JSON. The root dictionary contains a list under the
     15 key "managed_users". It contains auth tokens for which the server will claim
     16 that the user is managed. The token string "*" indicates that all users are
     17 claimed to be managed. Other keys in the root dictionary identify request
     18 scopes. Each request scope is described by a dictionary that holds two
     19 sub-dictionaries: "mandatory" and "recommended". Both these hold the policy
     20 definitions as key/value stores, their format is identical to what the Linux
     21 implementation reads from /etc.
     22 
     23 Example:
     24 
     25 {
     26   "chromeos/device": {
     27     "mandatory": {
     28       "HomepageLocation" : "http://www.chromium.org"
     29     },
     30     "recommended": {
     31       "JavascriptEnabled": false,
     32     },
     33   },
     34   "managed_users": [
     35     "secret123456"
     36   ]
     37 }
     38 
     39 
     40 """
     41 
     42 import cgi
     43 import logging
     44 import os
     45 import random
     46 import re
     47 import sys
     48 import time
     49 import tlslite
     50 import tlslite.api
     51 import tlslite.utils
     52 
     53 # The name and availability of the json module varies in python versions.
     54 try:
     55   import simplejson as json
     56 except ImportError:
     57   try:
     58     import json
     59   except ImportError:
     60     json = None
     61 
     62 import asn1der
     63 import device_management_backend_pb2 as dm
     64 import cloud_policy_pb2 as cp
     65 import chrome_device_policy_pb2 as dp
     66 
     67 # ASN.1 object identifier for PKCS#1/RSA.
     68 PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
     69 
     70 class RequestHandler(object):
     71   """Decodes and handles device management requests from clients.
     72 
     73   The handler implements all the request parsing and protobuf message decoding
     74   and encoding. It calls back into the server to lookup, register, and
     75   unregister clients.
     76   """
     77 
     78   def __init__(self, server, path, headers, request):
     79     """Initialize the handler.
     80 
     81     Args:
     82       server: The TestServer object to use for (un)registering clients.
     83       path: A string containing the request path and query parameters.
     84       headers: A rfc822.Message-like object containing HTTP headers.
     85       request: The request data received from the client as a string.
     86     """
     87     self._server = server
     88     self._path = path
     89     self._headers = headers
     90     self._request = request
     91     self._params = None
     92 
     93   def GetUniqueParam(self, name):
     94     """Extracts a unique query parameter from the request.
     95 
     96     Args:
     97       name: Names the parameter to fetch.
     98     Returns:
     99       The parameter value or None if the parameter doesn't exist or is not
    100       unique.
    101     """
    102     if not self._params:
    103       self._params = cgi.parse_qs(self._path[self._path.find('?') + 1:])
    104 
    105     param_list = self._params.get(name, [])
    106     if len(param_list) == 1:
    107       return param_list[0]
    108     return None;
    109 
    110   def HandleRequest(self):
    111     """Handles a request.
    112 
    113     Parses the data supplied at construction time and returns a pair indicating
    114     http status code and response data to be sent back to the client.
    115 
    116     Returns:
    117       A tuple of HTTP status code and response data to send to the client.
    118     """
    119     rmsg = dm.DeviceManagementRequest()
    120     rmsg.ParseFromString(self._request)
    121 
    122     logging.debug('auth -> ' + self._headers.getheader('Authorization', ''))
    123     logging.debug('deviceid -> ' + self.GetUniqueParam('deviceid'))
    124     self.DumpMessage('Request', rmsg)
    125 
    126     request_type = self.GetUniqueParam('request')
    127     # Check server side requirements, as defined in
    128     # device_management_backend.proto.
    129     if (self.GetUniqueParam('devicetype') != '2' or
    130         self.GetUniqueParam('apptype') != 'Chrome' or
    131         (request_type != 'ping' and
    132          len(self.GetUniqueParam('deviceid')) >= 64) or
    133         len(self.GetUniqueParam('agent')) >= 64):
    134       return (400, 'Invalid request parameter')
    135     if request_type == 'register':
    136       return self.ProcessRegister(rmsg.register_request)
    137     elif request_type == 'unregister':
    138       return self.ProcessUnregister(rmsg.unregister_request)
    139     elif request_type == 'policy' or request_type == 'ping':
    140       return self.ProcessPolicy(rmsg.policy_request, request_type)
    141     else:
    142       return (400, 'Invalid request parameter')
    143 
    144   def CheckGoogleLogin(self):
    145     """Extracts the GoogleLogin auth token from the HTTP request, and
    146     returns it. Returns None if the token is not present.
    147     """
    148     match = re.match('GoogleLogin auth=(\\w+)',
    149                      self._headers.getheader('Authorization', ''))
    150     if not match:
    151       return None
    152     return match.group(1)
    153 
    154   def ProcessRegister(self, msg):
    155     """Handles a register request.
    156 
    157     Checks the query for authorization and device identifier, registers the
    158     device with the server and constructs a response.
    159 
    160     Args:
    161       msg: The DeviceRegisterRequest message received from the client.
    162 
    163     Returns:
    164       A tuple of HTTP status code and response data to send to the client.
    165     """
    166     # Check the auth token and device ID.
    167     if not self.CheckGoogleLogin():
    168       return (403, 'No authorization')
    169 
    170     device_id = self.GetUniqueParam('deviceid')
    171     if not device_id:
    172       return (400, 'Missing device identifier')
    173 
    174     token_info = self._server.RegisterDevice(device_id,
    175                                              msg.machine_id,
    176                                              msg.type)
    177 
    178     # Send back the reply.
    179     response = dm.DeviceManagementResponse()
    180     response.register_response.device_management_token = (
    181         token_info['device_token'])
    182     response.register_response.machine_name = token_info['machine_name']
    183 
    184     self.DumpMessage('Response', response)
    185 
    186     return (200, response.SerializeToString())
    187 
    188   def ProcessUnregister(self, msg):
    189     """Handles a register request.
    190 
    191     Checks for authorization, unregisters the device and constructs the
    192     response.
    193 
    194     Args:
    195       msg: The DeviceUnregisterRequest message received from the client.
    196 
    197     Returns:
    198       A tuple of HTTP status code and response data to send to the client.
    199     """
    200     # Check the management token.
    201     token, response = self.CheckToken();
    202     if not token:
    203       return response
    204 
    205     # Unregister the device.
    206     self._server.UnregisterDevice(token);
    207 
    208     # Prepare and send the response.
    209     response = dm.DeviceManagementResponse()
    210     response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
    211 
    212     self.DumpMessage('Response', response)
    213 
    214     return (200, response.SerializeToString())
    215 
    216   def ProcessInitialPolicy(self, msg):
    217     """Handles a 'preregister policy' request.
    218 
    219     Queries the list of managed users and responds the client if their user
    220     is managed or not.
    221 
    222     Args:
    223       msg: The PolicyFetchRequest message received from the client.
    224 
    225     Returns:
    226       A tuple of HTTP status code and response data to send to the client.
    227     """
    228     # Check the GAIA token.
    229     auth = self.CheckGoogleLogin()
    230     if not auth:
    231       return (403, 'No authorization')
    232 
    233     chrome_initial_settings = dm.ChromeInitialSettingsProto()
    234     if ('*' in self._server.policy['managed_users'] or
    235         auth in self._server.policy['managed_users']):
    236       chrome_initial_settings.enrollment_provision = (
    237           dm.ChromeInitialSettingsProto.MANAGED);
    238     else:
    239       chrome_initial_settings.enrollment_provision = (
    240           dm.ChromeInitialSettingsProto.UNMANAGED);
    241 
    242     policy_data = dm.PolicyData()
    243     policy_data.policy_type = msg.policy_type
    244     policy_data.policy_value = chrome_initial_settings.SerializeToString()
    245 
    246     # Prepare and send the response.
    247     response = dm.DeviceManagementResponse()
    248     fetch_response = response.policy_response.response.add()
    249     fetch_response.policy_data = (
    250         policy_data.SerializeToString())
    251 
    252     self.DumpMessage('Response', response)
    253 
    254     return (200, response.SerializeToString())
    255 
    256   def ProcessDevicePolicy(self, msg):
    257     """Handles a policy request that uses the deprecated protcol.
    258     TODO(gfeher): Remove this when we certainly don't need it.
    259 
    260     Checks for authorization, encodes the policy into protobuf representation
    261     and constructs the response.
    262 
    263     Args:
    264       msg: The DevicePolicyRequest message received from the client.
    265 
    266     Returns:
    267       A tuple of HTTP status code and response data to send to the client.
    268     """
    269 
    270     # Check the management token.
    271     token, response = self.CheckToken()
    272     if not token:
    273       return response
    274 
    275     # Stuff the policy dictionary into a response message and send it back.
    276     response = dm.DeviceManagementResponse()
    277     response.policy_response.CopyFrom(dm.DevicePolicyResponse())
    278 
    279     # Respond only if the client requested policy for the cros/device scope,
    280     # since that's where chrome policy is supposed to live in.
    281     if msg.policy_scope == 'chromeos/device':
    282       policy = self._server.policy['google/chromeos/user']['mandatory']
    283       setting = response.policy_response.setting.add()
    284       setting.policy_key = 'chrome-policy'
    285       policy_value = dm.GenericSetting()
    286       for (key, value) in policy.iteritems():
    287         entry = policy_value.named_value.add()
    288         entry.name = key
    289         entry_value = dm.GenericValue()
    290         if isinstance(value, bool):
    291           entry_value.value_type = dm.GenericValue.VALUE_TYPE_BOOL
    292           entry_value.bool_value = value
    293         elif isinstance(value, int):
    294           entry_value.value_type = dm.GenericValue.VALUE_TYPE_INT64
    295           entry_value.int64_value = value
    296         elif isinstance(value, str) or isinstance(value, unicode):
    297           entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING
    298           entry_value.string_value = value
    299         elif isinstance(value, list):
    300           entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY
    301           for list_entry in value:
    302             entry_value.string_array.append(str(list_entry))
    303         entry.value.CopyFrom(entry_value)
    304       setting.policy_value.CopyFrom(policy_value)
    305 
    306     self.DumpMessage('Response', response)
    307 
    308     return (200, response.SerializeToString())
    309 
    310   def ProcessPolicy(self, msg, request_type):
    311     """Handles a policy request.
    312 
    313     Checks for authorization, encodes the policy into protobuf representation
    314     and constructs the response.
    315 
    316     Args:
    317       msg: The DevicePolicyRequest message received from the client.
    318 
    319     Returns:
    320       A tuple of HTTP status code and response data to send to the client.
    321     """
    322 
    323     if msg.request:
    324       for request in msg.request:
    325         if request.policy_type == 'google/chromeos/unregistered_user':
    326           if request_type != 'ping':
    327             return (400, 'Invalid request type')
    328           return self.ProcessInitialPolicy(request)
    329         elif (request.policy_type in
    330               ('google/chromeos/user', 'google/chromeos/device')):
    331           if request_type != 'policy':
    332             return (400, 'Invalid request type')
    333           return self.ProcessCloudPolicy(request)
    334         else:
    335           return (400, 'Invalid policy_type')
    336     else:
    337       return self.ProcessDevicePolicy(msg)
    338 
    339   def SetProtobufMessageField(self, group_message, field, field_value):
    340     '''Sets a field in a protobuf message.
    341 
    342     Args:
    343       group_message: The protobuf message.
    344       field: The field of the message to set, it shuold be a member of
    345           group_message.DESCRIPTOR.fields.
    346       field_value: The value to set.
    347     '''
    348     if field.label == field.LABEL_REPEATED:
    349       assert type(field_value) == list
    350       entries = group_message.__getattribute__(field.name)
    351       for list_item in field_value:
    352         entries.append(list_item)
    353       return
    354     elif field.type == field.TYPE_BOOL:
    355       assert type(field_value) == bool
    356     elif field.type == field.TYPE_STRING:
    357       assert type(field_value) == str or type(field_value) == unicode
    358     elif field.type == field.TYPE_INT64:
    359       assert type(field_value) == int
    360     elif (field.type == field.TYPE_MESSAGE and
    361           field.message_type.name == 'StringList'):
    362       assert type(field_value) == list
    363       entries = group_message.__getattribute__(field.name).entries
    364       for list_item in field_value:
    365         entries.append(list_item)
    366       return
    367     else:
    368       raise Exception('Unknown field type %s' % field.type)
    369     group_message.__setattr__(field.name, field_value)
    370 
    371   def GatherDevicePolicySettings(self, settings, policies):
    372     '''Copies all the policies from a dictionary into a protobuf of type
    373     CloudDeviceSettingsProto.
    374 
    375     Args:
    376       settings: The destination ChromeDeviceSettingsProto protobuf.
    377       policies: The source dictionary containing policies in JSON format.
    378     '''
    379     for group in settings.DESCRIPTOR.fields:
    380       # Create protobuf message for group.
    381       group_message = eval('dp.' + group.message_type.name + '()')
    382       # Indicates if at least one field was set in |group_message|.
    383       got_fields = False
    384       # Iterate over fields of the message and feed them from the
    385       # policy config file.
    386       for field in group_message.DESCRIPTOR.fields:
    387         field_value = None
    388         if field.name in policies:
    389           got_fields = True
    390           field_value = policies[field.name]
    391           self.SetProtobufMessageField(group_message, field, field_value)
    392       if got_fields:
    393         settings.__getattribute__(group.name).CopyFrom(group_message)
    394 
    395   def GatherUserPolicySettings(self, settings, policies):
    396     '''Copies all the policies from a dictionary into a protobuf of type
    397     CloudPolicySettings.
    398 
    399     Args:
    400       settings: The destination: a CloudPolicySettings protobuf.
    401       policies: The source: a dictionary containing policies under keys
    402           'recommended' and 'mandatory'.
    403     '''
    404     for group in settings.DESCRIPTOR.fields:
    405       # Create protobuf message for group.
    406       group_message = eval('cp.' + group.message_type.name + '()')
    407       # We assume that this policy group will be recommended, and only switch
    408       # it to mandatory if at least one of its members is mandatory.
    409       group_message.policy_options.mode = cp.PolicyOptions.RECOMMENDED
    410       # Indicates if at least one field was set in |group_message|.
    411       got_fields = False
    412       # Iterate over fields of the message and feed them from the
    413       # policy config file.
    414       for field in group_message.DESCRIPTOR.fields:
    415         field_value = None
    416         if field.name in policies['mandatory']:
    417           group_message.policy_options.mode = cp.PolicyOptions.MANDATORY
    418           field_value = policies['mandatory'][field.name]
    419         elif field.name in policies['recommended']:
    420           field_value = policies['recommended'][field.name]
    421         if field_value != None:
    422           got_fields = True
    423           self.SetProtobufMessageField(group_message, field, field_value)
    424       if got_fields:
    425         settings.__getattribute__(group.name).CopyFrom(group_message)
    426 
    427   def ProcessCloudPolicy(self, msg):
    428     """Handles a cloud policy request. (New protocol for policy requests.)
    429 
    430     Checks for authorization, encodes the policy into protobuf representation,
    431     signs it and constructs the repsonse.
    432 
    433     Args:
    434       msg: The CloudPolicyRequest message received from the client.
    435 
    436     Returns:
    437       A tuple of HTTP status code and response data to send to the client.
    438     """
    439 
    440     token_info, error = self.CheckToken()
    441     if not token_info:
    442       return error
    443 
    444     # Response is only given if the scope is specified in the config file.
    445     # Normally 'google/chromeos/device' and 'google/chromeos/user' should be
    446     # accepted.
    447     policy_value = ''
    448     if (msg.policy_type in token_info['allowed_policy_types'] and
    449         msg.policy_type in self._server.policy):
    450       if msg.policy_type == 'google/chromeos/user':
    451         settings = cp.CloudPolicySettings()
    452         self.GatherUserPolicySettings(settings,
    453                                       self._server.policy[msg.policy_type])
    454         policy_value = settings.SerializeToString()
    455       elif msg.policy_type == 'google/chromeos/device':
    456         settings = dp.ChromeDeviceSettingsProto()
    457         self.GatherDevicePolicySettings(settings,
    458                                         self._server.policy[msg.policy_type])
    459         policy_value = settings.SerializeToString()
    460 
    461     # Figure out the key we want to use. If multiple keys are configured, the
    462     # server will rotate through them in a round-robin fashion.
    463     signing_key = None
    464     req_key = None
    465     key_version = 1
    466     nkeys = len(self._server.keys)
    467     if msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and nkeys > 0:
    468       if msg.public_key_version in range(1, nkeys + 1):
    469         # requested key exists, use for signing and rotate.
    470         req_key = self._server.keys[msg.public_key_version - 1]['private_key']
    471         key_version = (msg.public_key_version % nkeys) + 1
    472       signing_key = self._server.keys[key_version - 1]
    473 
    474     # Fill the policy data protobuf.
    475     policy_data = dm.PolicyData()
    476     policy_data.policy_type = msg.policy_type
    477     policy_data.timestamp = int(time.time() * 1000)
    478     policy_data.request_token = token_info['device_token'];
    479     policy_data.policy_value = policy_value
    480     policy_data.machine_name = token_info['machine_name']
    481     if signing_key:
    482       policy_data.public_key_version = key_version
    483     policy_data.username = self._server.username
    484     policy_data.device_id = token_info['device_id']
    485     signed_data = policy_data.SerializeToString()
    486 
    487     response = dm.DeviceManagementResponse()
    488     fetch_response = response.policy_response.response.add()
    489     fetch_response.policy_data = signed_data
    490     if signing_key:
    491       fetch_response.policy_data_signature = (
    492           signing_key['private_key'].hashAndSign(signed_data).tostring())
    493       if msg.public_key_version != key_version:
    494         fetch_response.new_public_key = signing_key['public_key']
    495         if req_key:
    496           fetch_response.new_public_key_signature = (
    497               req_key.hashAndSign(fetch_response.new_public_key).tostring())
    498 
    499     self.DumpMessage('Response', response)
    500 
    501     return (200, response.SerializeToString())
    502 
    503   def CheckToken(self):
    504     """Helper for checking whether the client supplied a valid DM token.
    505 
    506     Extracts the token from the request and passed to the server in order to
    507     look up the client.
    508 
    509     Returns:
    510       A pair of token information record and error response. If the first
    511       element is None, then the second contains an error code to send back to
    512       the client. Otherwise the first element is the same structure that is
    513       returned by LookupToken().
    514     """
    515     error = None
    516     dmtoken = None
    517     request_device_id = self.GetUniqueParam('deviceid')
    518     match = re.match('GoogleDMToken token=(\\w+)',
    519                      self._headers.getheader('Authorization', ''))
    520     if match:
    521       dmtoken = match.group(1)
    522     if not dmtoken:
    523       error = dm.DeviceManagementResponse.DEVICE_MANAGEMENT_TOKEN_INVALID
    524     else:
    525       token_info = self._server.LookupToken(dmtoken)
    526       if (not token_info or
    527           not request_device_id or
    528           token_info['device_id'] != request_device_id):
    529         error = dm.DeviceManagementResponse.DEVICE_NOT_FOUND
    530       else:
    531         return (token_info, None)
    532 
    533     response = dm.DeviceManagementResponse()
    534     response.error = error
    535 
    536     self.DumpMessage('Response', response)
    537 
    538     return (None, (200, response.SerializeToString()))
    539 
    540   def DumpMessage(self, label, msg):
    541     """Helper for logging an ASCII dump of a protobuf message."""
    542     logging.debug('%s\n%s' % (label, str(msg)))
    543 
    544 class TestServer(object):
    545   """Handles requests and keeps global service state."""
    546 
    547   def __init__(self, policy_path, private_key_paths, policy_user):
    548     """Initializes the server.
    549 
    550     Args:
    551       policy_path: Names the file to read JSON-formatted policy from.
    552       private_key_paths: List of paths to read private keys from.
    553     """
    554     self._registered_tokens = {}
    555     self.policy = {}
    556 
    557     # There is no way to for the testserver to know the user name belonging to
    558     # the GAIA auth token we received (short of actually talking to GAIA). To
    559     # address this, we have a command line parameter to set the username that
    560     # the server should report to the client.
    561     self.username = policy_user
    562 
    563     if json is None:
    564       print 'No JSON module, cannot parse policy information'
    565     else :
    566       try:
    567         self.policy = json.loads(open(policy_path).read())
    568       except IOError:
    569         print 'Failed to load policy from %s' % policy_path
    570 
    571     self.keys = []
    572     if private_key_paths:
    573       # Load specified keys from the filesystem.
    574       for key_path in private_key_paths:
    575         try:
    576           key = tlslite.api.parsePEMKey(open(key_path).read(), private=True)
    577         except IOError:
    578           print 'Failed to load private key from %s' % key_path
    579           continue
    580 
    581         assert key != None
    582         self.keys.append({ 'private_key' : key })
    583     else:
    584       # Generate a key if none were specified.
    585       key = tlslite.api.generateRSAKey(1024)
    586       assert key != None
    587       self.keys.append({ 'private_key' : key })
    588 
    589     # Derive the public keys from the loaded private keys.
    590     for entry in self.keys:
    591       key = entry['private_key']
    592 
    593       algorithm = asn1der.Sequence(
    594           [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
    595             asn1der.Data(asn1der.NULL, '') ])
    596       rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
    597                                       asn1der.Integer(key.e) ])
    598       pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
    599       entry['public_key'] = pubkey;
    600 
    601   def HandleRequest(self, path, headers, request):
    602     """Handles a request.
    603 
    604     Args:
    605       path: The request path and query parameters received from the client.
    606       headers: A rfc822.Message-like object containing HTTP headers.
    607       request: The request data received from the client as a string.
    608     Returns:
    609       A pair of HTTP status code and response data to send to the client.
    610     """
    611     handler = RequestHandler(self, path, headers, request)
    612     return handler.HandleRequest()
    613 
    614   def RegisterDevice(self, device_id, machine_id, type):
    615     """Registers a device or user and generates a DM token for it.
    616 
    617     Args:
    618       device_id: The device identifier provided by the client.
    619 
    620     Returns:
    621       The newly generated device token for the device.
    622     """
    623     dmtoken_chars = []
    624     while len(dmtoken_chars) < 32:
    625       dmtoken_chars.append(random.choice('0123456789abcdef'))
    626     dmtoken = ''.join(dmtoken_chars)
    627     allowed_policy_types = {
    628       dm.DeviceRegisterRequest.USER: ['google/chromeos/user'],
    629       dm.DeviceRegisterRequest.DEVICE: ['google/chromeos/device'],
    630       dm.DeviceRegisterRequest.TT: ['google/chromeos/user'],
    631     }
    632     self._registered_tokens[dmtoken] = {
    633       'device_id': device_id,
    634       'device_token': dmtoken,
    635       'allowed_policy_types': allowed_policy_types[type],
    636       'machine_name': 'chromeos-' + machine_id,
    637     }
    638     return self._registered_tokens[dmtoken]
    639 
    640   def LookupToken(self, dmtoken):
    641     """Looks up a device or a user by DM token.
    642 
    643     Args:
    644       dmtoken: The device management token provided by the client.
    645 
    646     Returns:
    647       A dictionary with information about a device or user that is registered by
    648       dmtoken, or None if the token is not found.
    649     """
    650     return self._registered_tokens.get(dmtoken, None)
    651 
    652   def UnregisterDevice(self, dmtoken):
    653     """Unregisters a device identified by the given DM token.
    654 
    655     Args:
    656       dmtoken: The device management token provided by the client.
    657     """
    658     if dmtoken in self._registered_tokens.keys():
    659       del self._registered_tokens[dmtoken]
    660