summaryrefslogtreecommitdiffstats
path: root/net/tools/testserver
diff options
context:
space:
mode:
authorgfeher@chromium.org <gfeher@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-01-28 18:15:24 +0000
committergfeher@chromium.org <gfeher@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-01-28 18:15:24 +0000
commita032ea356ab1e79d1041a976de1fd1ab5b3e47bb (patch)
tree2d9f080bf49e3d6eaf8259dfdedd286983aaac08 /net/tools/testserver
parent3618712f7446888f98f1de967b4d5d955b54715d (diff)
downloadchromium_src-a032ea356ab1e79d1041a976de1fd1ab5b3e47bb.zip
chromium_src-a032ea356ab1e79d1041a976de1fd1ab5b3e47bb.tar.gz
chromium_src-a032ea356ab1e79d1041a976de1fd1ab5b3e47bb.tar.bz2
New protocol and testserver for the Chrome-DMServer protocol
New features: -Message to ping DMServer and ask if a user is managed -Signed policy responses -Server assigns names to ChromeOS devices A temporary version of cloud_policies.proto is also checked in (it will be auto-generated later). BUG=chromium-os:11253,chromium-os:11254 TEST=none Review URL: http://codereview.chromium.org/6161007 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@72975 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net/tools/testserver')
-rw-r--r--net/tools/testserver/device_management.py224
-rwxr-xr-xnet/tools/testserver/testserver.py13
2 files changed, 224 insertions, 13 deletions
diff --git a/net/tools/testserver/device_management.py b/net/tools/testserver/device_management.py
index d715227..183451b 100644
--- a/net/tools/testserver/device_management.py
+++ b/net/tools/testserver/device_management.py
@@ -1,5 +1,5 @@
#!/usr/bin/python2.5
-# Copyright (c) 2010 The Chromium Authors. All rights reserved.
+# 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.
@@ -7,21 +7,47 @@
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
-from files in a directory. The files should contain policy definitions in JSON
-format, using the top-level dictionary as a key/value store. The format is
-identical to what the Linux implementation reads from /etc. Here is an example:
+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:
{
- "HomepageLocation" : "http://www.chromium.org"
+ "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
# The name and availability of the json module varies in python versions.
try:
@@ -33,6 +59,8 @@ except ImportError:
json = None
import device_management_backend_pb2 as dm
+import cloud_policy_pb2 as cp
+
class RequestHandler(object):
"""Decodes and handles device management requests from clients.
@@ -95,9 +123,29 @@ class RequestHandler(object):
return self.ProcessUnregister(rmsg.unregister_request)
elif request_type == 'policy':
return self.ProcessPolicy(rmsg.policy_request)
+ elif request_type == 'cloud_policy':
+ return self.ProcessCloudPolicyRequest(rmsg.cloud_policy_request)
+ elif request_type == 'managed_check':
+ return self.ProcessManagedCheck(rmsg.managed_check_request)
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 GetDeviceName(self):
+ """Returns the name for the currently authenticated device based on its
+ device id.
+ """
+ return 'chromeos-' + self.GetUniqueParam('deviceid')
+
def ProcessRegister(self, msg):
"""Handles a register request.
@@ -111,11 +159,8 @@ class RequestHandler(object):
A tuple of HTTP status code and response data to send to the client.
"""
# Check the auth token and device ID.
- match = re.match('GoogleLogin auth=(\\w+)',
- self._headers.getheader('Authorization', ''))
- if not match:
+ if not self.CheckGoogleLogin():
return (403, 'No authorization')
- auth_token = match.group(1)
device_id = self.GetUniqueParam('deviceid')
if not device_id:
@@ -128,6 +173,7 @@ class RequestHandler(object):
response = dm.DeviceManagementResponse()
response.error = dm.DeviceManagementResponse.SUCCESS
response.register_response.device_management_token = dmtoken
+ response.register_response.device_name = self.GetDeviceName()
self.DumpMessage('Response', response)
@@ -162,11 +208,44 @@ class RequestHandler(object):
return (200, response.SerializeToString())
+ def ProcessManagedCheck(self, msg):
+ """Handles a 'managed check' request.
+
+ Queries the list of managed users and responds the client if their user
+ is managed or not.
+
+ Args:
+ msg: The ManagedCheckRequest message received from the client.
+
+ Returns:
+ A tuple of HTTP status code and response data to send to the client.
+ """
+ # Check the management token.
+ auth = self.CheckGoogleLogin()
+ if not auth:
+ return (403, 'No authorization')
+
+ managed_check_response = dm.ManagedCheckResponse()
+ if ('*' in self._server.policy['managed_users'] or
+ auth in self._server.policy['managed_users']):
+ managed_check_response.mode = dm.ManagedCheckResponse.MANAGED;
+ else:
+ managed_check_response.mode = dm.ManagedCheckResponse.UNMANAGED;
+
+ # Prepare and send the response.
+ response = dm.DeviceManagementResponse()
+ response.error = dm.DeviceManagementResponse.SUCCESS
+ response.managed_check_response.CopyFrom(managed_check_response)
+
+ self.DumpMessage('Response', response)
+
+ return (200, response.SerializeToString())
+
def ProcessPolicy(self, msg):
"""Handles a policy request.
Checks for authorization, encodes the policy into protobuf representation
- and constructs the repsonse.
+ and constructs the response.
Args:
msg: The DevicePolicyRequest message received from the client.
@@ -214,6 +293,113 @@ class RequestHandler(object):
return (200, response.SerializeToString())
+ 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
+ assert field.type == field.TYPE_STRING
+ list_field = group_message.__getattribute__(field.name)
+ for list_item in field_value:
+ list_field.append(list_item)
+ else:
+ # Simple cases:
+ if field.type == field.TYPE_BOOL:
+ assert type(field_value) == bool
+ elif field.type == field.TYPE_STRING:
+ assert type(field_value) == str
+ elif field.type == field.TYPE_INT64:
+ assert type(field_value) == int
+ else:
+ raise Exception('Unknown field type %s' % field.type_name)
+ group_message.__setattr__(field.name, field_value)
+
+ def GatherPolicySettings(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 ProcessCloudPolicyRequest(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, response = self.CheckToken()
+ if not token:
+ return response
+
+ settings = cp.CloudPolicySettings()
+
+ if msg.policy_scope in self._server.policy:
+ # Respond is only given if the scope is specified in the config file.
+ # Normally 'chromeos/device' and 'chromeos/user' should be accepted.
+ self.GatherPolicySettings(settings,
+ self._server.policy[msg.policy_scope])
+
+ # Construct response
+ signed_response = dm.SignedCloudPolicyResponse()
+ signed_response.settings.CopyFrom(settings)
+ signed_response.timestamp = int(time.time())
+ signed_response.request_token = token;
+ signed_response.device_name = self.GetDeviceName()
+
+ cloud_response = dm.CloudPolicyResponse()
+ cloud_response.signed_response = signed_response.SerializeToString()
+ signed_data = cloud_response.signed_response
+ cloud_response.signature = (
+ self._server.private_key.hashAndSign(signed_data).tostring())
+ for certificate in self._server.cert_chain:
+ cloud_response.certificate_chain.append(
+ certificate.writeBytes().tostring())
+
+ response = dm.DeviceManagementResponse()
+ response.error = dm.DeviceManagementResponse.SUCCESS
+ response.cloud_policy_response.CopyFrom(cloud_response)
+
+ self.DumpMessage('Response', response)
+
+ return (200, response.SerializeToString())
+
def CheckToken(self):
"""Helper for checking whether the client supplied a valid DM token.
@@ -254,11 +440,13 @@ class RequestHandler(object):
class TestServer(object):
"""Handles requests and keeps global service state."""
- def __init__(self, policy_path):
+ def __init__(self, policy_path, policy_cert_chain):
"""Initializes the server.
Args:
policy_path: Names the file to read JSON-formatted policy from.
+ policy_cert_chain: List of paths to X.509 certificate files of the
+ certificate chain used for signing responses.
"""
self._registered_devices = {}
self.policy = {}
@@ -270,6 +458,20 @@ class TestServer(object):
except IOError:
print 'Failed to load policy from %s' % policy_path
+ self.private_key = None
+ self.cert_chain = []
+ for cert_path in policy_cert_chain:
+ try:
+ cert_text = open(cert_path).read()
+ except IOError:
+ print 'Failed to load certificate from %s' % cert_path
+ certificate = tlslite.api.X509()
+ certificate.parse(cert_text)
+ self.cert_chain.append(certificate)
+ if self.private_key is None:
+ self.private_key = tlslite.api.parsePEMKey(cert_text, private=True)
+ assert self.private_key != None
+
def HandleRequest(self, path, headers, request):
"""Handles a request.
diff --git a/net/tools/testserver/testserver.py b/net/tools/testserver/testserver.py
index 9bad862..eb2a02e 100755
--- a/net/tools/testserver/testserver.py
+++ b/net/tools/testserver/testserver.py
@@ -1,5 +1,5 @@
#!/usr/bin/python2.4
-# Copyright (c) 2006-2010 The Chromium Authors. All rights reserved.
+# 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.
@@ -1284,7 +1284,8 @@ class TestPageHandler(BasePageHandler):
import device_management
policy_path = os.path.join(self.server.data_dir, 'device_management')
self.server._device_management_handler = (
- device_management.TestServer(policy_path))
+ device_management.TestServer(policy_path,
+ self.server.policy_cert_chain))
http_response, raw_reply = (
self.server._device_management_handler.HandleRequest(self.path,
@@ -1419,6 +1420,7 @@ def main(options, args):
server.file_root_url = options.file_root_url
server_data['port'] = server.server_port
server._device_management_handler = None
+ server.policy_cert_chain = options.policy_cert_chain
elif options.server_type == SERVER_SYNC:
server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
print 'Sync HTTP server started on port %d...' % server.server_port
@@ -1517,6 +1519,13 @@ if __name__ == '__main__':
option_parser.add_option('', '--startup-pipe', type='int',
dest='startup_pipe',
help='File handle of pipe to parent process')
+ option_parser.add_option('', '--policy-cert-chain', action='append',
+ help='Specify a path to a certificate file to sign '
+ 'policy responses. This option may be used '
+ 'multiple times to define a certificate chain. '
+ 'The first element will be used for signing, '
+ 'the last element should be the root '
+ 'certificate.')
options, args = option_parser.parse_args()
sys.exit(main(options, args))