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    elif path == '/configuration/test/exit':
233      # This is not part of the standard DM server protocol.
234      # This extension is added to make the test server exit gracefully
235      # when the test is complete.
236      self.server.stop = True
237      http_response = 200
238      raw_reply = 'OK'
239    else:
240      http_response = 404
241      raw_reply = 'Invalid path'
242    self.send_response(http_response)
243    self.end_headers()
244    self.wfile.write(raw_reply)
245
246  def do_POST(self):
247    http_response, raw_reply = self.HandleRequest()
248    self.send_response(http_response)
249    if (http_response == 200):
250      self.send_header('Content-Type', 'application/x-protobuffer')
251    self.end_headers()
252    self.wfile.write(raw_reply)
253
254  def HandleExternalPolicyDataRequest(self):
255    """Handles a request to download policy data for a component."""
256    policy_key = self.GetUniqueParam('key')
257    if not policy_key:
258      return (400, 'Missing key parameter')
259    data = self.server.ReadPolicyDataFromDataDir(policy_key)
260    if data is None:
261      return (404, 'Policy not found for ' + policy_key)
262    return (200, data)
263
264  def HandleRequest(self):
265    """Handles a request.
266
267    Parses the data supplied at construction time and returns a pair indicating
268    http status code and response data to be sent back to the client.
269
270    Returns:
271      A tuple of HTTP status code and response data to send to the client.
272    """
273    rmsg = dm.DeviceManagementRequest()
274    length = int(self.headers.getheader('content-length'))
275    rmsg.ParseFromString(self.rfile.read(length))
276
277    logging.debug('gaia auth token -> ' +
278                  self.headers.getheader('Authorization', ''))
279    logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token')))
280    logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid')))
281    self.DumpMessage('Request', rmsg)
282
283    request_type = self.GetUniqueParam('request')
284    # Check server side requirements, as defined in
285    # device_management_backend.proto.
286    if (self.GetUniqueParam('devicetype') != '2' or
287        self.GetUniqueParam('apptype') != 'Chrome' or
288        len(self.GetUniqueParam('deviceid')) >= 64):
289      return (400, 'Invalid request parameter')
290    if request_type == 'register':
291      response = self.ProcessRegister(rmsg.register_request)
292    elif request_type == 'api_authorization':
293      response = self.ProcessApiAuthorization(rmsg.service_api_access_request)
294    elif request_type == 'unregister':
295      response = self.ProcessUnregister(rmsg.unregister_request)
296    elif request_type == 'policy':
297      response = self.ProcessPolicy(rmsg, request_type)
298    elif request_type == 'enterprise_check':
299      response = self.ProcessAutoEnrollment(rmsg.auto_enrollment_request)
300    elif request_type == 'device_state_retrieval':
301      response = self.ProcessDeviceStateRetrievalRequest(
302          rmsg.device_state_retrieval_request)
303    else:
304      return (400, 'Invalid request parameter')
305
306    self.DumpMessage('Response', response[1])
307    return (response[0], response[1].SerializeToString())
308
309  def CreatePolicyForExternalPolicyData(self, policy_key):
310    """Returns an ExternalPolicyData protobuf for policy_key.
311
312    If there is policy data for policy_key then the download url will be
313    set so that it points to that data, and the appropriate hash is also set.
314    Otherwise, the protobuf will be empty.
315
316    Args:
317      policy_key: The policy type and settings entity id, joined by '/'.
318
319    Returns:
320      A serialized ExternalPolicyData.
321    """
322    settings = ep.ExternalPolicyData()
323    data = self.server.ReadPolicyDataFromDataDir(policy_key)
324    if data:
325      settings.download_url = urlparse.urljoin(
326          self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key)
327      settings.secure_hash = hashlib.sha256(data).digest()
328    return settings.SerializeToString()
329
330  def CheckGoogleLogin(self):
331    """Extracts the auth token from the request and returns it. The token may
332    either be a GoogleLogin token from an Authorization header, or an OAuth V2
333    token from the oauth_token query parameter. Returns None if no token is
334    present.
335    """
336    oauth_token = self.GetUniqueParam('oauth_token')
337    if oauth_token:
338      return oauth_token
339
340    match = re.match('GoogleLogin auth=(\\w+)',
341                     self.headers.getheader('Authorization', ''))
342    if match:
343      return match.group(1)
344
345    return None
346
347  def ProcessRegister(self, msg):
348    """Handles a register request.
349
350    Checks the query for authorization and device identifier, registers the
351    device with the server and constructs a response.
352
353    Args:
354      msg: The DeviceRegisterRequest message received from the client.
355
356    Returns:
357      A tuple of HTTP status code and response data to send to the client.
358    """
359    # Check the auth token and device ID.
360    auth = self.CheckGoogleLogin()
361    if not auth:
362      return (403, 'No authorization')
363
364    policy = self.server.GetPolicies()
365    if ('*' not in policy['managed_users'] and
366        auth not in policy['managed_users']):
367      return (403, 'Unmanaged')
368
369    device_id = self.GetUniqueParam('deviceid')
370    if not device_id:
371      return (400, 'Missing device identifier')
372
373    token_info = self.server.RegisterDevice(device_id,
374                                             msg.machine_id,
375                                             msg.type)
376
377    # Send back the reply.
378    response = dm.DeviceManagementResponse()
379    response.register_response.device_management_token = (
380        token_info['device_token'])
381    response.register_response.machine_name = token_info['machine_name']
382    response.register_response.enrollment_type = token_info['enrollment_mode']
383
384    return (200, response)
385
386  def ProcessApiAuthorization(self, msg):
387    """Handles an API authorization request.
388
389    Args:
390      msg: The DeviceServiceApiAccessRequest message received from the client.
391
392    Returns:
393      A tuple of HTTP status code and response data to send to the client.
394    """
395    policy = self.server.GetPolicies()
396
397    # Return the auth code from the config file if it's defined,
398    # else return a descriptive default value.
399    response = dm.DeviceManagementResponse()
400    response.service_api_access_response.auth_code = policy.get(
401        'robot_api_auth_code', 'policy_testserver.py-auth_code')
402
403    return (200, response)
404
405  def ProcessUnregister(self, msg):
406    """Handles a register request.
407
408    Checks for authorization, unregisters the device and constructs the
409    response.
410
411    Args:
412      msg: The DeviceUnregisterRequest message received from the client.
413
414    Returns:
415      A tuple of HTTP status code and response data to send to the client.
416    """
417    # Check the management token.
418    token, response = self.CheckToken()
419    if not token:
420      return response
421
422    # Unregister the device.
423    self.server.UnregisterDevice(token['device_token'])
424
425    # Prepare and send the response.
426    response = dm.DeviceManagementResponse()
427    response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
428
429    return (200, response)
430
431  def ProcessPolicy(self, msg, request_type):
432    """Handles a policy request.
433
434    Checks for authorization, encodes the policy into protobuf representation
435    and constructs the response.
436
437    Args:
438      msg: The DeviceManagementRequest message received from the client.
439
440    Returns:
441      A tuple of HTTP status code and response data to send to the client.
442    """
443    token_info, error = self.CheckToken()
444    if not token_info:
445      return error
446
447    key_update_request = msg.device_state_key_update_request
448    if len(key_update_request.server_backed_state_key) > 0:
449      self.server.UpdateStateKeys(token_info['device_token'],
450                                  key_update_request.server_backed_state_key)
451
452    # If this is a |publicaccount| request, get the |username| now and use
453    # it in every PolicyFetchResponse produced. This is required to validate
454    # policy for extensions in device-local accounts.
455    # Unfortunately, the |username| can't be obtained from |msg| because that
456    # requires interacting with GAIA.
457    username = None
458    for request in msg.policy_request.request:
459      if request.policy_type == 'google/chromeos/publicaccount':
460        username = request.settings_entity_id
461
462    response = dm.DeviceManagementResponse()
463    for request in msg.policy_request.request:
464      if (request.policy_type in
465             ('google/android/user',
466              'google/chromeos/device',
467              'google/chromeos/publicaccount',
468              'google/chromeos/user',
469              'google/chrome/user',
470              'google/ios/user')):
471        fetch_response = response.policy_response.response.add()
472        self.ProcessCloudPolicy(request, token_info, fetch_response, username)
473      elif request.policy_type == 'google/chrome/extension':
474        self.ProcessCloudPolicyForExtensions(
475            request, response.policy_response, token_info, username)
476      else:
477        fetch_response.error_code = 400
478        fetch_response.error_message = 'Invalid policy_type'
479
480    return (200, response)
481
482  def ProcessAutoEnrollment(self, msg):
483    """Handles an auto-enrollment check request.
484
485    The reply depends on the value of the modulus:
486      1: replies with no new modulus and the sha256 hash of "0"
487      2: replies with a new modulus, 4.
488      4: replies with a new modulus, 2.
489      8: fails with error 400.
490      16: replies with a new modulus, 16.
491      32: replies with a new modulus, 1.
492      anything else: replies with no new modulus and an empty list of hashes
493
494    These allow the client to pick the testing scenario its wants to simulate.
495
496    Args:
497      msg: The DeviceAutoEnrollmentRequest message received from the client.
498
499    Returns:
500      A tuple of HTTP status code and response data to send to the client.
501    """
502    auto_enrollment_response = dm.DeviceAutoEnrollmentResponse()
503
504    if msg.modulus == 1:
505      auto_enrollment_response.hash.extend(
506          self.server.GetMatchingStateKeyHashes(msg.modulus, msg.remainder))
507    elif msg.modulus == 2:
508      auto_enrollment_response.expected_modulus = 4
509    elif msg.modulus == 4:
510      auto_enrollment_response.expected_modulus = 2
511    elif msg.modulus == 8:
512      return (400, 'Server error')
513    elif msg.modulus == 16:
514      auto_enrollment_response.expected_modulus = 16
515    elif msg.modulus == 32:
516      auto_enrollment_response.expected_modulus = 1
517
518    response = dm.DeviceManagementResponse()
519    response.auto_enrollment_response.CopyFrom(auto_enrollment_response)
520    return (200, response)
521
522  def ProcessDeviceStateRetrievalRequest(self, msg):
523    """Handles a device state retrieval request.
524
525    Response data is taken from server configuration.
526
527    Returns:
528      A tuple of HTTP status code and response data to send to the client.
529    """
530    device_state_retrieval_response = dm.DeviceStateRetrievalResponse()
531
532    client = self.server.LookupByStateKey(msg.server_backed_state_key)
533    if client is not None:
534      state = self.server.GetPolicies().get('device_state', {})
535      FIELDS = [
536          'management_domain',
537          'restore_mode',
538      ]
539      for field in FIELDS:
540        if field in state:
541          setattr(device_state_retrieval_response, field, state[field])
542
543    response = dm.DeviceManagementResponse()
544    response.device_state_retrieval_response.CopyFrom(
545        device_state_retrieval_response)
546    return (200, response)
547
548  def SetProtobufMessageField(self, group_message, field, field_value):
549    """Sets a field in a protobuf message.
550
551    Args:
552      group_message: The protobuf message.
553      field: The field of the message to set, it should be a member of
554          group_message.DESCRIPTOR.fields.
555      field_value: The value to set.
556    """
557    if field.label == field.LABEL_REPEATED:
558      assert type(field_value) == list
559      entries = group_message.__getattribute__(field.name)
560      if field.message_type is None:
561        for list_item in field_value:
562          entries.append(list_item)
563      else:
564        # This field is itself a protobuf.
565        sub_type = field.message_type
566        for sub_value in field_value:
567          assert type(sub_value) == dict
568          # Add a new sub-protobuf per list entry.
569          sub_message = entries.add()
570          # Now iterate over its fields and recursively add them.
571          for sub_field in sub_message.DESCRIPTOR.fields:
572            if sub_field.name in sub_value:
573              value = sub_value[sub_field.name]
574              self.SetProtobufMessageField(sub_message, sub_field, value)
575      return
576    elif field.type == field.TYPE_BOOL:
577      assert type(field_value) == bool
578    elif field.type == field.TYPE_STRING:
579      assert type(field_value) == str or type(field_value) == unicode
580    elif field.type == field.TYPE_INT64:
581      assert type(field_value) == int
582    elif (field.type == field.TYPE_MESSAGE and
583          field.message_type.name == 'StringList'):
584      assert type(field_value) == list
585      entries = group_message.__getattribute__(field.name).entries
586      for list_item in field_value:
587        entries.append(list_item)
588      return
589    else:
590      raise Exception('Unknown field type %s' % field.type)
591    group_message.__setattr__(field.name, field_value)
592
593  def GatherDevicePolicySettings(self, settings, policies):
594    """Copies all the policies from a dictionary into a protobuf of type
595    CloudDeviceSettingsProto.
596
597    Args:
598      settings: The destination ChromeDeviceSettingsProto protobuf.
599      policies: The source dictionary containing policies in JSON format.
600    """
601    for group in settings.DESCRIPTOR.fields:
602      # Create protobuf message for group.
603      group_message = eval('dp.' + group.message_type.name + '()')
604      # Indicates if at least one field was set in |group_message|.
605      got_fields = False
606      # Iterate over fields of the message and feed them from the
607      # policy config file.
608      for field in group_message.DESCRIPTOR.fields:
609        field_value = None
610        if field.name in policies:
611          got_fields = True
612          field_value = policies[field.name]
613          self.SetProtobufMessageField(group_message, field, field_value)
614      if got_fields:
615        settings.__getattribute__(group.name).CopyFrom(group_message)
616
617  def GatherUserPolicySettings(self, settings, policies):
618    """Copies all the policies from a dictionary into a protobuf of type
619    CloudPolicySettings.
620
621    Args:
622      settings: The destination: a CloudPolicySettings protobuf.
623      policies: The source: a dictionary containing policies under keys
624          'recommended' and 'mandatory'.
625    """
626    for field in settings.DESCRIPTOR.fields:
627      # |field| is the entry for a specific policy in the top-level
628      # CloudPolicySettings proto.
629
630      # Look for this policy's value in the mandatory or recommended dicts.
631      if field.name in policies.get('mandatory', {}):
632        mode = cp.PolicyOptions.MANDATORY
633        value = policies['mandatory'][field.name]
634      elif field.name in policies.get('recommended', {}):
635        mode = cp.PolicyOptions.RECOMMENDED
636        value = policies['recommended'][field.name]
637      else:
638        continue
639
640      # Create protobuf message for this policy.
641      policy_message = eval('cp.' + field.message_type.name + '()')
642      policy_message.policy_options.mode = mode
643      field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value']
644      self.SetProtobufMessageField(policy_message, field_descriptor, value)
645      settings.__getattribute__(field.name).CopyFrom(policy_message)
646
647  def ProcessCloudPolicyForExtensions(self, request, response, token_info,
648                                      username=None):
649    """Handles a request for policy for extensions.
650
651    A request for policy for extensions is slightly different from the other
652    cloud policy requests, because it can trigger 0, one or many
653    PolicyFetchResponse messages in the response.
654
655    Args:
656      request: The PolicyFetchRequest that triggered this handler.
657      response: The DevicePolicyResponse message for the response. Multiple
658      PolicyFetchResponses will be appended to this message.
659      token_info: The token extracted from the request.
660      username: The username for the response. May be None.
661    """
662    # Send one PolicyFetchResponse for each extension that has
663    # configuration data at the server.
664    ids = self.server.ListMatchingComponents('google/chrome/extension')
665    for settings_entity_id in ids:
666      # Reuse the extension policy request, to trigger the same signature
667      # type in the response.
668      request.settings_entity_id = settings_entity_id
669      fetch_response = response.response.add()
670      self.ProcessCloudPolicy(request, token_info, fetch_response, username)
671      # Don't do key rotations for these messages.
672      fetch_response.ClearField('new_public_key')
673      fetch_response.ClearField('new_public_key_signature')
674      fetch_response.ClearField('new_public_key_verification_signature')
675
676  def ProcessCloudPolicy(self, msg, token_info, response, username=None):
677    """Handles a cloud policy request. (New protocol for policy requests.)
678
679    Encodes the policy into protobuf representation, signs it and constructs
680    the response.
681
682    Args:
683      msg: The CloudPolicyRequest message received from the client.
684      token_info: The token extracted from the request.
685      response: A PolicyFetchResponse message that should be filled with the
686                response data.
687      username: The username for the response. May be None.
688    """
689
690    if msg.machine_id:
691      self.server.UpdateMachineId(token_info['device_token'], msg.machine_id)
692
693    # Response is only given if the scope is specified in the config file.
694    # Normally 'google/chromeos/device', 'google/chromeos/user' and
695    # 'google/chromeos/publicaccount' should be accepted.
696    policy = self.server.GetPolicies()
697    policy_value = ''
698    policy_key = msg.policy_type
699    if msg.settings_entity_id:
700      policy_key += '/' + msg.settings_entity_id
701    if msg.policy_type in token_info['allowed_policy_types']:
702      if msg.policy_type in ('google/android/user',
703                             'google/chromeos/publicaccount',
704                             'google/chromeos/user',
705                             'google/chrome/user',
706                             'google/ios/user'):
707        settings = cp.CloudPolicySettings()
708        payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
709        if payload is None:
710          self.GatherUserPolicySettings(settings, policy.get(policy_key, {}))
711          payload = settings.SerializeToString()
712      elif dp is not None and msg.policy_type == 'google/chromeos/device':
713        settings = dp.ChromeDeviceSettingsProto()
714        payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
715        if payload is None:
716          self.GatherDevicePolicySettings(settings, policy.get(policy_key, {}))
717          payload = settings.SerializeToString()
718      elif msg.policy_type == 'google/chrome/extension':
719        settings = ep.ExternalPolicyData()
720        payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
721        if payload is None:
722          payload = self.CreatePolicyForExternalPolicyData(policy_key)
723      else:
724        response.error_code = 400
725        response.error_message = 'Invalid policy type'
726        return
727    else:
728      response.error_code = 400
729      response.error_message = 'Request not allowed for the token used'
730      return
731
732    # Sign with 'current_key_index', defaulting to key 0.
733    signing_key = None
734    req_key = None
735    current_key_index = policy.get('current_key_index', 0)
736    nkeys = len(self.server.keys)
737    if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and
738        current_key_index in range(nkeys)):
739      signing_key = self.server.keys[current_key_index]
740      if msg.public_key_version in range(1, nkeys + 1):
741        # requested key exists, use for signing and rotate.
742        req_key = self.server.keys[msg.public_key_version - 1]['private_key']
743
744    # Fill the policy data protobuf.
745    policy_data = dm.PolicyData()
746    policy_data.policy_type = msg.policy_type
747    policy_data.timestamp = int(time.time() * 1000)
748    policy_data.request_token = token_info['device_token']
749    policy_data.policy_value = payload
750    policy_data.machine_name = token_info['machine_name']
751    policy_data.valid_serial_number_missing = (
752        token_info['machine_id'] in BAD_MACHINE_IDS)
753    policy_data.settings_entity_id = msg.settings_entity_id
754    policy_data.service_account_identity = policy.get(
755        'service_account_identity',
756        'policy_testserver.py-service_account_identity')
757    invalidation_source = policy.get('invalidation_source')
758    if invalidation_source is not None:
759      policy_data.invalidation_source = invalidation_source
760    # Since invalidation_name is type bytes in the proto, the Unicode name
761    # provided needs to be encoded as ASCII to set the correct byte pattern.
762    invalidation_name = policy.get('invalidation_name')
763    if invalidation_name is not None:
764      policy_data.invalidation_name = invalidation_name.encode('ascii')
765
766    if signing_key:
767      policy_data.public_key_version = current_key_index + 1
768
769    if username:
770      policy_data.username = username
771    else:
772      # For regular user/device policy, there is no way for the testserver to
773      # know the user name belonging to the GAIA auth token we received (short
774      # of actually talking to GAIA). To address this, we read the username from
775      # the policy configuration dictionary, or use a default.
776      policy_data.username = policy.get('policy_user', 'user@example.com')
777    policy_data.device_id = token_info['device_id']
778    signed_data = policy_data.SerializeToString()
779
780    response.policy_data = signed_data
781    if signing_key:
782      response.policy_data_signature = (
783          bytes(signing_key['private_key'].hashAndSign(signed_data)))
784      if msg.public_key_version != current_key_index + 1:
785        response.new_public_key = signing_key['public_key']
786
787        # Set the verification signature appropriate for the policy domain.
788        # TODO(atwilson): Use the enrollment domain for public accounts when
789        # we add key validation for ChromeOS (http://crbug.com/328038).
790        if 'signatures' in signing_key:
791          verification_sig = self.GetSignatureForDomain(
792              signing_key['signatures'], policy_data.username)
793
794          if verification_sig:
795            assert len(verification_sig) == 256, \
796                'bad signature size: %d' % len(verification_sig)
797            response.new_public_key_verification_signature = verification_sig
798
799        if req_key:
800          response.new_public_key_signature = (
801              bytes(req_key.hashAndSign(response.new_public_key)))
802
803    return (200, response.SerializeToString())
804
805  def GetSignatureForDomain(self, signatures, username):
806    parsed_username = username.split("@", 1)
807    if len(parsed_username) != 2:
808      logging.error('Could not extract domain from username: %s' % username)
809      return None
810    domain = parsed_username[1]
811
812    # Lookup the domain's signature in the passed dictionary. If none is found,
813    # fallback to a wildcard signature.
814    if domain in signatures:
815      return signatures[domain]
816    if '*' in signatures:
817      return signatures['*']
818
819    # No key matching this domain.
820    logging.error('No verification signature matching domain: %s' % domain)
821    return None
822
823  def CheckToken(self):
824    """Helper for checking whether the client supplied a valid DM token.
825
826    Extracts the token from the request and passed to the server in order to
827    look up the client.
828
829    Returns:
830      A pair of token information record and error response. If the first
831      element is None, then the second contains an error code to send back to
832      the client. Otherwise the first element is the same structure that is
833      returned by LookupToken().
834    """
835    error = 500
836    dmtoken = None
837    request_device_id = self.GetUniqueParam('deviceid')
838    match = re.match('GoogleDMToken token=(\\w+)',
839                     self.headers.getheader('Authorization', ''))
840    if match:
841      dmtoken = match.group(1)
842    if not dmtoken:
843      error = 401
844    else:
845      token_info = self.server.LookupToken(dmtoken)
846      if (not token_info or
847          not request_device_id or
848          token_info['device_id'] != request_device_id):
849        error = 410
850      else:
851        return (token_info, None)
852
853    logging.debug('Token check failed with error %d' % error)
854
855    return (None, (error, 'Server error %d' % error))
856
857  def DumpMessage(self, label, msg):
858    """Helper for logging an ASCII dump of a protobuf message."""
859    logging.debug('%s\n%s' % (label, str(msg)))
860
861
862class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn,
863                       testserver_base.StoppableHTTPServer):
864  """Handles requests and keeps global service state."""
865
866  def __init__(self, server_address, data_dir, policy_path, client_state_file,
867               private_key_paths, server_base_url):
868    """Initializes the server.
869
870    Args:
871      server_address: Server host and port.
872      policy_path: Names the file to read JSON-formatted policy from.
873      private_key_paths: List of paths to read private keys from.
874    """
875    testserver_base.StoppableHTTPServer.__init__(self, server_address,
876                                                 PolicyRequestHandler)
877    self._registered_tokens = {}
878    self.data_dir = data_dir
879    self.policy_path = policy_path
880    self.client_state_file = client_state_file
881    self.server_base_url = server_base_url
882
883    self.keys = []
884    if private_key_paths:
885      # Load specified keys from the filesystem.
886      for key_path in private_key_paths:
887        try:
888          key_str = open(key_path).read()
889        except IOError:
890          print 'Failed to load private key from %s' % key_path
891          continue
892        try:
893          key = tlslite.api.parsePEMKey(key_str, private=True)
894        except SyntaxError:
895          key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
896              bytearray(key_str))
897
898        assert key is not None
899        key_info = { 'private_key' : key }
900
901        # Now try to read in a signature, if one exists.
902        try:
903          key_sig = open(key_path + '.sig').read()
904          # Create a dictionary with the wildcard domain + signature
905          key_info['signatures'] = {'*': key_sig}
906        except IOError:
907          print 'Failed to read validation signature from %s.sig' % key_path
908        self.keys.append(key_info)
909    else:
910      # Use the canned private keys if none were passed from the command line.
911      for signing_key in SIGNING_KEYS:
912        decoded_key = base64.b64decode(signing_key['key']);
913        key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
914            bytearray(decoded_key))
915        assert key is not None
916        # Grab the signature dictionary for this key and decode all of the
917        # signatures.
918        signature_dict = signing_key['signatures']
919        decoded_signatures = {}
920        for domain in signature_dict:
921          decoded_signatures[domain] = base64.b64decode(signature_dict[domain])
922        self.keys.append({'private_key': key,
923                          'signatures': decoded_signatures})
924
925    # Derive the public keys from the private keys.
926    for entry in self.keys:
927      key = entry['private_key']
928
929      algorithm = asn1der.Sequence(
930          [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
931            asn1der.Data(asn1der.NULL, '') ])
932      rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
933                                      asn1der.Integer(key.e) ])
934      pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
935      entry['public_key'] = pubkey
936
937    # Load client state.
938    if self.client_state_file is not None:
939      try:
940        file_contents = open(self.client_state_file).read()
941        self._registered_tokens = json.loads(file_contents, strict=False)
942      except IOError:
943        pass
944
945  def GetPolicies(self):
946    """Returns the policies to be used, reloaded form the backend file every
947       time this is called.
948    """
949    policy = {}
950    if json is None:
951      print 'No JSON module, cannot parse policy information'
952    else :
953      try:
954        policy = json.loads(open(self.policy_path).read(), strict=False)
955      except IOError:
956        print 'Failed to load policy from %s' % self.policy_path
957    return policy
958
959  def RegisterDevice(self, device_id, machine_id, type):
960    """Registers a device or user and generates a DM token for it.
961
962    Args:
963      device_id: The device identifier provided by the client.
964
965    Returns:
966      The newly generated device token for the device.
967    """
968    dmtoken_chars = []
969    while len(dmtoken_chars) < 32:
970      dmtoken_chars.append(random.choice('0123456789abcdef'))
971    dmtoken = ''.join(dmtoken_chars)
972    allowed_policy_types = {
973      dm.DeviceRegisterRequest.BROWSER: [
974          'google/chrome/user',
975          'google/chrome/extension'
976      ],
977      dm.DeviceRegisterRequest.USER: [
978          'google/chromeos/user',
979          'google/chrome/extension'
980      ],
981      dm.DeviceRegisterRequest.DEVICE: [
982          'google/chromeos/device',
983          'google/chromeos/publicaccount',
984          'google/chrome/extension'
985      ],
986      dm.DeviceRegisterRequest.ANDROID_BROWSER: [
987          'google/android/user'
988      ],
989      dm.DeviceRegisterRequest.IOS_BROWSER: [
990          'google/ios/user'
991      ],
992      dm.DeviceRegisterRequest.TT: ['google/chromeos/user',
993                                    'google/chrome/user'],
994    }
995    if machine_id in KIOSK_MACHINE_IDS:
996      enrollment_mode = dm.DeviceRegisterResponse.RETAIL
997    else:
998      enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE
999    self._registered_tokens[dmtoken] = {
1000      'device_id': device_id,
1001      'device_token': dmtoken,
1002      'allowed_policy_types': allowed_policy_types[type],
1003      'machine_name': 'chromeos-' + machine_id,
1004      'machine_id': machine_id,
1005      'enrollment_mode': enrollment_mode,
1006    }
1007    self.WriteClientState()
1008    return self._registered_tokens[dmtoken]
1009
1010  def UpdateMachineId(self, dmtoken, machine_id):
1011    """Updates the machine identifier for a registered device.
1012
1013    Args:
1014      dmtoken: The device management token provided by the client.
1015      machine_id: Updated hardware identifier value.
1016    """
1017    if dmtoken in self._registered_tokens:
1018      self._registered_tokens[dmtoken]['machine_id'] = machine_id
1019      self.WriteClientState()
1020
1021  def UpdateStateKeys(self, dmtoken, state_keys):
1022    """Updates the state keys for a given client.
1023
1024    Args:
1025      dmtoken: The device management token provided by the client.
1026      state_keys: The state keys to set.
1027    """
1028    if dmtoken in self._registered_tokens:
1029      self._registered_tokens[dmtoken]['state_keys'] = map(
1030          lambda key : key.encode('hex'), state_keys)
1031      self.WriteClientState()
1032
1033  def LookupToken(self, dmtoken):
1034    """Looks up a device or a user by DM token.
1035
1036    Args:
1037      dmtoken: The device management token provided by the client.
1038
1039    Returns:
1040      A dictionary with information about a device or user that is registered by
1041      dmtoken, or None if the token is not found.
1042    """
1043    return self._registered_tokens.get(dmtoken, None)
1044
1045  def LookupByStateKey(self, state_key):
1046    """Looks up a device or a user by a state key.
1047
1048    Args:
1049      state_key: The state key provided by the client.
1050
1051    Returns:
1052      A dictionary with information about a device or user or None if there is
1053      no matching record.
1054    """
1055    for client in self._registered_tokens.values():
1056      if state_key.encode('hex') in client.get('state_keys', []):
1057        return client
1058
1059    return None
1060
1061  def GetMatchingStateKeyHashes(self, modulus, remainder):
1062    """Returns all clients registered with the server.
1063
1064    Returns:
1065      The list of registered clients.
1066    """
1067    state_keys = sum([ c.get('state_keys', [])
1068                       for c in self._registered_tokens.values() ], [])
1069    hashed_keys = map(lambda key: hashlib.sha256(key.decode('hex')).digest(),
1070                      set(state_keys))
1071    return filter(
1072        lambda hash : int(hash.encode('hex'), 16) % modulus == remainder,
1073        hashed_keys)
1074
1075  def UnregisterDevice(self, dmtoken):
1076    """Unregisters a device identified by the given DM token.
1077
1078    Args:
1079      dmtoken: The device management token provided by the client.
1080    """
1081    if dmtoken in self._registered_tokens.keys():
1082      del self._registered_tokens[dmtoken]
1083      self.WriteClientState()
1084
1085  def WriteClientState(self):
1086    """Writes the client state back to the file."""
1087    if self.client_state_file is not None:
1088      json_data = json.dumps(self._registered_tokens)
1089      open(self.client_state_file, 'w').write(json_data)
1090
1091  def GetBaseFilename(self, policy_selector):
1092    """Returns the base filename for the given policy_selector.
1093
1094    Args:
1095      policy_selector: The policy type and settings entity id, joined by '/'.
1096
1097    Returns:
1098      The filename corresponding to the policy_selector, without a file
1099      extension.
1100    """
1101    sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector)
1102    return os.path.join(self.data_dir or '',
1103                        'policy_%s' % sanitized_policy_selector)
1104
1105  def ListMatchingComponents(self, policy_type):
1106    """Returns a list of settings entity IDs that have a configuration file.
1107
1108    Args:
1109      policy_type: The policy type to look for. Only settings entity IDs for
1110      file selectors That match this policy_type will be returned.
1111
1112    Returns:
1113      A list of settings entity IDs for the given |policy_type| that have a
1114      configuration file in this server (either as a .bin, .txt or .data file).
1115    """
1116    base_name = self.GetBaseFilename(policy_type)
1117    files = glob.glob('%s_*.*' % base_name)
1118    len_base_name = len(base_name) + 1
1119    return [ file[len_base_name:file.rfind('.')] for file in files ]
1120
1121  def ReadPolicyFromDataDir(self, policy_selector, proto_message):
1122    """Tries to read policy payload from a file in the data directory.
1123
1124    First checks for a binary rendition of the policy protobuf in
1125    <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns
1126    it. If that file doesn't exist, tries
1127    <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a
1128    protobuf using proto_message. If that fails as well, returns None.
1129
1130    Args:
1131      policy_selector: Selects which policy to read.
1132      proto_message: Optional protobuf message object used for decoding the
1133          proto text format.
1134
1135    Returns:
1136      The binary payload message, or None if not found.
1137    """
1138    base_filename = self.GetBaseFilename(policy_selector)
1139
1140    # Try the binary payload file first.
1141    try:
1142      return open(base_filename + '.bin').read()
1143    except IOError:
1144      pass
1145
1146    # If that fails, try the text version instead.
1147    if proto_message is None:
1148      return None
1149
1150    try:
1151      text = open(base_filename + '.txt').read()
1152      google.protobuf.text_format.Merge(text, proto_message)
1153      return proto_message.SerializeToString()
1154    except IOError:
1155      return None
1156    except google.protobuf.text_format.ParseError:
1157      return None
1158
1159  def ReadPolicyDataFromDataDir(self, policy_selector):
1160    """Returns the external policy data for |policy_selector| if found.
1161
1162    Args:
1163      policy_selector: Selects which policy to read.
1164
1165    Returns:
1166      The data for the corresponding policy type and entity id, if found.
1167    """
1168    base_filename = self.GetBaseFilename(policy_selector)
1169    try:
1170      return open(base_filename + '.data').read()
1171    except IOError:
1172      return None
1173
1174  def GetBaseURL(self):
1175    """Returns the server base URL.
1176
1177    Respects the |server_base_url| configuration parameter, if present. Falls
1178    back to construct the URL from the server hostname and port otherwise.
1179
1180    Returns:
1181      The URL to use for constructing URLs that get returned to clients.
1182    """
1183    base_url = self.server_base_url
1184    if base_url is None:
1185      base_url = 'http://%s:%s' % self.server_address[:2]
1186
1187    return base_url
1188
1189
1190class PolicyServerRunner(testserver_base.TestServerRunner):
1191
1192  def __init__(self):
1193    super(PolicyServerRunner, self).__init__()
1194
1195  def create_server(self, server_data):
1196    data_dir = self.options.data_dir or ''
1197    config_file = (self.options.config_file or
1198                   os.path.join(data_dir, 'device_management'))
1199    server = PolicyTestServer((self.options.host, self.options.port),
1200                              data_dir, config_file,
1201                              self.options.client_state_file,
1202                              self.options.policy_keys,
1203                              self.options.server_base_url)
1204    server_data['port'] = server.server_port
1205    return server
1206
1207  def add_options(self):
1208    testserver_base.TestServerRunner.add_options(self)
1209    self.option_parser.add_option('--client-state', dest='client_state_file',
1210                                  help='File that client state should be '
1211                                  'persisted to. This allows the server to be '
1212                                  'seeded by a list of pre-registered clients '
1213                                  'and restarts without abandoning registered '
1214                                  'clients.')
1215    self.option_parser.add_option('--policy-key', action='append',
1216                                  dest='policy_keys',
1217                                  help='Specify a path to a PEM-encoded '
1218                                  'private key to use for policy signing. May '
1219                                  'be specified multiple times in order to '
1220                                  'load multiple keys into the server. If the '
1221                                  'server has multiple keys, it will rotate '
1222                                  'through them in at each request in a '
1223                                  'round-robin fashion. The server will '
1224                                  'use a canned key if none is specified '
1225                                  'on the command line. The test server will '
1226                                  'also look for a verification signature file '
1227                                  'in the same location: <filename>.sig and if '
1228                                  'present will add the signature to the '
1229                                  'policy blob as appropriate via the '
1230                                  'new_public_key_verification_signature '
1231                                  'field.')
1232    self.option_parser.add_option('--log-level', dest='log_level',
1233                                  default='WARN',
1234                                  help='Log level threshold to use.')
1235    self.option_parser.add_option('--config-file', dest='config_file',
1236                                  help='Specify a configuration file to use '
1237                                  'instead of the default '
1238                                  '<data_dir>/device_management')
1239    self.option_parser.add_option('--server-base-url', dest='server_base_url',
1240                                  help='The server base URL to use when '
1241                                  'constructing URLs to return to the client.')
1242
1243  def run_server(self):
1244    logger = logging.getLogger()
1245    logger.setLevel(getattr(logging, str(self.options.log_level).upper()))
1246    if (self.options.log_to_console):
1247      logger.addHandler(logging.StreamHandler())
1248    if (self.options.log_file):
1249      logger.addHandler(logging.FileHandler(self.options.log_file))
1250
1251    testserver_base.TestServerRunner.run_server(self)
1252
1253
1254if __name__ == '__main__':
1255  sys.exit(PolicyServerRunner().main())
1256