diff options
author | mnissler@chromium.org <mnissler@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-11-11 08:47:14 +0000 |
---|---|---|
committer | mnissler@chromium.org <mnissler@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-11-11 08:47:14 +0000 |
commit | 5ceb5e9ce90a6287436857bed52dd98bc60330a0 (patch) | |
tree | 0fd44b2d525b2d4cdaa48eda4e799b1f8bb6277d | |
parent | 3a8bdeaf675f4496a79b0adc034671cce3ffff00 (diff) | |
download | chromium_src-5ceb5e9ce90a6287436857bed52dd98bc60330a0.zip chromium_src-5ceb5e9ce90a6287436857bed52dd98bc60330a0.tar.gz chromium_src-5ceb5e9ce90a6287436857bed52dd98bc60330a0.tar.bz2 |
Add device management test server to net infrastructure.
This adds a simple device management server implementation written in python that's useful for unit tests and local testing.
BUG=62318
TEST=Compiles and browser tests in device_management_backend_browsertest succeed
Review URL: http://codereview.chromium.org/4659002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@65783 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/browser/policy/device_management_backend_impl_browsertest.cc | 55 | ||||
-rw-r--r-- | chrome/browser/policy/proto/device_management_proto.gyp | 79 | ||||
-rw-r--r-- | chrome/chrome_browser.gypi | 50 | ||||
-rw-r--r-- | chrome/test/data/policy/device_management | 3 | ||||
-rw-r--r-- | net/net.gyp | 1 | ||||
-rw-r--r-- | net/test/test_server.cc | 2 | ||||
-rw-r--r-- | net/tools/testserver/device_management.py | 318 | ||||
-rwxr-xr-x | net/tools/testserver/testserver.py | 27 |
8 files changed, 480 insertions, 55 deletions
diff --git a/chrome/browser/policy/device_management_backend_impl_browsertest.cc b/chrome/browser/policy/device_management_backend_impl_browsertest.cc index 43df7be..770317b 100644 --- a/chrome/browser/policy/device_management_backend_impl_browsertest.cc +++ b/chrome/browser/policy/device_management_backend_impl_browsertest.cc @@ -8,6 +8,7 @@ #include "chrome/browser/browser_thread.h" #include "chrome/browser/policy/device_management_backend_mock.h" #include "chrome/test/in_process_browser_test.h" +#include "net/test/test_server.h" #include "net/url_request/url_request.h" #include "net/url_request/url_request_test_job.h" #include "testing/gtest/include/gtest/gtest.h" @@ -81,10 +82,6 @@ class DeviceManagementBackendImplIntegrationTest : public InProcessBrowserTest { } protected: - DeviceManagementBackendImplIntegrationTest() { - URLFetcher::enable_interception_for_tests(true); - } - std::string token_; }; @@ -93,7 +90,8 @@ static void QuitMessageLoop() { } IN_PROC_BROWSER_TEST_F(DeviceManagementBackendImplIntegrationTest, - RegisterAndFetchPolicy) { + CannedResponses) { + URLFetcher::enable_interception_for_tests(true); DeviceManagementBackendImpl service(kServiceUrl); { @@ -138,4 +136,51 @@ IN_PROC_BROWSER_TEST_F(DeviceManagementBackendImplIntegrationTest, } } +IN_PROC_BROWSER_TEST_F(DeviceManagementBackendImplIntegrationTest, + WithTestServer) { + net::TestServer test_server( + net::TestServer::TYPE_HTTP, + FilePath(FILE_PATH_LITERAL("chrome/test/data/policy"))); + ASSERT_TRUE(test_server.Start()); + DeviceManagementBackendImpl service( + test_server.GetURL("device_management").spec()); + + { + DeviceRegisterResponseDelegateMock delegate; + EXPECT_CALL(delegate, HandleRegisterResponse(_)) + .WillOnce(DoAll(Invoke(this, &DeviceManagementBackendImplIntegrationTest + ::CaptureToken), + InvokeWithoutArgs(QuitMessageLoop))); + em::DeviceRegisterRequest request; + service.ProcessRegisterRequest("token", "device id", request, &delegate); + MessageLoop::current()->Run(); + } + + { + em::DevicePolicyResponse expected_response; + + DevicePolicyResponseDelegateMock delegate; + EXPECT_CALL(delegate, HandlePolicyResponse(_)) + .WillOnce(InvokeWithoutArgs(QuitMessageLoop)); + em::DevicePolicyRequest request; + request.set_policy_scope("chrome"); + em::DevicePolicySettingRequest* setting_request = + request.add_setting_request(); + setting_request->set_key("policy"); + service.ProcessPolicyRequest(token_, request, &delegate); + + MessageLoop::current()->Run(); + } + + { + DeviceUnregisterResponseDelegateMock delegate; + EXPECT_CALL(delegate, HandleUnregisterResponse(_)) + .WillOnce(InvokeWithoutArgs(QuitMessageLoop)); + em::DeviceUnregisterRequest request; + service.ProcessUnregisterRequest(token_, request, &delegate); + + MessageLoop::current()->Run(); + } +} + } // namespace policy diff --git a/chrome/browser/policy/proto/device_management_proto.gyp b/chrome/browser/policy/proto/device_management_proto.gyp new file mode 100644 index 0000000..c8b61e0 --- /dev/null +++ b/chrome/browser/policy/proto/device_management_proto.gyp @@ -0,0 +1,79 @@ +# Copyright (c) 2010 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. + +{ + 'variables': { + 'chromium_code': 1, + 'protoc_out_dir': '<(SHARED_INTERMEDIATE_DIR)/protoc_out', + }, + 'targets': [ + { + # Protobuf compiler / generate rule for the device management protocol. + 'target_name': 'device_management_proto', + 'type': 'none', + 'sources': [ + 'device_management_backend.proto', + ], + 'rules': [ + { + 'rule_name': 'genproto', + 'extension': 'proto', + 'inputs': [ + '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)protoc<(EXECUTABLE_SUFFIX)', + ], + 'variables': { + # The protoc compiler requires a proto_path argument with the + # directory containing the .proto file. There's no generator + # variable that corresponds to this, so fake it. + 'rule_input_relpath': 'chrome/browser/policy/proto', + }, + 'outputs': [ + '<(PRODUCT_DIR)/pyproto/device_management_pb/<(RULE_INPUT_ROOT)_pb2.py', + '<(protoc_out_dir)/<(rule_input_relpath)/<(RULE_INPUT_ROOT).pb.h', + '<(protoc_out_dir)/<(rule_input_relpath)/<(RULE_INPUT_ROOT).pb.cc', + ], + 'action': [ + '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)protoc<(EXECUTABLE_SUFFIX)', + '--proto_path=.', + './<(RULE_INPUT_ROOT)<(RULE_INPUT_EXT)', + '--cpp_out=<(protoc_out_dir)/<(rule_input_relpath)', + '--python_out=<(PRODUCT_DIR)/pyproto/device_management_pb', + ], + 'message': 'Generating C++ and Python code from <(RULE_INPUT_PATH)', + }, + ], + 'dependencies': [ + '../../../../third_party/protobuf/protobuf.gyp:protoc#host', + ], + 'direct_dependent_settings': { + 'include_dirs': [ + '<(protoc_out_dir)', + ] + }, + }, + { + 'target_name': 'device_management_proto_cpp', + 'type': 'none', + 'export_dependent_settings': [ + '../../../../third_party/protobuf/protobuf.gyp:protobuf_lite', + 'device_management_proto', + ], + 'dependencies': [ + '../../../../third_party/protobuf/protobuf.gyp:protobuf_lite', + 'device_management_proto', + ], + 'direct_dependent_settings': { + 'include_dirs': [ + '<(protoc_out_dir)', + ] + }, + }, + ], +} + +# Local Variables: +# tab-width:2 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=2 shiftwidth=2: diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 28e0b82..e0610c9 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -15,11 +15,11 @@ 'chrome_resources', 'chrome_strings', 'debugger', - 'device_management_backend_proto', 'installer_util', 'platform_locale_settings', 'profile_import', 'browser/sync/protocol/sync_proto.gyp:sync_proto_cpp', + 'browser/policy/proto/device_management_proto.gyp:device_management_proto_cpp', 'syncapi', 'theme_resources', 'userfeedback_proto', @@ -4237,54 +4237,6 @@ '../third_party/protobuf/protobuf.gyp:protobuf_lite', ], }, - { - # Protobuf compiler / generate rule for google apps policy - # TODO(danno): This rule shares a lot with the user feedback proto rule - # and probably should be generalized to handle both - 'target_name': 'device_management_backend_proto', - 'type': 'none', - 'sources': [ - 'browser/policy/proto/device_management_backend.proto', - ], - 'rules': [ - { - 'rule_name': 'genproto', - 'extension': 'proto', - 'inputs': [ - '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)protoc<(EXECUTABLE_SUFFIX)', - ], - 'variables': { - # The protoc compiler requires a proto_path argument with the - # directory containing the .proto file. - # There's no generator variable that corresponds to this, so fake it. - 'rule_input_relpath': 'browser/policy/proto', - }, - 'outputs': [ - '<(protoc_out_dir)/chrome/<(rule_input_relpath)/<(RULE_INPUT_ROOT).pb.h', - '<(protoc_out_dir)/chrome/<(rule_input_relpath)/<(RULE_INPUT_ROOT).pb.cc', - ], - 'action': [ - '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)protoc<(EXECUTABLE_SUFFIX)', - '--proto_path=./<(rule_input_relpath)', - './<(rule_input_relpath)/<(RULE_INPUT_ROOT)<(RULE_INPUT_EXT)', - '--cpp_out=<(protoc_out_dir)/chrome/<(rule_input_relpath)', - ], - 'message': 'Generating C++ code from <(RULE_INPUT_PATH)', - }, - ], - 'dependencies': [ - '../third_party/protobuf/protobuf.gyp:protobuf_lite', - '../third_party/protobuf/protobuf.gyp:protoc#host', - ], - 'direct_dependent_settings': { - 'include_dirs': [ - '<(protoc_out_dir)', - ] - }, - 'export_dependent_settings': [ - '../third_party/protobuf/protobuf.gyp:protobuf_lite', - ], - }, ], } diff --git a/chrome/test/data/policy/device_management b/chrome/test/data/policy/device_management new file mode 100644 index 0000000..06a49f9 --- /dev/null +++ b/chrome/test/data/policy/device_management @@ -0,0 +1,3 @@ +{ + "HomepageLocation" : "http://www.chromium.org" +} diff --git a/net/net.gyp b/net/net.gyp index 5283400..306ee2b 100644 --- a/net/net.gyp +++ b/net/net.gyp @@ -1174,6 +1174,7 @@ ['inside_chromium_build==1', { 'dependencies': [ '../chrome/browser/sync/protocol/sync_proto.gyp:sync_proto', + '../chrome/browser/policy/proto/device_management_proto.gyp:device_management_proto', '../third_party/protobuf/protobuf.gyp:py_proto', ], }], diff --git a/net/test/test_server.cc b/net/test/test_server.cc index f5592a2..8e92699 100644 --- a/net/test/test_server.cc +++ b/net/test/test_server.cc @@ -292,6 +292,8 @@ bool TestServer::SetPythonPath() { AppendToPythonPath(pyproto_code_dir); AppendToPythonPath(pyproto_code_dir.Append(FILE_PATH_LITERAL("sync_pb"))); + AppendToPythonPath(pyproto_code_dir.Append( + FILE_PATH_LITERAL("device_management_pb"))); return true; } diff --git a/net/tools/testserver/device_management.py b/net/tools/testserver/device_management.py new file mode 100644 index 0000000..7037608 --- /dev/null +++ b/net/tools/testserver/device_management.py @@ -0,0 +1,318 @@ +#!/usr/bin/python2.5 +# Copyright (c) 2010 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 +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: + +{ + "HomepageLocation" : "http://www.chromium.org" +} + +""" + +import cgi +import logging +import random +import re +import sys + +# 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 device_management_backend_pb2 as dm + +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) + + self.DumpMessage('Request', rmsg) + + request_type = self.GetUniqueParam('request') + if request_type == 'register': + return self.ProcessRegister(rmsg.register_request) + elif request_type == 'unregister': + return self.ProcessUnregister(rmsg.unregister_request) + elif request_type == 'policy': + return self.ProcessPolicy(rmsg.policy_request) + else: + return (400, 'Invalid request parameter') + + 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. + match = re.match('GoogleLogin auth=(\\w+)', + self._headers.getheader('Authorization', '')) + if not match: + return (403, 'No authorization') + auth_token = match.group(1) + + device_id = self.GetUniqueParam('deviceid') + if not device_id: + return (400, 'Missing device identifier') + + # Register the device and create a token. + dmtoken = self._server.RegisterDevice(device_id) + + # Send back the reply. + response = dm.DeviceManagementResponse() + response.error = dm.DeviceManagementResponse.SUCCESS + response.register_response.device_management_token = dmtoken + + 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.error = dm.DeviceManagementResponse.SUCCESS + response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) + + 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. + + 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.error = dm.DeviceManagementResponse.SUCCESS + 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 == 'cros/device': + setting = response.policy_response.setting.add() + setting.policy_key = 'chrome-policy' + policy_value = dm.GenericSetting() + for (key, value) in self._server.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 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 and error response. If the token + is None, the error response is a pair of status code and error message. + + Returns: + A pair of DM token and error response. If the token is None, the message + will contain the error response to send back. + """ + error = None + + 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 + elif not self._server.LookupDevice(dmtoken): + error = dm.DeviceManagementResponse.DEVICE_NOT_FOUND + else: + return (dmtoken, 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): + """Initializes the server. + + Args: + policy_path: Names the file to read JSON-formatted policy from. + """ + self._registered_devices = {} + self.policy = {} + 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 + + 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): + """Registers a device and generate 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) + self._registered_devices[dmtoken] = device_id + return dmtoken + + def LookupDevice(self, dmtoken): + """Looks up a device by DMToken. + + Args: + dmtoken: The device management token provided by the client. + + Returns: + The corresponding device identifier or None if not found. + """ + return self._registered_devices.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_devices: + del self._registered_devices[dmtoken] diff --git a/net/tools/testserver/testserver.py b/net/tools/testserver/testserver.py index 55aa6a9..53444e3 100755 --- a/net/tools/testserver/testserver.py +++ b/net/tools/testserver/testserver.py @@ -146,7 +146,8 @@ class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.EchoTitleHandler, self.EchoAllHandler, self.ChromiumSyncCommandHandler, - self.EchoHandler] + self._get_handlers + self.EchoHandler, + self.DeviceManagementHandler] + self._get_handlers self._put_handlers = [ self.EchoTitleHandler, self.EchoAllHandler, @@ -1104,6 +1105,29 @@ class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.wfile.write(contents) return True + def DeviceManagementHandler(self): + """Delegates to the device management service used for cloud policy.""" + if not self._ShouldHandleRequest("/device_management"): + return False + + length = int(self.headers.getheader('content-length')) + raw_request = self.rfile.read(length) + + if not self.server._device_management_handler: + import device_management + policy_path = os.path.join(self.server.data_dir, 'device_management') + self.server._device_management_handler = ( + device_management.TestServer(policy_path)) + + http_response, raw_reply = ( + self.server._device_management_handler.HandleRequest(self.path, + self.headers, + raw_request)) + self.send_response(http_response) + self.end_headers() + self.wfile.write(raw_reply) + return True + def do_CONNECT(self): for handler in self._connect_handlers: if handler(): @@ -1199,6 +1223,7 @@ def main(options, args): server.data_dir = MakeDataDir() server.file_root_url = options.file_root_url server._sync_handler = None + server._device_management_handler = None # means FTP Server else: |