Cleanup BrowserPluginEmbedder
[chromium-blink-merge.git] / chrome / browser / policy / test / policy_testserver.py
blob185296c137febd111c283f96cdcedbff81f97045
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """A bare-bones test server for testing cloud policy support.
7 This implements a simple cloud policy test server that can be used to test
8 chrome's device management service client. The policy information is read from
9 the file named device_management in the server's data directory. It contains
10 enforced and recommended policies for the device and user scope, and a list
11 of managed users.
13 The format of the file is JSON. The root dictionary contains a list under the
14 key "managed_users". It contains auth tokens for which the server will claim
15 that the user is managed. The token string "*" indicates that all users are
16 claimed to be managed. Other keys in the root dictionary identify request
17 scopes. The user-request scope is described by a dictionary that holds two
18 sub-dictionaries: "mandatory" and "recommended". Both these hold the policy
19 definitions as key/value stores, their format is identical to what the Linux
20 implementation reads from /etc.
21 The device-scope holds the policy-definition directly as key/value stores in the
22 protobuf-format.
24 Example:
27 "google/chromeos/device" : {
28 "guest_mode_enabled" : false
30 "google/chromeos/user" : {
31 "mandatory" : {
32 "HomepageLocation" : "http://www.chromium.org",
33 "IncognitoEnabled" : false
35 "recommended" : {
36 "JavascriptEnabled": false
39 "google/chromeos/publicaccount/user@example.com" : {
40 "mandatory" : {
41 "HomepageLocation" : "http://www.chromium.org"
43 "recommended" : {
46 "managed_users" : [
47 "secret123456"
49 "current_key_index": 0,
50 "robot_api_auth_code": "fake_auth_code",
51 "invalidation_source": 1025,
52 "invalidation_name": "UENUPOL"
55 """
57 import base64
58 import BaseHTTPServer
59 import cgi
60 import glob
61 import google.protobuf.text_format
62 import hashlib
63 import logging
64 import os
65 import random
66 import re
67 import sys
68 import time
69 import tlslite
70 import tlslite.api
71 import tlslite.utils
72 import tlslite.utils.cryptomath
73 import urlparse
75 # The name and availability of the json module varies in python versions.
76 try:
77 import simplejson as json
78 except ImportError:
79 try:
80 import json
81 except ImportError:
82 json = None
84 import asn1der
85 import testserver_base
87 import device_management_backend_pb2 as dm
88 import cloud_policy_pb2 as cp
90 # Policy for extensions is not supported on Android nor iOS.
91 try:
92 import chrome_extension_policy_pb2 as ep
93 except ImportError:
94 ep = None
96 # Device policy is only available on Chrome OS builds.
97 try:
98 import chrome_device_policy_pb2 as dp
99 except ImportError:
100 dp = None
102 # ASN.1 object identifier for PKCS#1/RSA.
103 PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
105 # List of bad machine identifiers that trigger the |valid_serial_number_missing|
106 # flag to be set set in the policy fetch response.
107 BAD_MACHINE_IDS = [ '123490EN400015' ]
109 # List of machines that trigger the server to send kiosk enrollment response
110 # for the register request.
111 KIOSK_MACHINE_IDS = [ 'KIOSK' ]
113 # Dictionary containing base64-encoded policy signing keys plus per-domain
114 # signatures. Format is:
116 # 'key': <base64-encoded PKCS8-format private key>,
117 # 'signatures': {
118 # <domain1>: <base64-encdoded SHA256 signature for key + domain1>
119 # <domain2>: <base64-encdoded SHA256 signature for key + domain2>
120 # ...
123 SIGNING_KEYS = [
124 # Key1
125 {'key':
126 'MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2c3KzcPqvnJ5HCk3OZkf1'
127 'LMO8Ht4dw4FO2U0EmKvpo0zznj4RwUdmKobH1AFWzwZP4CDY2M67MsukE/1Jnbx1QIDAQ'
128 'ABAkBkKcLZa/75hHVz4PR3tZaw34PATlfxEG6RiRIwXlf/FFlfGIZOSxdW/I1A3XRl0/9'
129 'nZMuctBSKBrcTRZQWfT/hAiEA9g8xbQbMO6BEH/XCRSsQbPlvj4c9wDtVEzeAzZ/ht9kC'
130 'IQDiml+/lXS1emqml711jJcYJNYJzdy1lL/ieKogR59oXQIhAK+Pl4xa1U2VxAWpq7r+R'
131 'vH55wdZT03hB4p2h4gvEzXBAiAkw9kvE0eZPiBZoRrrHIFTOH7FnnHlwBmV2+/2RsiVPQ'
132 'IhAKqx/4qisivvmoM/xbzUagfoxwsu1A/4mGjhBKiS0BCq',
133 'signatures':
134 {'example.com':
135 'l+sT5mziei/GbmiP7VtRCCfwpZcg7uKbW2OlnK5B/TTELutjEIAMdHduNBwbO44qOn'
136 '/5c7YrtkXbBehaaDYFPGI6bGTbDmG9KRxhS+DaB7opgfCQWLi79Gn/jytKLZhRN/VS'
137 'y+PEbezqMi3d1/xDxlThwWZDNwnhv9ER/Nu/32ZTjzgtqonSn2CQtwXCIILm4FdV/1'
138 '/BdmZG+Ge4i4FTqYtInir5YFe611KXU/AveGhQGBIAXo4qYg1IqbVrvKBSU9dlI6Sl'
139 '9TJJLbJ3LGaXuljgFhyMAl3gcy7ftC9MohEmwa+sc7y2mOAgYQ5SSmyAtQwQgAkX9J'
140 '3+tfxjmoA/dg==',
141 'chromepolicytest.com':
142 'TzBiigZKwBdr6lyP6tUDsw+Q9wYO1Yepyxm0O4JZ4RID32L27sWzC1/hwC51fRcCvP'
143 'luEVIW6mH+BFODXMrteUFWfbbG7jgV+Wg+QdzMqgJjxhNKFXPTsZ7/286LAd1vBY/A'
144 'nGd8Wog6AhzfrgMbLNsH794GD0xIUwRvXUWFNP8pClj5VPgQnJrIA9aZwW8FNGbteA'
145 'HacFB0T/oqP5s7XT4Qvkj14RLmCgTwEM8Vcpqy5teJaF8yN17wniveddoOQGH6s0HC'
146 'ocprEccrH5fP/WVAPxCfx4vVYQY5q4CZ4K3f6dTC2FV4IDelM6dugEkvSS02YCzDaO'
147 'N+Z7IwElzTKg==',
148 'managedchrome.com':
149 'T0wXC5w3GXyovA09pyOLX7ui/NI603UfbZXYyTbHI7xtzCIaHVPH35Nx4zdqVrdsej'
150 'ErQ12yVLDDIJokY4Yl+/fj/zrkAPxThI+TNQ+jo0i+al05PuopfpzvCzIXiZBbkbyW'
151 '3XfedxXP3IPN2XU2/3vX+ZXUNG6pxeETem64kGezkjkUraqnHw3JVzwJYHhpMcwdLP'
152 'PYK6V23BbEHEVBtQZd/ledXacz7gOzm1zGni4e+vxA2roAdJWyhbjU0dTKNNUsZmMv'
153 'ryQH9Af1Jw+dqs0RAbhcJXm2i8EUWIgNv6aMn1Z2DzZwKKjXsKgcYSRo8pdYa8RZAo'
154 'UExd9roA9a5w==',
157 # Key2
158 {'key':
159 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmZhreV04M3knCi6wibr49'
160 'oDesHny1G33PKOX9ko8pcxAiu9ZqsKCj7wNW2PGqnLi81fddACwQtYn5xdhCtzB9wIDAQ'
161 'ABAkA0z8m0cy8N08xundspoFZWO71WJLgv/peSDBYGI0RzJR1l9Np355EukQUQwRs5XrL'
162 '3vRQZy2vDqeiR96epkAhRAiEAzJ4DVI8k3pAl7CGv5icqFkJ02viExIwehhIEXBcB6p0C'
163 'IQDAKmzpoRpBEZRQ9xrTvPOi+Ea8Jnd478BU7CI/LFfgowIgMfLIoVWoDGRnvXKju60Hy'
164 'xNB70oHLut9cADp64j6QMkCIDrgxN4QbmrhaAAmtiGKE1wrlgCwCIsVamiasSOKAqLhAi'
165 'EAo/ItVcFtQPod97qG71CY/O4JzOciuU6AMhprs181vfM=',
166 'signatures':
167 # Key2 signatures
168 {'example.com':
169 'cO0nQjRptkeefKDw5QpJSQDavHABxUvbR9Wvoa235OG9Whw1RFqq2ye6pKnI3ezW6/'
170 '7b4ANcpi5a7HV5uF8K7gWyYdxY8NHLeyrbwXxg5j6HAmHmkP1UZcf/dAnWqo7cW8g4'
171 'DIQOhC43KkveMYJ2HnelwdXt/7zqkbe8/3Yj4nhjAUeARx86Sb8Nzydwkrvqs5Jw/x'
172 '5LG+BODExrXXcGu/ubDlW4ivJFqfNUPQysqBXSMY2XCHPJDx3eECLGVVN/fFAWWgjM'
173 'HFObAriAt0b18cc9Nr0mAt4Qq1oDzWcAHCPHE+5dr8Uf46BUrMLJRNRKCY7rrsoIin'
174 '9Be9gs3W+Aww==',
175 'chromepolicytest.com':
176 'mr+9CCYvR0cTvPwlzkxqlpGYy55gY7cPiIkPAPoql51yHK1tkMTOSFru8Dy/nMt+0o'
177 '4z7WO60F1wnIBGkQxnTj/DsO6QpCYi7oHqtLmZ2jsLQFlMyvPGUtpJEFvRwjr/TNbh'
178 '6RqUtz1LQFuJQ848kBrx7nkte1L8SuPDExgx+Q3LtbNj4SuTdvMUBMvEERXiLuwfFL'
179 'BefGjtsqfWETQVlJTCW7xcqOLedIX8UYgEDBpDOZ23A3GzCShuBsIut5m87R5mODht'
180 'EUmKNDK1+OMc6SyDpf+r48Wph4Db1bVaKy8fcpSNJOwEgsrmH7/+owKPGcN7I5jYAF'
181 'Z2PGxHTQ9JNA==',
182 'managedchrome.com':
183 'o5MVSo4bRwIJ/aooGyXpRXsEsWPG8fNA2UTG8hgwnLYhNeJCCnLs/vW2vdp0URE8jn'
184 'qiG4N8KjbuiGw0rJtO1EygdLfpnMEtqYlFjrOie38sy92l/AwohXj6luYzMWL+FqDu'
185 'WQeXasjgyY4s9BOLQVDEnEj3pvqhrk/mXvMwUeXGpbxTNbWAd0C8BTZrGOwU/kIXxo'
186 'vAMGg8L+rQaDwBTEnMsMZcvlrIyqSg5v4BxCWuL3Yd2xvUqZEUWRp1aKetsHRnz5hw'
187 'H7WK7DzvKepDn06XjPG9lchi448U3HB3PRKtCzfO3nD9YXMKTuqRpKPF8PeK11CWh1'
188 'DBvBYwi20vbQ==',
193 class PolicyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
194 """Decodes and handles device management requests from clients.
196 The handler implements all the request parsing and protobuf message decoding
197 and encoding. It calls back into the server to lookup, register, and
198 unregister clients.
201 def __init__(self, request, client_address, server):
202 """Initialize the handler.
204 Args:
205 request: The request data received from the client as a string.
206 client_address: The client address.
207 server: The TestServer object to use for (un)registering clients.
209 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
210 client_address, server)
212 def GetUniqueParam(self, name):
213 """Extracts a unique query parameter from the request.
215 Args:
216 name: Names the parameter to fetch.
217 Returns:
218 The parameter value or None if the parameter doesn't exist or is not
219 unique.
221 if not hasattr(self, '_params'):
222 self._params = cgi.parse_qs(self.path[self.path.find('?') + 1:])
224 param_list = self._params.get(name, [])
225 if len(param_list) == 1:
226 return param_list[0]
227 return None
229 def do_GET(self):
230 """Handles GET requests.
232 Currently this is only used to serve external policy data."""
233 sep = self.path.find('?')
234 path = self.path if sep == -1 else self.path[:sep]
235 if path == '/externalpolicydata':
236 http_response, raw_reply = self.HandleExternalPolicyDataRequest()
237 elif path == '/configuration/test/exit':
238 # This is not part of the standard DM server protocol.
239 # This extension is added to make the test server exit gracefully
240 # when the test is complete.
241 self.server.stop = True
242 http_response = 200
243 raw_reply = 'OK'
244 elif path == '/test/ping':
245 # This path and reply are used by the test setup of host-driven tests for
246 # Android to determine if the server is up, and are not part of the
247 # DM protocol.
248 http_response = 200
249 raw_reply = 'Policy server is up.'
250 else:
251 http_response = 404
252 raw_reply = 'Invalid path'
253 self.send_response(http_response)
254 self.end_headers()
255 self.wfile.write(raw_reply)
257 def do_POST(self):
258 http_response, raw_reply = self.HandleRequest()
259 self.send_response(http_response)
260 if (http_response == 200):
261 self.send_header('Content-Type', 'application/x-protobuffer')
262 self.end_headers()
263 self.wfile.write(raw_reply)
265 def HandleExternalPolicyDataRequest(self):
266 """Handles a request to download policy data for a component."""
267 policy_key = self.GetUniqueParam('key')
268 if not policy_key:
269 return (400, 'Missing key parameter')
270 data = self.server.ReadPolicyDataFromDataDir(policy_key)
271 if data is None:
272 return (404, 'Policy not found for ' + policy_key)
273 return (200, data)
275 def HandleRequest(self):
276 """Handles a request.
278 Parses the data supplied at construction time and returns a pair indicating
279 http status code and response data to be sent back to the client.
281 Returns:
282 A tuple of HTTP status code and response data to send to the client.
284 rmsg = dm.DeviceManagementRequest()
285 length = int(self.headers.getheader('content-length'))
286 rmsg.ParseFromString(self.rfile.read(length))
288 logging.debug('gaia auth token -> ' +
289 self.headers.getheader('Authorization', ''))
290 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token')))
291 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid')))
292 self.DumpMessage('Request', rmsg)
294 request_type = self.GetUniqueParam('request')
295 # Check server side requirements, as defined in
296 # device_management_backend.proto.
297 if (self.GetUniqueParam('devicetype') != '2' or
298 self.GetUniqueParam('apptype') != 'Chrome' or
299 len(self.GetUniqueParam('deviceid')) >= 64):
300 return (400, 'Invalid request parameter')
301 if request_type == 'register':
302 response = self.ProcessRegister(rmsg.register_request)
303 elif request_type == 'api_authorization':
304 response = self.ProcessApiAuthorization(rmsg.service_api_access_request)
305 elif request_type == 'unregister':
306 response = self.ProcessUnregister(rmsg.unregister_request)
307 elif request_type == 'policy':
308 response = self.ProcessPolicy(rmsg, request_type)
309 elif request_type == 'enterprise_check':
310 response = self.ProcessAutoEnrollment(rmsg.auto_enrollment_request)
311 elif request_type == 'device_state_retrieval':
312 response = self.ProcessDeviceStateRetrievalRequest(
313 rmsg.device_state_retrieval_request)
314 elif request_type == 'status_upload':
315 response = self.ProcessStatusUploadRequest(
316 rmsg.device_status_report_request, rmsg.session_status_report_request)
317 else:
318 return (400, 'Invalid request parameter')
320 if isinstance(response[1], basestring):
321 body = response[1]
322 elif isinstance(response[1], google.protobuf.message.Message):
323 self.DumpMessage('Response', response[1])
324 body = response[1].SerializeToString()
325 else:
326 body = ''
327 return (response[0], body)
329 def CreatePolicyForExternalPolicyData(self, policy_key):
330 """Returns an ExternalPolicyData protobuf for policy_key.
332 If there is policy data for policy_key then the download url will be
333 set so that it points to that data, and the appropriate hash is also set.
334 Otherwise, the protobuf will be empty.
336 Args:
337 policy_key: The policy type and settings entity id, joined by '/'.
339 Returns:
340 A serialized ExternalPolicyData.
342 settings = ep.ExternalPolicyData()
343 data = self.server.ReadPolicyDataFromDataDir(policy_key)
344 if data:
345 settings.download_url = urlparse.urljoin(
346 self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key)
347 settings.secure_hash = hashlib.sha256(data).digest()
348 return settings.SerializeToString()
350 def CheckGoogleLogin(self):
351 """Extracts the auth token from the request and returns it. The token may
352 either be a GoogleLogin token from an Authorization header, or an OAuth V2
353 token from the oauth_token query parameter. Returns None if no token is
354 present.
356 oauth_token = self.GetUniqueParam('oauth_token')
357 if oauth_token:
358 return oauth_token
360 match = re.match('GoogleLogin auth=(\\w+)',
361 self.headers.getheader('Authorization', ''))
362 if match:
363 return match.group(1)
365 return None
367 def ProcessRegister(self, msg):
368 """Handles a register request.
370 Checks the query for authorization and device identifier, registers the
371 device with the server and constructs a response.
373 Args:
374 msg: The DeviceRegisterRequest message received from the client.
376 Returns:
377 A tuple of HTTP status code and response data to send to the client.
379 # Check the auth token and device ID.
380 auth = self.CheckGoogleLogin()
381 if not auth:
382 return (403, 'No authorization')
384 policy = self.server.GetPolicies()
385 if ('*' not in policy['managed_users'] and
386 auth not in policy['managed_users']):
387 return (403, 'Unmanaged')
389 device_id = self.GetUniqueParam('deviceid')
390 if not device_id:
391 return (400, 'Missing device identifier')
393 token_info = self.server.RegisterDevice(device_id,
394 msg.machine_id,
395 msg.type)
397 # Send back the reply.
398 response = dm.DeviceManagementResponse()
399 response.register_response.device_management_token = (
400 token_info['device_token'])
401 response.register_response.machine_name = token_info['machine_name']
402 response.register_response.enrollment_type = token_info['enrollment_mode']
404 return (200, response)
406 def ProcessApiAuthorization(self, msg):
407 """Handles an API authorization request.
409 Args:
410 msg: The DeviceServiceApiAccessRequest message received from the client.
412 Returns:
413 A tuple of HTTP status code and response data to send to the client.
415 policy = self.server.GetPolicies()
417 # Return the auth code from the config file if it's defined,
418 # else return a descriptive default value.
419 response = dm.DeviceManagementResponse()
420 response.service_api_access_response.auth_code = policy.get(
421 'robot_api_auth_code', 'policy_testserver.py-auth_code')
423 return (200, response)
425 def ProcessUnregister(self, msg):
426 """Handles a register request.
428 Checks for authorization, unregisters the device and constructs the
429 response.
431 Args:
432 msg: The DeviceUnregisterRequest message received from the client.
434 Returns:
435 A tuple of HTTP status code and response data to send to the client.
437 # Check the management token.
438 token, response = self.CheckToken()
439 if not token:
440 return response
442 # Unregister the device.
443 self.server.UnregisterDevice(token['device_token'])
445 # Prepare and send the response.
446 response = dm.DeviceManagementResponse()
447 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
449 return (200, response)
451 def ProcessPolicy(self, msg, request_type):
452 """Handles a policy request.
454 Checks for authorization, encodes the policy into protobuf representation
455 and constructs the response.
457 Args:
458 msg: The DeviceManagementRequest message received from the client.
460 Returns:
461 A tuple of HTTP status code and response data to send to the client.
463 token_info, error = self.CheckToken()
464 if not token_info:
465 return error
467 key_update_request = msg.device_state_key_update_request
468 if len(key_update_request.server_backed_state_key) > 0:
469 self.server.UpdateStateKeys(token_info['device_token'],
470 key_update_request.server_backed_state_key)
472 # If this is a |publicaccount| request, get the |username| now and use
473 # it in every PolicyFetchResponse produced. This is required to validate
474 # policy for extensions in device-local accounts.
475 # Unfortunately, the |username| can't be obtained from |msg| because that
476 # requires interacting with GAIA.
477 username = None
478 for request in msg.policy_request.request:
479 if request.policy_type == 'google/chromeos/publicaccount':
480 username = request.settings_entity_id
482 response = dm.DeviceManagementResponse()
483 for request in msg.policy_request.request:
484 if (request.policy_type in
485 ('google/android/user',
486 'google/chromeos/device',
487 'google/chromeos/publicaccount',
488 'google/chromeos/user',
489 'google/chrome/user',
490 'google/ios/user')):
491 fetch_response = response.policy_response.response.add()
492 self.ProcessCloudPolicy(request, token_info, fetch_response, username)
493 elif request.policy_type == 'google/chrome/extension':
494 self.ProcessCloudPolicyForExtensions(
495 request, response.policy_response, token_info, username)
496 else:
497 fetch_response.error_code = 400
498 fetch_response.error_message = 'Invalid policy_type'
500 return (200, response)
502 def ProcessAutoEnrollment(self, msg):
503 """Handles an auto-enrollment check request.
505 The reply depends on the value of the modulus:
506 1: replies with no new modulus and the sha256 hash of "0"
507 2: replies with a new modulus, 4.
508 4: replies with a new modulus, 2.
509 8: fails with error 400.
510 16: replies with a new modulus, 16.
511 32: replies with a new modulus, 1.
512 anything else: replies with no new modulus and an empty list of hashes
514 These allow the client to pick the testing scenario its wants to simulate.
516 Args:
517 msg: The DeviceAutoEnrollmentRequest message received from the client.
519 Returns:
520 A tuple of HTTP status code and response data to send to the client.
522 auto_enrollment_response = dm.DeviceAutoEnrollmentResponse()
524 if msg.modulus == 1:
525 auto_enrollment_response.hash.extend(
526 self.server.GetMatchingStateKeyHashes(msg.modulus, msg.remainder))
527 elif msg.modulus == 2:
528 auto_enrollment_response.expected_modulus = 4
529 elif msg.modulus == 4:
530 auto_enrollment_response.expected_modulus = 2
531 elif msg.modulus == 8:
532 return (400, 'Server error')
533 elif msg.modulus == 16:
534 auto_enrollment_response.expected_modulus = 16
535 elif msg.modulus == 32:
536 auto_enrollment_response.expected_modulus = 1
538 response = dm.DeviceManagementResponse()
539 response.auto_enrollment_response.CopyFrom(auto_enrollment_response)
540 return (200, response)
542 def ProcessDeviceStateRetrievalRequest(self, msg):
543 """Handles a device state retrieval request.
545 Response data is taken from server configuration.
547 Returns:
548 A tuple of HTTP status code and response data to send to the client.
550 device_state_retrieval_response = dm.DeviceStateRetrievalResponse()
552 client = self.server.LookupByStateKey(msg.server_backed_state_key)
553 if client is not None:
554 state = self.server.GetPolicies().get('device_state', {})
555 FIELDS = [
556 'management_domain',
557 'restore_mode',
559 for field in FIELDS:
560 if field in state:
561 setattr(device_state_retrieval_response, field, state[field])
563 response = dm.DeviceManagementResponse()
564 response.device_state_retrieval_response.CopyFrom(
565 device_state_retrieval_response)
566 return (200, response)
568 def ProcessStatusUploadRequest(self, device_status, session_status):
569 """Handles a device/session status upload request.
571 Returns:
572 A tuple of HTTP status code and response data to send to the client.
574 # Empty responses indicate a successful upload.
575 device_status_report_response = dm.DeviceStatusReportResponse()
576 session_status_report_response = dm.SessionStatusReportResponse()
578 response = dm.DeviceManagementResponse()
579 response.device_status_report_response.CopyFrom(
580 device_status_report_response)
581 response.session_status_report_response.CopyFrom(
582 session_status_report_response)
584 return (200, response)
586 def SetProtobufMessageField(self, group_message, field, field_value):
587 """Sets a field in a protobuf message.
589 Args:
590 group_message: The protobuf message.
591 field: The field of the message to set, it should be a member of
592 group_message.DESCRIPTOR.fields.
593 field_value: The value to set.
595 if field.label == field.LABEL_REPEATED:
596 assert type(field_value) == list
597 entries = group_message.__getattribute__(field.name)
598 if field.message_type is None:
599 for list_item in field_value:
600 entries.append(list_item)
601 else:
602 # This field is itself a protobuf.
603 sub_type = field.message_type
604 for sub_value in field_value:
605 assert type(sub_value) == dict
606 # Add a new sub-protobuf per list entry.
607 sub_message = entries.add()
608 # Now iterate over its fields and recursively add them.
609 for sub_field in sub_message.DESCRIPTOR.fields:
610 if sub_field.name in sub_value:
611 value = sub_value[sub_field.name]
612 self.SetProtobufMessageField(sub_message, sub_field, value)
613 return
614 elif field.type == field.TYPE_BOOL:
615 assert type(field_value) == bool
616 elif field.type == field.TYPE_STRING:
617 assert type(field_value) == str or type(field_value) == unicode
618 elif field.type == field.TYPE_INT64:
619 assert type(field_value) == int
620 elif (field.type == field.TYPE_MESSAGE and
621 field.message_type.name == 'StringList'):
622 assert type(field_value) == list
623 entries = group_message.__getattribute__(field.name).entries
624 for list_item in field_value:
625 entries.append(list_item)
626 return
627 else:
628 raise Exception('Unknown field type %s' % field.type)
629 group_message.__setattr__(field.name, field_value)
631 def GatherDevicePolicySettings(self, settings, policies):
632 """Copies all the policies from a dictionary into a protobuf of type
633 CloudDeviceSettingsProto.
635 Args:
636 settings: The destination ChromeDeviceSettingsProto protobuf.
637 policies: The source dictionary containing policies in JSON format.
639 for group in settings.DESCRIPTOR.fields:
640 # Create protobuf message for group.
641 group_message = eval('dp.' + group.message_type.name + '()')
642 # Indicates if at least one field was set in |group_message|.
643 got_fields = False
644 # Iterate over fields of the message and feed them from the
645 # policy config file.
646 for field in group_message.DESCRIPTOR.fields:
647 field_value = None
648 if field.name in policies:
649 got_fields = True
650 field_value = policies[field.name]
651 self.SetProtobufMessageField(group_message, field, field_value)
652 if got_fields:
653 settings.__getattribute__(group.name).CopyFrom(group_message)
655 def GatherUserPolicySettings(self, settings, policies):
656 """Copies all the policies from a dictionary into a protobuf of type
657 CloudPolicySettings.
659 Args:
660 settings: The destination: a CloudPolicySettings protobuf.
661 policies: The source: a dictionary containing policies under keys
662 'recommended' and 'mandatory'.
664 for field in settings.DESCRIPTOR.fields:
665 # |field| is the entry for a specific policy in the top-level
666 # CloudPolicySettings proto.
668 # Look for this policy's value in the mandatory or recommended dicts.
669 if field.name in policies.get('mandatory', {}):
670 mode = cp.PolicyOptions.MANDATORY
671 value = policies['mandatory'][field.name]
672 elif field.name in policies.get('recommended', {}):
673 mode = cp.PolicyOptions.RECOMMENDED
674 value = policies['recommended'][field.name]
675 else:
676 continue
678 # Create protobuf message for this policy.
679 policy_message = eval('cp.' + field.message_type.name + '()')
680 policy_message.policy_options.mode = mode
681 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value']
682 self.SetProtobufMessageField(policy_message, field_descriptor, value)
683 settings.__getattribute__(field.name).CopyFrom(policy_message)
685 def ProcessCloudPolicyForExtensions(self, request, response, token_info,
686 username=None):
687 """Handles a request for policy for extensions.
689 A request for policy for extensions is slightly different from the other
690 cloud policy requests, because it can trigger 0, one or many
691 PolicyFetchResponse messages in the response.
693 Args:
694 request: The PolicyFetchRequest that triggered this handler.
695 response: The DevicePolicyResponse message for the response. Multiple
696 PolicyFetchResponses will be appended to this message.
697 token_info: The token extracted from the request.
698 username: The username for the response. May be None.
700 # Send one PolicyFetchResponse for each extension that has
701 # configuration data at the server.
702 ids = self.server.ListMatchingComponents('google/chrome/extension')
703 for settings_entity_id in ids:
704 # Reuse the extension policy request, to trigger the same signature
705 # type in the response.
706 request.settings_entity_id = settings_entity_id
707 fetch_response = response.response.add()
708 self.ProcessCloudPolicy(request, token_info, fetch_response, username)
709 # Don't do key rotations for these messages.
710 fetch_response.ClearField('new_public_key')
711 fetch_response.ClearField('new_public_key_signature')
712 fetch_response.ClearField('new_public_key_verification_signature')
714 def ProcessCloudPolicy(self, msg, token_info, response, username=None):
715 """Handles a cloud policy request. (New protocol for policy requests.)
717 Encodes the policy into protobuf representation, signs it and constructs
718 the response.
720 Args:
721 msg: The CloudPolicyRequest message received from the client.
722 token_info: The token extracted from the request.
723 response: A PolicyFetchResponse message that should be filled with the
724 response data.
725 username: The username for the response. May be None.
728 if msg.machine_id:
729 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id)
731 # Response is only given if the scope is specified in the config file.
732 # Normally 'google/chromeos/device', 'google/chromeos/user' and
733 # 'google/chromeos/publicaccount' should be accepted.
734 policy = self.server.GetPolicies()
735 policy_value = ''
736 policy_key = msg.policy_type
737 if msg.settings_entity_id:
738 policy_key += '/' + msg.settings_entity_id
739 if msg.policy_type in token_info['allowed_policy_types']:
740 if msg.policy_type in ('google/android/user',
741 'google/chromeos/publicaccount',
742 'google/chromeos/user',
743 'google/chrome/user',
744 'google/ios/user'):
745 settings = cp.CloudPolicySettings()
746 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
747 if payload is None:
748 self.GatherUserPolicySettings(settings, policy.get(policy_key, {}))
749 payload = settings.SerializeToString()
750 elif dp is not None and msg.policy_type == 'google/chromeos/device':
751 settings = dp.ChromeDeviceSettingsProto()
752 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
753 if payload is None:
754 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {}))
755 payload = settings.SerializeToString()
756 elif ep is not None and msg.policy_type == 'google/chrome/extension':
757 settings = ep.ExternalPolicyData()
758 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
759 if payload is None:
760 payload = self.CreatePolicyForExternalPolicyData(policy_key)
761 else:
762 response.error_code = 400
763 response.error_message = 'Invalid policy type'
764 return
765 else:
766 response.error_code = 400
767 response.error_message = 'Request not allowed for the token used'
768 return
770 # Sign with 'current_key_index', defaulting to key 0.
771 signing_key = None
772 req_key = None
773 current_key_index = policy.get('current_key_index', 0)
774 nkeys = len(self.server.keys)
775 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and
776 current_key_index in range(nkeys)):
777 signing_key = self.server.keys[current_key_index]
778 if msg.public_key_version in range(1, nkeys + 1):
779 # requested key exists, use for signing and rotate.
780 req_key = self.server.keys[msg.public_key_version - 1]['private_key']
782 # Fill the policy data protobuf.
783 policy_data = dm.PolicyData()
784 policy_data.policy_type = msg.policy_type
785 policy_data.timestamp = int(time.time() * 1000)
786 policy_data.request_token = token_info['device_token']
787 policy_data.policy_value = payload
788 policy_data.machine_name = token_info['machine_name']
789 policy_data.valid_serial_number_missing = (
790 token_info['machine_id'] in BAD_MACHINE_IDS)
791 policy_data.settings_entity_id = msg.settings_entity_id
792 policy_data.service_account_identity = policy.get(
793 'service_account_identity',
794 'policy_testserver.py-service_account_identity')
795 invalidation_source = policy.get('invalidation_source')
796 if invalidation_source is not None:
797 policy_data.invalidation_source = invalidation_source
798 # Since invalidation_name is type bytes in the proto, the Unicode name
799 # provided needs to be encoded as ASCII to set the correct byte pattern.
800 invalidation_name = policy.get('invalidation_name')
801 if invalidation_name is not None:
802 policy_data.invalidation_name = invalidation_name.encode('ascii')
804 if signing_key:
805 policy_data.public_key_version = current_key_index + 1
807 if username:
808 policy_data.username = username
809 else:
810 # For regular user/device policy, there is no way for the testserver to
811 # know the user name belonging to the GAIA auth token we received (short
812 # of actually talking to GAIA). To address this, we read the username from
813 # the policy configuration dictionary, or use a default.
814 policy_data.username = policy.get('policy_user', 'user@example.com')
815 policy_data.device_id = token_info['device_id']
816 signed_data = policy_data.SerializeToString()
818 response.policy_data = signed_data
819 if signing_key:
820 response.policy_data_signature = (
821 bytes(signing_key['private_key'].hashAndSign(signed_data)))
822 if msg.public_key_version != current_key_index + 1:
823 response.new_public_key = signing_key['public_key']
825 # Set the verification signature appropriate for the policy domain.
826 # TODO(atwilson): Use the enrollment domain for public accounts when
827 # we add key validation for ChromeOS (http://crbug.com/328038).
828 if 'signatures' in signing_key:
829 verification_sig = self.GetSignatureForDomain(
830 signing_key['signatures'], policy_data.username)
832 if verification_sig:
833 assert len(verification_sig) == 256, \
834 'bad signature size: %d' % len(verification_sig)
835 response.new_public_key_verification_signature = verification_sig
837 if req_key:
838 response.new_public_key_signature = (
839 bytes(req_key.hashAndSign(response.new_public_key)))
841 return (200, response.SerializeToString())
843 def GetSignatureForDomain(self, signatures, username):
844 parsed_username = username.split("@", 1)
845 if len(parsed_username) != 2:
846 logging.error('Could not extract domain from username: %s' % username)
847 return None
848 domain = parsed_username[1]
850 # Lookup the domain's signature in the passed dictionary. If none is found,
851 # fallback to a wildcard signature.
852 if domain in signatures:
853 return signatures[domain]
854 if '*' in signatures:
855 return signatures['*']
857 # No key matching this domain.
858 logging.error('No verification signature matching domain: %s' % domain)
859 return None
861 def CheckToken(self):
862 """Helper for checking whether the client supplied a valid DM token.
864 Extracts the token from the request and passed to the server in order to
865 look up the client.
867 Returns:
868 A pair of token information record and error response. If the first
869 element is None, then the second contains an error code to send back to
870 the client. Otherwise the first element is the same structure that is
871 returned by LookupToken().
873 error = 500
874 dmtoken = None
875 request_device_id = self.GetUniqueParam('deviceid')
876 match = re.match('GoogleDMToken token=(\\w+)',
877 self.headers.getheader('Authorization', ''))
878 if match:
879 dmtoken = match.group(1)
880 if not dmtoken:
881 error = 401
882 else:
883 token_info = self.server.LookupToken(dmtoken)
884 if (not token_info or
885 not request_device_id or
886 token_info['device_id'] != request_device_id):
887 error = 410
888 else:
889 return (token_info, None)
891 logging.debug('Token check failed with error %d' % error)
893 return (None, (error, 'Server error %d' % error))
895 def DumpMessage(self, label, msg):
896 """Helper for logging an ASCII dump of a protobuf message."""
897 logging.debug('%s\n%s' % (label, str(msg)))
900 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn,
901 testserver_base.StoppableHTTPServer):
902 """Handles requests and keeps global service state."""
904 def __init__(self, server_address, data_dir, policy_path, client_state_file,
905 private_key_paths, server_base_url):
906 """Initializes the server.
908 Args:
909 server_address: Server host and port.
910 policy_path: Names the file to read JSON-formatted policy from.
911 private_key_paths: List of paths to read private keys from.
913 testserver_base.StoppableHTTPServer.__init__(self, server_address,
914 PolicyRequestHandler)
915 self._registered_tokens = {}
916 self.data_dir = data_dir
917 self.policy_path = policy_path
918 self.client_state_file = client_state_file
919 self.server_base_url = server_base_url
921 self.keys = []
922 if private_key_paths:
923 # Load specified keys from the filesystem.
924 for key_path in private_key_paths:
925 try:
926 key_str = open(key_path).read()
927 except IOError:
928 print 'Failed to load private key from %s' % key_path
929 continue
930 try:
931 key = tlslite.api.parsePEMKey(key_str, private=True)
932 except SyntaxError:
933 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
934 bytearray(key_str))
936 assert key is not None
937 key_info = { 'private_key' : key }
939 # Now try to read in a signature, if one exists.
940 try:
941 key_sig = open(key_path + '.sig').read()
942 # Create a dictionary with the wildcard domain + signature
943 key_info['signatures'] = {'*': key_sig}
944 except IOError:
945 print 'Failed to read validation signature from %s.sig' % key_path
946 self.keys.append(key_info)
947 else:
948 # Use the canned private keys if none were passed from the command line.
949 for signing_key in SIGNING_KEYS:
950 decoded_key = base64.b64decode(signing_key['key']);
951 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
952 bytearray(decoded_key))
953 assert key is not None
954 # Grab the signature dictionary for this key and decode all of the
955 # signatures.
956 signature_dict = signing_key['signatures']
957 decoded_signatures = {}
958 for domain in signature_dict:
959 decoded_signatures[domain] = base64.b64decode(signature_dict[domain])
960 self.keys.append({'private_key': key,
961 'signatures': decoded_signatures})
963 # Derive the public keys from the private keys.
964 for entry in self.keys:
965 key = entry['private_key']
967 algorithm = asn1der.Sequence(
968 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
969 asn1der.Data(asn1der.NULL, '') ])
970 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
971 asn1der.Integer(key.e) ])
972 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
973 entry['public_key'] = pubkey
975 # Load client state.
976 if self.client_state_file is not None:
977 try:
978 file_contents = open(self.client_state_file).read()
979 self._registered_tokens = json.loads(file_contents, strict=False)
980 except IOError:
981 pass
983 def GetPolicies(self):
984 """Returns the policies to be used, reloaded form the backend file every
985 time this is called.
987 policy = {}
988 if json is None:
989 print 'No JSON module, cannot parse policy information'
990 else :
991 try:
992 policy = json.loads(open(self.policy_path).read(), strict=False)
993 except IOError:
994 print 'Failed to load policy from %s' % self.policy_path
995 return policy
997 def RegisterDevice(self, device_id, machine_id, type):
998 """Registers a device or user and generates a DM token for it.
1000 Args:
1001 device_id: The device identifier provided by the client.
1003 Returns:
1004 The newly generated device token for the device.
1006 dmtoken_chars = []
1007 while len(dmtoken_chars) < 32:
1008 dmtoken_chars.append(random.choice('0123456789abcdef'))
1009 dmtoken = ''.join(dmtoken_chars)
1010 allowed_policy_types = {
1011 dm.DeviceRegisterRequest.BROWSER: [
1012 'google/chrome/user',
1013 'google/chrome/extension'
1015 dm.DeviceRegisterRequest.USER: [
1016 'google/chromeos/user',
1017 'google/chrome/extension'
1019 dm.DeviceRegisterRequest.DEVICE: [
1020 'google/chromeos/device',
1021 'google/chromeos/publicaccount',
1022 'google/chrome/extension'
1024 dm.DeviceRegisterRequest.ANDROID_BROWSER: [
1025 'google/android/user'
1027 dm.DeviceRegisterRequest.IOS_BROWSER: [
1028 'google/ios/user'
1030 dm.DeviceRegisterRequest.TT: ['google/chromeos/user',
1031 'google/chrome/user'],
1033 if machine_id in KIOSK_MACHINE_IDS:
1034 enrollment_mode = dm.DeviceRegisterResponse.RETAIL
1035 else:
1036 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE
1037 self._registered_tokens[dmtoken] = {
1038 'device_id': device_id,
1039 'device_token': dmtoken,
1040 'allowed_policy_types': allowed_policy_types[type],
1041 'machine_name': 'chromeos-' + machine_id,
1042 'machine_id': machine_id,
1043 'enrollment_mode': enrollment_mode,
1045 self.WriteClientState()
1046 return self._registered_tokens[dmtoken]
1048 def UpdateMachineId(self, dmtoken, machine_id):
1049 """Updates the machine identifier for a registered device.
1051 Args:
1052 dmtoken: The device management token provided by the client.
1053 machine_id: Updated hardware identifier value.
1055 if dmtoken in self._registered_tokens:
1056 self._registered_tokens[dmtoken]['machine_id'] = machine_id
1057 self.WriteClientState()
1059 def UpdateStateKeys(self, dmtoken, state_keys):
1060 """Updates the state keys for a given client.
1062 Args:
1063 dmtoken: The device management token provided by the client.
1064 state_keys: The state keys to set.
1066 if dmtoken in self._registered_tokens:
1067 self._registered_tokens[dmtoken]['state_keys'] = map(
1068 lambda key : key.encode('hex'), state_keys)
1069 self.WriteClientState()
1071 def LookupToken(self, dmtoken):
1072 """Looks up a device or a user by DM token.
1074 Args:
1075 dmtoken: The device management token provided by the client.
1077 Returns:
1078 A dictionary with information about a device or user that is registered by
1079 dmtoken, or None if the token is not found.
1081 return self._registered_tokens.get(dmtoken, None)
1083 def LookupByStateKey(self, state_key):
1084 """Looks up a device or a user by a state key.
1086 Args:
1087 state_key: The state key provided by the client.
1089 Returns:
1090 A dictionary with information about a device or user or None if there is
1091 no matching record.
1093 for client in self._registered_tokens.values():
1094 if state_key.encode('hex') in client.get('state_keys', []):
1095 return client
1097 return None
1099 def GetMatchingStateKeyHashes(self, modulus, remainder):
1100 """Returns all clients registered with the server.
1102 Returns:
1103 The list of registered clients.
1105 state_keys = sum([ c.get('state_keys', [])
1106 for c in self._registered_tokens.values() ], [])
1107 hashed_keys = map(lambda key: hashlib.sha256(key.decode('hex')).digest(),
1108 set(state_keys))
1109 return filter(
1110 lambda hash : int(hash.encode('hex'), 16) % modulus == remainder,
1111 hashed_keys)
1113 def UnregisterDevice(self, dmtoken):
1114 """Unregisters a device identified by the given DM token.
1116 Args:
1117 dmtoken: The device management token provided by the client.
1119 if dmtoken in self._registered_tokens.keys():
1120 del self._registered_tokens[dmtoken]
1121 self.WriteClientState()
1123 def WriteClientState(self):
1124 """Writes the client state back to the file."""
1125 if self.client_state_file is not None:
1126 json_data = json.dumps(self._registered_tokens)
1127 open(self.client_state_file, 'w').write(json_data)
1129 def GetBaseFilename(self, policy_selector):
1130 """Returns the base filename for the given policy_selector.
1132 Args:
1133 policy_selector: The policy type and settings entity id, joined by '/'.
1135 Returns:
1136 The filename corresponding to the policy_selector, without a file
1137 extension.
1139 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector)
1140 return os.path.join(self.data_dir or '',
1141 'policy_%s' % sanitized_policy_selector)
1143 def ListMatchingComponents(self, policy_type):
1144 """Returns a list of settings entity IDs that have a configuration file.
1146 Args:
1147 policy_type: The policy type to look for. Only settings entity IDs for
1148 file selectors That match this policy_type will be returned.
1150 Returns:
1151 A list of settings entity IDs for the given |policy_type| that have a
1152 configuration file in this server (either as a .bin, .txt or .data file).
1154 base_name = self.GetBaseFilename(policy_type)
1155 files = glob.glob('%s_*.*' % base_name)
1156 len_base_name = len(base_name) + 1
1157 return [ file[len_base_name:file.rfind('.')] for file in files ]
1159 def ReadPolicyFromDataDir(self, policy_selector, proto_message):
1160 """Tries to read policy payload from a file in the data directory.
1162 First checks for a binary rendition of the policy protobuf in
1163 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns
1164 it. If that file doesn't exist, tries
1165 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a
1166 protobuf using proto_message. If that fails as well, returns None.
1168 Args:
1169 policy_selector: Selects which policy to read.
1170 proto_message: Optional protobuf message object used for decoding the
1171 proto text format.
1173 Returns:
1174 The binary payload message, or None if not found.
1176 base_filename = self.GetBaseFilename(policy_selector)
1178 # Try the binary payload file first.
1179 try:
1180 return open(base_filename + '.bin').read()
1181 except IOError:
1182 pass
1184 # If that fails, try the text version instead.
1185 if proto_message is None:
1186 return None
1188 try:
1189 text = open(base_filename + '.txt').read()
1190 google.protobuf.text_format.Merge(text, proto_message)
1191 return proto_message.SerializeToString()
1192 except IOError:
1193 return None
1194 except google.protobuf.text_format.ParseError:
1195 return None
1197 def ReadPolicyDataFromDataDir(self, policy_selector):
1198 """Returns the external policy data for |policy_selector| if found.
1200 Args:
1201 policy_selector: Selects which policy to read.
1203 Returns:
1204 The data for the corresponding policy type and entity id, if found.
1206 base_filename = self.GetBaseFilename(policy_selector)
1207 try:
1208 return open(base_filename + '.data').read()
1209 except IOError:
1210 return None
1212 def GetBaseURL(self):
1213 """Returns the server base URL.
1215 Respects the |server_base_url| configuration parameter, if present. Falls
1216 back to construct the URL from the server hostname and port otherwise.
1218 Returns:
1219 The URL to use for constructing URLs that get returned to clients.
1221 base_url = self.server_base_url
1222 if base_url is None:
1223 base_url = 'http://%s:%s' % self.server_address[:2]
1225 return base_url
1228 class PolicyServerRunner(testserver_base.TestServerRunner):
1230 def __init__(self):
1231 super(PolicyServerRunner, self).__init__()
1233 def create_server(self, server_data):
1234 data_dir = self.options.data_dir or ''
1235 config_file = (self.options.config_file or
1236 os.path.join(data_dir, 'device_management'))
1237 server = PolicyTestServer((self.options.host, self.options.port),
1238 data_dir, config_file,
1239 self.options.client_state_file,
1240 self.options.policy_keys,
1241 self.options.server_base_url)
1242 server_data['port'] = server.server_port
1243 return server
1245 def add_options(self):
1246 testserver_base.TestServerRunner.add_options(self)
1247 self.option_parser.add_option('--client-state', dest='client_state_file',
1248 help='File that client state should be '
1249 'persisted to. This allows the server to be '
1250 'seeded by a list of pre-registered clients '
1251 'and restarts without abandoning registered '
1252 'clients.')
1253 self.option_parser.add_option('--policy-key', action='append',
1254 dest='policy_keys',
1255 help='Specify a path to a PEM-encoded '
1256 'private key to use for policy signing. May '
1257 'be specified multiple times in order to '
1258 'load multiple keys into the server. If the '
1259 'server has multiple keys, it will rotate '
1260 'through them in at each request in a '
1261 'round-robin fashion. The server will '
1262 'use a canned key if none is specified '
1263 'on the command line. The test server will '
1264 'also look for a verification signature file '
1265 'in the same location: <filename>.sig and if '
1266 'present will add the signature to the '
1267 'policy blob as appropriate via the '
1268 'new_public_key_verification_signature '
1269 'field.')
1270 self.option_parser.add_option('--log-level', dest='log_level',
1271 default='WARN',
1272 help='Log level threshold to use.')
1273 self.option_parser.add_option('--config-file', dest='config_file',
1274 help='Specify a configuration file to use '
1275 'instead of the default '
1276 '<data_dir>/device_management')
1277 self.option_parser.add_option('--server-base-url', dest='server_base_url',
1278 help='The server base URL to use when '
1279 'constructing URLs to return to the client.')
1281 def run_server(self):
1282 logger = logging.getLogger()
1283 logger.setLevel(getattr(logging, str(self.options.log_level).upper()))
1284 if (self.options.log_to_console):
1285 logger.addHandler(logging.StreamHandler())
1286 if (self.options.log_file):
1287 logger.addHandler(logging.FileHandler(self.options.log_file))
1289 testserver_base.TestServerRunner.run_server(self)
1292 if __name__ == '__main__':
1293 sys.exit(PolicyServerRunner().main())