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