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
8This implements a simple cloud policy test server that can be used to test
9chrome's device management service client. The policy information is read from
10the file named device_management in the server's data directory. It contains
11enforced and recommended policies for the device and user scope, and a list
12of managed users.
13
14The format of the file is JSON. The root dictionary contains a list under the
15key "managed_users". It contains auth tokens for which the server will claim
16that the user is managed. The token string "*" indicates that all users are
17claimed to be managed. Other keys in the root dictionary identify request
18scopes. Each request scope is described by a dictionary that holds two
19sub-dictionaries: "mandatory" and "recommended". Both these hold the policy
20definitions as key/value stores, their format is identical to what the Linux
21implementation reads from /etc.
22
23Example:
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
42import cgi
43import logging
44import os
45import random
46import re
47import sys
48import time
49import tlslite
50import tlslite.api
51import tlslite.utils
52
53# The name and availability of the json module varies in python versions.
54try:
55  import simplejson as json
56except ImportError:
57  try:
58    import json
59  except ImportError:
60    json = None
61
62import asn1der
63import device_management_backend_pb2 as dm
64import cloud_policy_pb2 as cp
65import chrome_device_policy_pb2 as dp
66
67# ASN.1 object identifier for PKCS#1/RSA.
68PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
69
70class 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
544class 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