#!/usr/bin/python2.5 # Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """A bare-bones test server for testing cloud policy support. This implements a simple cloud policy test server that can be used to test chrome's device management service client. The policy information is read from the file named device_management in the server's data directory. It contains enforced and recommended policies for the device and user scope, and a list of managed users. The format of the file is JSON. The root dictionary contains a list under the key "managed_users". It contains auth tokens for which the server will claim that the user is managed. The token string "*" indicates that all users are claimed to be managed. Other keys in the root dictionary identify request scopes. Each request scope is described by a dictionary that holds two sub-dictionaries: "mandatory" and "recommended". Both these hold the policy definitions as key/value stores, their format is identical to what the Linux implementation reads from /etc. Example: { "chromeos/device": { "mandatory": { "HomepageLocation" : "http://www.chromium.org" }, "recommended": { "JavascriptEnabled": false, }, }, "managed_users": [ "secret123456" ] } """ import cgi import logging import os import random import re import sys import time import tlslite import tlslite.api import tlslite.utils # The name and availability of the json module varies in python versions. try: import simplejson as json except ImportError: try: import json except ImportError: json = None import asn1der import device_management_backend_pb2 as dm import cloud_policy_pb2 as cp import chrome_device_policy_pb2 as dp # ASN.1 object identifier for PKCS#1/RSA. PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01' class RequestHandler(object): """Decodes and handles device management requests from clients. The handler implements all the request parsing and protobuf message decoding and encoding. It calls back into the server to lookup, register, and unregister clients. """ def __init__(self, server, path, headers, request): """Initialize the handler. Args: server: The TestServer object to use for (un)registering clients. path: A string containing the request path and query parameters. headers: A rfc822.Message-like object containing HTTP headers. request: The request data received from the client as a string. """ self._server = server self._path = path self._headers = headers self._request = request self._params = None def GetUniqueParam(self, name): """Extracts a unique query parameter from the request. Args: name: Names the parameter to fetch. Returns: The parameter value or None if the parameter doesn't exist or is not unique. """ if not self._params: self._params = cgi.parse_qs(self._path[self._path.find('?') + 1:]) param_list = self._params.get(name, []) if len(param_list) == 1: return param_list[0] return None; def HandleRequest(self): """Handles a request. Parses the data supplied at construction time and returns a pair indicating http status code and response data to be sent back to the client. Returns: A tuple of HTTP status code and response data to send to the client. """ rmsg = dm.DeviceManagementRequest() rmsg.ParseFromString(self._request) logging.debug('auth -> ' + self._headers.getheader('Authorization', '')) logging.debug('deviceid -> ' + self.GetUniqueParam('deviceid')) self.DumpMessage('Request', rmsg) request_type = self.GetUniqueParam('request') # Check server side requirements, as defined in # device_management_backend.proto. if (self.GetUniqueParam('devicetype') != '2' or self.GetUniqueParam('apptype') != 'Chrome' or (request_type != 'ping' and len(self.GetUniqueParam('deviceid')) >= 64) or len(self.GetUniqueParam('agent')) >= 64): return (400, 'Invalid request parameter') if request_type == 'register': return self.ProcessRegister(rmsg.register_request) elif request_type == 'unregister': return self.ProcessUnregister(rmsg.unregister_request) elif request_type == 'policy' or request_type == 'ping': return self.ProcessPolicy(rmsg.policy_request, request_type) else: return (400, 'Invalid request parameter') def CheckGoogleLogin(self): """Extracts the GoogleLogin auth token from the HTTP request, and returns it. Returns None if the token is not present. """ match = re.match('GoogleLogin auth=(\\w+)', self._headers.getheader('Authorization', '')) if not match: return None return match.group(1) def ProcessRegister(self, msg): """Handles a register request. Checks the query for authorization and device identifier, registers the device with the server and constructs a response. Args: msg: The DeviceRegisterRequest message received from the client. Returns: A tuple of HTTP status code and response data to send to the client. """ # Check the auth token and device ID. if not self.CheckGoogleLogin(): return (403, 'No authorization') device_id = self.GetUniqueParam('deviceid') if not device_id: return (400, 'Missing device identifier') token_info = self._server.RegisterDevice(device_id, msg.machine_id, msg.type) # Send back the reply. response = dm.DeviceManagementResponse() response.register_response.device_management_token = ( token_info['device_token']) response.register_response.machine_name = token_info['machine_name'] self.DumpMessage('Response', response) return (200, response.SerializeToString()) def ProcessUnregister(self, msg): """Handles a register request. Checks for authorization, unregisters the device and constructs the response. Args: msg: The DeviceUnregisterRequest message received from the client. Returns: A tuple of HTTP status code and response data to send to the client. """ # Check the management token. token, response = self.CheckToken(); if not token: return response # Unregister the device. self._server.UnregisterDevice(token); # Prepare and send the response. response = dm.DeviceManagementResponse() response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) self.DumpMessage('Response', response) return (200, response.SerializeToString()) def ProcessInitialPolicy(self, msg): """Handles a 'preregister policy' request. Queries the list of managed users and responds the client if their user is managed or not. Args: msg: The PolicyFetchRequest message received from the client. Returns: A tuple of HTTP status code and response data to send to the client. """ # Check the GAIA token. auth = self.CheckGoogleLogin() if not auth: return (403, 'No authorization') chrome_initial_settings = dm.ChromeInitialSettingsProto() if ('*' in self._server.policy['managed_users'] or auth in self._server.policy['managed_users']): chrome_initial_settings.enrollment_provision = ( dm.ChromeInitialSettingsProto.MANAGED); else: chrome_initial_settings.enrollment_provision = ( dm.ChromeInitialSettingsProto.UNMANAGED); policy_data = dm.PolicyData() policy_data.policy_type = msg.policy_type policy_data.policy_value = chrome_initial_settings.SerializeToString() # Prepare and send the response. response = dm.DeviceManagementResponse() fetch_response = response.policy_response.response.add() fetch_response.policy_data = ( policy_data.SerializeToString()) self.DumpMessage('Response', response) return (200, response.SerializeToString()) def ProcessDevicePolicy(self, msg): """Handles a policy request that uses the deprecated protcol. TODO(gfeher): Remove this when we certainly don't need it. Checks for authorization, encodes the policy into protobuf representation and constructs the response. Args: msg: The DevicePolicyRequest message received from the client. Returns: A tuple of HTTP status code and response data to send to the client. """ # Check the management token. token, response = self.CheckToken() if not token: return response # Stuff the policy dictionary into a response message and send it back. response = dm.DeviceManagementResponse() response.policy_response.CopyFrom(dm.DevicePolicyResponse()) # Respond only if the client requested policy for the cros/device scope, # since that's where chrome policy is supposed to live in. if msg.policy_scope == 'chromeos/device': policy = self._server.policy['google/chromeos/user']['mandatory'] setting = response.policy_response.setting.add() setting.policy_key = 'chrome-policy' policy_value = dm.GenericSetting() for (key, value) in policy.iteritems(): entry = policy_value.named_value.add() entry.name = key entry_value = dm.GenericValue() if isinstance(value, bool): entry_value.value_type = dm.GenericValue.VALUE_TYPE_BOOL entry_value.bool_value = value elif isinstance(value, int): entry_value.value_type = dm.GenericValue.VALUE_TYPE_INT64 entry_value.int64_value = value elif isinstance(value, str) or isinstance(value, unicode): entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING entry_value.string_value = value elif isinstance(value, list): entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY for list_entry in value: entry_value.string_array.append(str(list_entry)) entry.value.CopyFrom(entry_value) setting.policy_value.CopyFrom(policy_value) self.DumpMessage('Response', response) return (200, response.SerializeToString()) def ProcessPolicy(self, msg, request_type): """Handles a policy request. Checks for authorization, encodes the policy into protobuf representation and constructs the response. Args: msg: The DevicePolicyRequest message received from the client. Returns: A tuple of HTTP status code and response data to send to the client. """ if msg.request: for request in msg.request: if request.policy_type == 'google/chromeos/unregistered_user': if request_type != 'ping': return (400, 'Invalid request type') return self.ProcessInitialPolicy(request) elif (request.policy_type in ('google/chromeos/user', 'google/chromeos/device')): if request_type != 'policy': return (400, 'Invalid request type') return self.ProcessCloudPolicy(request) else: return (400, 'Invalid policy_type') else: return self.ProcessDevicePolicy(msg) def SetProtobufMessageField(self, group_message, field, field_value): '''Sets a field in a protobuf message. Args: group_message: The protobuf message. field: The field of the message to set, it shuold be a member of group_message.DESCRIPTOR.fields. field_value: The value to set. ''' if field.label == field.LABEL_REPEATED: assert type(field_value) == list entries = group_message.__getattribute__(field.name) for list_item in field_value: entries.append(list_item) return elif field.type == field.TYPE_BOOL: assert type(field_value) == bool elif field.type == field.TYPE_STRING: assert type(field_value) == str or type(field_value) == unicode elif field.type == field.TYPE_INT64: assert type(field_value) == int elif (field.type == field.TYPE_MESSAGE and field.message_type.name == 'StringList'): assert type(field_value) == list entries = group_message.__getattribute__(field.name).entries for list_item in field_value: entries.append(list_item) return else: raise Exception('Unknown field type %s' % field.type) group_message.__setattr__(field.name, field_value) def GatherDevicePolicySettings(self, settings, policies): '''Copies all the policies from a dictionary into a protobuf of type CloudDeviceSettingsProto. Args: settings: The destination ChromeDeviceSettingsProto protobuf. policies: The source dictionary containing policies in JSON format. ''' for group in settings.DESCRIPTOR.fields: # Create protobuf message for group. group_message = eval('dp.' + group.message_type.name + '()') # Indicates if at least one field was set in |group_message|. got_fields = False # Iterate over fields of the message and feed them from the # policy config file. for field in group_message.DESCRIPTOR.fields: field_value = None if field.name in policies: got_fields = True field_value = policies[field.name] self.SetProtobufMessageField(group_message, field, field_value) if got_fields: settings.__getattribute__(group.name).CopyFrom(group_message) def GatherUserPolicySettings(self, settings, policies): '''Copies all the policies from a dictionary into a protobuf of type CloudPolicySettings. Args: settings: The destination: a CloudPolicySettings protobuf. policies: The source: a dictionary containing policies under keys 'recommended' and 'mandatory'. ''' for group in settings.DESCRIPTOR.fields: # Create protobuf message for group. group_message = eval('cp.' + group.message_type.name + '()') # We assume that this policy group will be recommended, and only switch # it to mandatory if at least one of its members is mandatory. group_message.policy_options.mode = cp.PolicyOptions.RECOMMENDED # Indicates if at least one field was set in |group_message|. got_fields = False # Iterate over fields of the message and feed them from the # policy config file. for field in group_message.DESCRIPTOR.fields: field_value = None if field.name in policies['mandatory']: group_message.policy_options.mode = cp.PolicyOptions.MANDATORY field_value = policies['mandatory'][field.name] elif field.name in policies['recommended']: field_value = policies['recommended'][field.name] if field_value != None: got_fields = True self.SetProtobufMessageField(group_message, field, field_value) if got_fields: settings.__getattribute__(group.name).CopyFrom(group_message) def ProcessCloudPolicy(self, msg): """Handles a cloud policy request. (New protocol for policy requests.) Checks for authorization, encodes the policy into protobuf representation, signs it and constructs the repsonse. Args: msg: The CloudPolicyRequest message received from the client. Returns: A tuple of HTTP status code and response data to send to the client. """ token_info, error = self.CheckToken() if not token_info: return error # Response is only given if the scope is specified in the config file. # Normally 'google/chromeos/device' and 'google/chromeos/user' should be # accepted. policy_value = '' if (msg.policy_type in token_info['allowed_policy_types'] and msg.policy_type in self._server.policy): if msg.policy_type == 'google/chromeos/user': settings = cp.CloudPolicySettings() self.GatherUserPolicySettings(settings, self._server.policy[msg.policy_type]) policy_value = settings.SerializeToString() elif msg.policy_type == 'google/chromeos/device': settings = dp.ChromeDeviceSettingsProto() self.GatherDevicePolicySettings(settings, self._server.policy[msg.policy_type]) policy_value = settings.SerializeToString() # Figure out the key we want to use. If multiple keys are configured, the # server will rotate through them in a round-robin fashion. signing_key = None req_key = None key_version = 1 nkeys = len(self._server.keys) if msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and nkeys > 0: if msg.public_key_version in range(1, nkeys + 1): # requested key exists, use for signing and rotate. req_key = self._server.keys[msg.public_key_version - 1]['private_key'] key_version = (msg.public_key_version % nkeys) + 1 signing_key = self._server.keys[key_version - 1] # Fill the policy data protobuf. policy_data = dm.PolicyData() policy_data.policy_type = msg.policy_type policy_data.timestamp = int(time.time() * 1000) policy_data.request_token = token_info['device_token']; policy_data.policy_value = policy_value policy_data.machine_name = token_info['machine_name'] if signing_key: policy_data.public_key_version = key_version policy_data.username = self._server.username policy_data.device_id = token_info['device_id'] signed_data = policy_data.SerializeToString() response = dm.DeviceManagementResponse() fetch_response = response.policy_response.response.add() fetch_response.policy_data = signed_data if signing_key: fetch_response.policy_data_signature = ( signing_key['private_key'].hashAndSign(signed_data).tostring()) if msg.public_key_version != key_version: fetch_response.new_public_key = signing_key['public_key'] if req_key: fetch_response.new_public_key_signature = ( req_key.hashAndSign(fetch_response.new_public_key).tostring()) self.DumpMessage('Response', response) return (200, response.SerializeToString()) def CheckToken(self): """Helper for checking whether the client supplied a valid DM token. Extracts the token from the request and passed to the server in order to look up the client. Returns: A pair of token information record and error response. If the first element is None, then the second contains an error code to send back to the client. Otherwise the first element is the same structure that is returned by LookupToken(). """ error = None dmtoken = None request_device_id = self.GetUniqueParam('deviceid') match = re.match('GoogleDMToken token=(\\w+)', self._headers.getheader('Authorization', '')) if match: dmtoken = match.group(1) if not dmtoken: error = dm.DeviceManagementResponse.DEVICE_MANAGEMENT_TOKEN_INVALID else: token_info = self._server.LookupToken(dmtoken) if (not token_info or not request_device_id or token_info['device_id'] != request_device_id): error = dm.DeviceManagementResponse.DEVICE_NOT_FOUND else: return (token_info, None) response = dm.DeviceManagementResponse() response.error = error self.DumpMessage('Response', response) return (None, (200, response.SerializeToString())) def DumpMessage(self, label, msg): """Helper for logging an ASCII dump of a protobuf message.""" logging.debug('%s\n%s' % (label, str(msg))) class TestServer(object): """Handles requests and keeps global service state.""" def __init__(self, policy_path, private_key_paths, policy_user): """Initializes the server. Args: policy_path: Names the file to read JSON-formatted policy from. private_key_paths: List of paths to read private keys from. """ self._registered_tokens = {} self.policy = {} # There is no way to for the testserver to know the user name belonging to # the GAIA auth token we received (short of actually talking to GAIA). To # address this, we have a command line parameter to set the username that # the server should report to the client. self.username = policy_user if json is None: print 'No JSON module, cannot parse policy information' else : try: self.policy = json.loads(open(policy_path).read()) except IOError: print 'Failed to load policy from %s' % policy_path self.keys = [] if private_key_paths: # Load specified keys from the filesystem. for key_path in private_key_paths: try: key = tlslite.api.parsePEMKey(open(key_path).read(), private=True) except IOError: print 'Failed to load private key from %s' % key_path continue assert key != None self.keys.append({ 'private_key' : key }) else: # Generate a key if none were specified. key = tlslite.api.generateRSAKey(1024) assert key != None self.keys.append({ 'private_key' : key }) # Derive the public keys from the loaded private keys. for entry in self.keys: key = entry['private_key'] algorithm = asn1der.Sequence( [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID), asn1der.Data(asn1der.NULL, '') ]) rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n), asn1der.Integer(key.e) ]) pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ]) entry['public_key'] = pubkey; def HandleRequest(self, path, headers, request): """Handles a request. Args: path: The request path and query parameters received from the client. headers: A rfc822.Message-like object containing HTTP headers. request: The request data received from the client as a string. Returns: A pair of HTTP status code and response data to send to the client. """ handler = RequestHandler(self, path, headers, request) return handler.HandleRequest() def RegisterDevice(self, device_id, machine_id, type): """Registers a device or user and generates a DM token for it. Args: device_id: The device identifier provided by the client. Returns: The newly generated device token for the device. """ dmtoken_chars = [] while len(dmtoken_chars) < 32: dmtoken_chars.append(random.choice('0123456789abcdef')) dmtoken = ''.join(dmtoken_chars) allowed_policy_types = { dm.DeviceRegisterRequest.USER: ['google/chromeos/user'], dm.DeviceRegisterRequest.DEVICE: ['google/chromeos/device'], dm.DeviceRegisterRequest.TT: ['google/chromeos/user'], } self._registered_tokens[dmtoken] = { 'device_id': device_id, 'device_token': dmtoken, 'allowed_policy_types': allowed_policy_types[type], 'machine_name': 'chromeos-' + machine_id, } return self._registered_tokens[dmtoken] def LookupToken(self, dmtoken): """Looks up a device or a user by DM token. Args: dmtoken: The device management token provided by the client. Returns: A dictionary with information about a device or user that is registered by dmtoken, or None if the token is not found. """ return self._registered_tokens.get(dmtoken, None) def UnregisterDevice(self, dmtoken): """Unregisters a device identified by the given DM token. Args: dmtoken: The device management token provided by the client. """ if dmtoken in self._registered_tokens.keys(): del self._registered_tokens[dmtoken]