Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / policy / test / policy_testserver.py
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
7 This implements a simple cloud policy test server that can be used to test
8 chrome's device management service client. The policy information is read from
9 the file named device_management in the server's data directory. It contains
10 enforced and recommended policies for the device and user scope, and a list
11 of managed users.
12
13 The format of the file is JSON. The root dictionary contains a list under the
14 key "managed_users". It contains auth tokens for which the server will claim
15 that the user is managed. The token string "*" indicates that all users are
16 claimed to be managed. Other keys in the root dictionary identify request
17 scopes. The user-request scope is described by a dictionary that holds two
18 sub-dictionaries: "mandatory" and "recommended". Both these hold the policy
19 definitions as key/value stores, their format is identical to what the Linux
20 implementation reads from /etc.
21 The device-scope holds the policy-definition directly as key/value stores in the
22 protobuf-format.
23
24 Example:
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
57 import base64
58 import BaseHTTPServer
59 import cgi
60 import glob
61 import google.protobuf.text_format
62 import hashlib
63 import logging
64 import os
65 import random
66 import re
67 import sys
68 import time
69 import tlslite
70 import tlslite.api
71 import tlslite.utils
72 import tlslite.utils.cryptomath
73 import urlparse
74
75 # The name and availability of the json module varies in python versions.
76 try:
77   import simplejson as json
78 except ImportError:
79   try:
80     import json
81   except ImportError:
82     json = None
83
84 import asn1der
85 import testserver_base
86
87 import device_management_backend_pb2 as dm
88 import cloud_policy_pb2 as cp
89 import chrome_extension_policy_pb2 as ep
90
91 # Device policy is only available on Chrome OS builds.
92 try:
93   import chrome_device_policy_pb2 as dp
94 except ImportError:
95   dp = None
96
97 # ASN.1 object identifier for PKCS#1/RSA.
98 PKCS1_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.
102 BAD_MACHINE_IDS = [ '123490EN400015' ]
103
104 # List of machines that trigger the server to send kiosk enrollment response
105 # for the register request.
106 KIOSK_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 # }
118 SIGNING_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
188 class 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     response = dm.DeviceManagementResponse()
447     for request in msg.policy_request.request:
448       if (request.policy_type in
449              ('google/android/user',
450               'google/chromeos/device',
451               'google/chromeos/publicaccount',
452               'google/chromeos/user',
453               'google/chrome/user',
454               'google/ios/user')):
455         fetch_response = response.policy_response.response.add()
456         self.ProcessCloudPolicy(request, token_info, fetch_response)
457       elif request.policy_type == 'google/chrome/extension':
458         self.ProcessCloudPolicyForExtensions(
459             request, response.policy_response, token_info)
460       else:
461         fetch_response.error_code = 400
462         fetch_response.error_message = 'Invalid policy_type'
463
464     return (200, response)
465
466   def ProcessAutoEnrollment(self, msg):
467     """Handles an auto-enrollment check request.
468
469     The reply depends on the value of the modulus:
470       1: replies with no new modulus and the sha256 hash of "0"
471       2: replies with a new modulus, 4.
472       4: replies with a new modulus, 2.
473       8: fails with error 400.
474       16: replies with a new modulus, 16.
475       32: replies with a new modulus, 1.
476       anything else: replies with no new modulus and an empty list of hashes
477
478     These allow the client to pick the testing scenario its wants to simulate.
479
480     Args:
481       msg: The DeviceAutoEnrollmentRequest message received from the client.
482
483     Returns:
484       A tuple of HTTP status code and response data to send to the client.
485     """
486     auto_enrollment_response = dm.DeviceAutoEnrollmentResponse()
487
488     if msg.modulus == 1:
489       auto_enrollment_response.hash.extend(
490           self.server.GetMatchingStateKeyHashes(msg.modulus, msg.remainder))
491     elif msg.modulus == 2:
492       auto_enrollment_response.expected_modulus = 4
493     elif msg.modulus == 4:
494       auto_enrollment_response.expected_modulus = 2
495     elif msg.modulus == 8:
496       return (400, 'Server error')
497     elif msg.modulus == 16:
498       auto_enrollment_response.expected_modulus = 16
499     elif msg.modulus == 32:
500       auto_enrollment_response.expected_modulus = 1
501
502     response = dm.DeviceManagementResponse()
503     response.auto_enrollment_response.CopyFrom(auto_enrollment_response)
504     return (200, response)
505
506   def ProcessDeviceStateRetrievalRequest(self, msg):
507     """Handles a device state retrieval request.
508
509     Response data is taken from server configuration.
510
511     Returns:
512       A tuple of HTTP status code and response data to send to the client.
513     """
514     device_state_retrieval_response = dm.DeviceStateRetrievalResponse()
515
516     client = self.server.LookupByStateKey(msg.server_backed_state_key)
517     if client is not None:
518       state = self.server.GetPolicies().get('device_state', {})
519       FIELDS = [
520           'management_domain',
521           'restore_mode',
522       ]
523       for field in FIELDS:
524         if field in state:
525           setattr(device_state_retrieval_response, field, state[field])
526
527     response = dm.DeviceManagementResponse()
528     response.device_state_retrieval_response.CopyFrom(
529         device_state_retrieval_response)
530     return (200, response)
531
532   def SetProtobufMessageField(self, group_message, field, field_value):
533     """Sets a field in a protobuf message.
534
535     Args:
536       group_message: The protobuf message.
537       field: The field of the message to set, it should be a member of
538           group_message.DESCRIPTOR.fields.
539       field_value: The value to set.
540     """
541     if field.label == field.LABEL_REPEATED:
542       assert type(field_value) == list
543       entries = group_message.__getattribute__(field.name)
544       if field.message_type is None:
545         for list_item in field_value:
546           entries.append(list_item)
547       else:
548         # This field is itself a protobuf.
549         sub_type = field.message_type
550         for sub_value in field_value:
551           assert type(sub_value) == dict
552           # Add a new sub-protobuf per list entry.
553           sub_message = entries.add()
554           # Now iterate over its fields and recursively add them.
555           for sub_field in sub_message.DESCRIPTOR.fields:
556             if sub_field.name in sub_value:
557               value = sub_value[sub_field.name]
558               self.SetProtobufMessageField(sub_message, sub_field, value)
559       return
560     elif field.type == field.TYPE_BOOL:
561       assert type(field_value) == bool
562     elif field.type == field.TYPE_STRING:
563       assert type(field_value) == str or type(field_value) == unicode
564     elif field.type == field.TYPE_INT64:
565       assert type(field_value) == int
566     elif (field.type == field.TYPE_MESSAGE and
567           field.message_type.name == 'StringList'):
568       assert type(field_value) == list
569       entries = group_message.__getattribute__(field.name).entries
570       for list_item in field_value:
571         entries.append(list_item)
572       return
573     else:
574       raise Exception('Unknown field type %s' % field.type)
575     group_message.__setattr__(field.name, field_value)
576
577   def GatherDevicePolicySettings(self, settings, policies):
578     """Copies all the policies from a dictionary into a protobuf of type
579     CloudDeviceSettingsProto.
580
581     Args:
582       settings: The destination ChromeDeviceSettingsProto protobuf.
583       policies: The source dictionary containing policies in JSON format.
584     """
585     for group in settings.DESCRIPTOR.fields:
586       # Create protobuf message for group.
587       group_message = eval('dp.' + group.message_type.name + '()')
588       # Indicates if at least one field was set in |group_message|.
589       got_fields = False
590       # Iterate over fields of the message and feed them from the
591       # policy config file.
592       for field in group_message.DESCRIPTOR.fields:
593         field_value = None
594         if field.name in policies:
595           got_fields = True
596           field_value = policies[field.name]
597           self.SetProtobufMessageField(group_message, field, field_value)
598       if got_fields:
599         settings.__getattribute__(group.name).CopyFrom(group_message)
600
601   def GatherUserPolicySettings(self, settings, policies):
602     """Copies all the policies from a dictionary into a protobuf of type
603     CloudPolicySettings.
604
605     Args:
606       settings: The destination: a CloudPolicySettings protobuf.
607       policies: The source: a dictionary containing policies under keys
608           'recommended' and 'mandatory'.
609     """
610     for field in settings.DESCRIPTOR.fields:
611       # |field| is the entry for a specific policy in the top-level
612       # CloudPolicySettings proto.
613
614       # Look for this policy's value in the mandatory or recommended dicts.
615       if field.name in policies.get('mandatory', {}):
616         mode = cp.PolicyOptions.MANDATORY
617         value = policies['mandatory'][field.name]
618       elif field.name in policies.get('recommended', {}):
619         mode = cp.PolicyOptions.RECOMMENDED
620         value = policies['recommended'][field.name]
621       else:
622         continue
623
624       # Create protobuf message for this policy.
625       policy_message = eval('cp.' + field.message_type.name + '()')
626       policy_message.policy_options.mode = mode
627       field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value']
628       self.SetProtobufMessageField(policy_message, field_descriptor, value)
629       settings.__getattribute__(field.name).CopyFrom(policy_message)
630
631   def ProcessCloudPolicyForExtensions(self, request, response, token_info):
632     """Handles a request for policy for extensions.
633
634     A request for policy for extensions is slightly different from the other
635     cloud policy requests, because it can trigger 0, one or many
636     PolicyFetchResponse messages in the response.
637
638     Args:
639       request: The PolicyFetchRequest that triggered this handler.
640       response: The DevicePolicyResponse message for the response. Multiple
641       PolicyFetchResponses will be appended to this message.
642       token_info: The token extracted from the request.
643     """
644     # Send one PolicyFetchResponse for each extension that has
645     # configuration data at the server.
646     ids = self.server.ListMatchingComponents('google/chrome/extension')
647     for settings_entity_id in ids:
648       # Reuse the extension policy request, to trigger the same signature
649       # type in the response.
650       request.settings_entity_id = settings_entity_id
651       fetch_response = response.response.add()
652       self.ProcessCloudPolicy(request, token_info, fetch_response)
653       # Don't do key rotations for these messages.
654       fetch_response.ClearField('new_public_key')
655       fetch_response.ClearField('new_public_key_signature')
656       fetch_response.ClearField('new_public_key_verification_signature')
657
658   def ProcessCloudPolicy(self, msg, token_info, response):
659     """Handles a cloud policy request. (New protocol for policy requests.)
660
661     Encodes the policy into protobuf representation, signs it and constructs
662     the response.
663
664     Args:
665       msg: The CloudPolicyRequest message received from the client.
666       token_info: The token extracted from the request.
667       response: A PolicyFetchResponse message that should be filled with the
668                 response data.
669     """
670
671     if msg.machine_id:
672       self.server.UpdateMachineId(token_info['device_token'], msg.machine_id)
673
674     # Response is only given if the scope is specified in the config file.
675     # Normally 'google/chromeos/device', 'google/chromeos/user' and
676     # 'google/chromeos/publicaccount' should be accepted.
677     policy = self.server.GetPolicies()
678     policy_value = ''
679     policy_key = msg.policy_type
680     if msg.settings_entity_id:
681       policy_key += '/' + msg.settings_entity_id
682     if msg.policy_type in token_info['allowed_policy_types']:
683       if msg.policy_type in ('google/android/user',
684                              'google/chromeos/publicaccount',
685                              'google/chromeos/user',
686                              'google/chrome/user',
687                              'google/ios/user'):
688         settings = cp.CloudPolicySettings()
689         payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
690         if payload is None:
691           self.GatherUserPolicySettings(settings, policy.get(policy_key, {}))
692           payload = settings.SerializeToString()
693       elif dp is not None and msg.policy_type == 'google/chromeos/device':
694         settings = dp.ChromeDeviceSettingsProto()
695         payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
696         if payload is None:
697           self.GatherDevicePolicySettings(settings, policy.get(policy_key, {}))
698           payload = settings.SerializeToString()
699       elif msg.policy_type == 'google/chrome/extension':
700         settings = ep.ExternalPolicyData()
701         payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
702         if payload is None:
703           payload = self.CreatePolicyForExternalPolicyData(policy_key)
704       else:
705         response.error_code = 400
706         response.error_message = 'Invalid policy type'
707         return
708     else:
709       response.error_code = 400
710       response.error_message = 'Request not allowed for the token used'
711       return
712
713     # Sign with 'current_key_index', defaulting to key 0.
714     signing_key = None
715     req_key = None
716     current_key_index = policy.get('current_key_index', 0)
717     nkeys = len(self.server.keys)
718     if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and
719         current_key_index in range(nkeys)):
720       signing_key = self.server.keys[current_key_index]
721       if msg.public_key_version in range(1, nkeys + 1):
722         # requested key exists, use for signing and rotate.
723         req_key = self.server.keys[msg.public_key_version - 1]['private_key']
724
725     # Fill the policy data protobuf.
726     policy_data = dm.PolicyData()
727     policy_data.policy_type = msg.policy_type
728     policy_data.timestamp = int(time.time() * 1000)
729     policy_data.request_token = token_info['device_token']
730     policy_data.policy_value = payload
731     policy_data.machine_name = token_info['machine_name']
732     policy_data.valid_serial_number_missing = (
733         token_info['machine_id'] in BAD_MACHINE_IDS)
734     policy_data.settings_entity_id = msg.settings_entity_id
735     policy_data.service_account_identity = policy.get(
736         'service_account_identity',
737         'policy_testserver.py-service_account_identity')
738     invalidation_source = policy.get('invalidation_source')
739     if invalidation_source is not None:
740       policy_data.invalidation_source = invalidation_source
741     # Since invalidation_name is type bytes in the proto, the Unicode name
742     # provided needs to be encoded as ASCII to set the correct byte pattern.
743     invalidation_name = policy.get('invalidation_name')
744     if invalidation_name is not None:
745       policy_data.invalidation_name = invalidation_name.encode('ascii')
746
747     if signing_key:
748       policy_data.public_key_version = current_key_index + 1
749     if msg.policy_type == 'google/chromeos/publicaccount':
750       policy_data.username = msg.settings_entity_id
751     else:
752       # For regular user/device policy, there is no way for the testserver to
753       # know the user name belonging to the GAIA auth token we received (short
754       # of actually talking to GAIA). To address this, we read the username from
755       # the policy configuration dictionary, or use a default.
756       policy_data.username = policy.get('policy_user', 'user@example.com')
757     policy_data.device_id = token_info['device_id']
758     signed_data = policy_data.SerializeToString()
759
760     response.policy_data = signed_data
761     if signing_key:
762       response.policy_data_signature = (
763           bytes(signing_key['private_key'].hashAndSign(signed_data)))
764       if msg.public_key_version != current_key_index + 1:
765         response.new_public_key = signing_key['public_key']
766
767         # Set the verification signature appropriate for the policy domain.
768         # TODO(atwilson): Use the enrollment domain for public accounts when
769         # we add key validation for ChromeOS (http://crbug.com/328038).
770         if 'signatures' in signing_key:
771           verification_sig = self.GetSignatureForDomain(
772               signing_key['signatures'], policy_data.username)
773
774           if verification_sig:
775             assert len(verification_sig) == 256, \
776                 'bad signature size: %d' % len(verification_sig)
777             response.new_public_key_verification_signature = verification_sig
778
779         if req_key:
780           response.new_public_key_signature = (
781               bytes(req_key.hashAndSign(response.new_public_key)))
782
783     return (200, response.SerializeToString())
784
785   def GetSignatureForDomain(self, signatures, username):
786     parsed_username = username.split("@", 1)
787     if len(parsed_username) != 2:
788       logging.error('Could not extract domain from username: %s' % username)
789       return None
790     domain = parsed_username[1]
791
792     # Lookup the domain's signature in the passed dictionary. If none is found,
793     # fallback to a wildcard signature.
794     if domain in signatures:
795       return signatures[domain]
796     if '*' in signatures:
797       return signatures['*']
798
799     # No key matching this domain.
800     logging.error('No verification signature matching domain: %s' % domain)
801     return None
802
803   def CheckToken(self):
804     """Helper for checking whether the client supplied a valid DM token.
805
806     Extracts the token from the request and passed to the server in order to
807     look up the client.
808
809     Returns:
810       A pair of token information record and error response. If the first
811       element is None, then the second contains an error code to send back to
812       the client. Otherwise the first element is the same structure that is
813       returned by LookupToken().
814     """
815     error = 500
816     dmtoken = None
817     request_device_id = self.GetUniqueParam('deviceid')
818     match = re.match('GoogleDMToken token=(\\w+)',
819                      self.headers.getheader('Authorization', ''))
820     if match:
821       dmtoken = match.group(1)
822     if not dmtoken:
823       error = 401
824     else:
825       token_info = self.server.LookupToken(dmtoken)
826       if (not token_info or
827           not request_device_id or
828           token_info['device_id'] != request_device_id):
829         error = 410
830       else:
831         return (token_info, None)
832
833     logging.debug('Token check failed with error %d' % error)
834
835     return (None, (error, 'Server error %d' % error))
836
837   def DumpMessage(self, label, msg):
838     """Helper for logging an ASCII dump of a protobuf message."""
839     logging.debug('%s\n%s' % (label, str(msg)))
840
841
842 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn,
843                        testserver_base.StoppableHTTPServer):
844   """Handles requests and keeps global service state."""
845
846   def __init__(self, server_address, data_dir, policy_path, client_state_file,
847                private_key_paths, server_base_url):
848     """Initializes the server.
849
850     Args:
851       server_address: Server host and port.
852       policy_path: Names the file to read JSON-formatted policy from.
853       private_key_paths: List of paths to read private keys from.
854     """
855     testserver_base.StoppableHTTPServer.__init__(self, server_address,
856                                                  PolicyRequestHandler)
857     self._registered_tokens = {}
858     self.data_dir = data_dir
859     self.policy_path = policy_path
860     self.client_state_file = client_state_file
861     self.server_base_url = server_base_url
862
863     self.keys = []
864     if private_key_paths:
865       # Load specified keys from the filesystem.
866       for key_path in private_key_paths:
867         try:
868           key_str = open(key_path).read()
869         except IOError:
870           print 'Failed to load private key from %s' % key_path
871           continue
872         try:
873           key = tlslite.api.parsePEMKey(key_str, private=True)
874         except SyntaxError:
875           key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
876               bytearray(key_str))
877
878         assert key is not None
879         key_info = { 'private_key' : key }
880
881         # Now try to read in a signature, if one exists.
882         try:
883           key_sig = open(key_path + '.sig').read()
884           # Create a dictionary with the wildcard domain + signature
885           key_info['signatures'] = {'*': key_sig}
886         except IOError:
887           print 'Failed to read validation signature from %s.sig' % key_path
888         self.keys.append(key_info)
889     else:
890       # Use the canned private keys if none were passed from the command line.
891       for signing_key in SIGNING_KEYS:
892         decoded_key = base64.b64decode(signing_key['key']);
893         key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
894             bytearray(decoded_key))
895         assert key is not None
896         # Grab the signature dictionary for this key and decode all of the
897         # signatures.
898         signature_dict = signing_key['signatures']
899         decoded_signatures = {}
900         for domain in signature_dict:
901           decoded_signatures[domain] = base64.b64decode(signature_dict[domain])
902         self.keys.append({'private_key': key,
903                           'signatures': decoded_signatures})
904
905     # Derive the public keys from the private keys.
906     for entry in self.keys:
907       key = entry['private_key']
908
909       algorithm = asn1der.Sequence(
910           [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
911             asn1der.Data(asn1der.NULL, '') ])
912       rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
913                                       asn1der.Integer(key.e) ])
914       pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
915       entry['public_key'] = pubkey
916
917     # Load client state.
918     if self.client_state_file is not None:
919       try:
920         file_contents = open(self.client_state_file).read()
921         self._registered_tokens = json.loads(file_contents, strict=False)
922       except IOError:
923         pass
924
925   def GetPolicies(self):
926     """Returns the policies to be used, reloaded form the backend file every
927        time this is called.
928     """
929     policy = {}
930     if json is None:
931       print 'No JSON module, cannot parse policy information'
932     else :
933       try:
934         policy = json.loads(open(self.policy_path).read(), strict=False)
935       except IOError:
936         print 'Failed to load policy from %s' % self.policy_path
937     return policy
938
939   def RegisterDevice(self, device_id, machine_id, type):
940     """Registers a device or user and generates a DM token for it.
941
942     Args:
943       device_id: The device identifier provided by the client.
944
945     Returns:
946       The newly generated device token for the device.
947     """
948     dmtoken_chars = []
949     while len(dmtoken_chars) < 32:
950       dmtoken_chars.append(random.choice('0123456789abcdef'))
951     dmtoken = ''.join(dmtoken_chars)
952     allowed_policy_types = {
953       dm.DeviceRegisterRequest.BROWSER: [
954           'google/chrome/user',
955           'google/chrome/extension'
956       ],
957       dm.DeviceRegisterRequest.USER: [
958           'google/chromeos/user',
959           'google/chrome/extension'
960       ],
961       dm.DeviceRegisterRequest.DEVICE: [
962           'google/chromeos/device',
963           'google/chromeos/publicaccount'
964       ],
965       dm.DeviceRegisterRequest.ANDROID_BROWSER: [
966           'google/android/user'
967       ],
968       dm.DeviceRegisterRequest.IOS_BROWSER: [
969           'google/ios/user'
970       ],
971       dm.DeviceRegisterRequest.TT: ['google/chromeos/user',
972                                     'google/chrome/user'],
973     }
974     if machine_id in KIOSK_MACHINE_IDS:
975       enrollment_mode = dm.DeviceRegisterResponse.RETAIL
976     else:
977       enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE
978     self._registered_tokens[dmtoken] = {
979       'device_id': device_id,
980       'device_token': dmtoken,
981       'allowed_policy_types': allowed_policy_types[type],
982       'machine_name': 'chromeos-' + machine_id,
983       'machine_id': machine_id,
984       'enrollment_mode': enrollment_mode,
985     }
986     self.WriteClientState()
987     return self._registered_tokens[dmtoken]
988
989   def UpdateMachineId(self, dmtoken, machine_id):
990     """Updates the machine identifier for a registered device.
991
992     Args:
993       dmtoken: The device management token provided by the client.
994       machine_id: Updated hardware identifier value.
995     """
996     if dmtoken in self._registered_tokens:
997       self._registered_tokens[dmtoken]['machine_id'] = machine_id
998       self.WriteClientState()
999
1000   def UpdateStateKeys(self, dmtoken, state_keys):
1001     """Updates the state keys for a given client.
1002
1003     Args:
1004       dmtoken: The device management token provided by the client.
1005       state_keys: The state keys to set.
1006     """
1007     if dmtoken in self._registered_tokens:
1008       self._registered_tokens[dmtoken]['state_keys'] = map(
1009           lambda key : key.encode('hex'), state_keys)
1010       self.WriteClientState()
1011
1012   def LookupToken(self, dmtoken):
1013     """Looks up a device or a user by DM token.
1014
1015     Args:
1016       dmtoken: The device management token provided by the client.
1017
1018     Returns:
1019       A dictionary with information about a device or user that is registered by
1020       dmtoken, or None if the token is not found.
1021     """
1022     return self._registered_tokens.get(dmtoken, None)
1023
1024   def LookupByStateKey(self, state_key):
1025     """Looks up a device or a user by a state key.
1026
1027     Args:
1028       state_key: The state key provided by the client.
1029
1030     Returns:
1031       A dictionary with information about a device or user or None if there is
1032       no matching record.
1033     """
1034     for client in self._registered_tokens.values():
1035       if state_key.encode('hex') in client.get('state_keys', []):
1036         return client
1037
1038     return None
1039
1040   def GetMatchingStateKeyHashes(self, modulus, remainder):
1041     """Returns all clients registered with the server.
1042
1043     Returns:
1044       The list of registered clients.
1045     """
1046     state_keys = sum([ c.get('state_keys', [])
1047                        for c in self._registered_tokens.values() ], [])
1048     hashed_keys = map(lambda key: hashlib.sha256(key.decode('hex')).digest(),
1049                       set(state_keys))
1050     return filter(
1051         lambda hash : int(hash.encode('hex'), 16) % modulus == remainder,
1052         hashed_keys)
1053
1054   def UnregisterDevice(self, dmtoken):
1055     """Unregisters a device identified by the given DM token.
1056
1057     Args:
1058       dmtoken: The device management token provided by the client.
1059     """
1060     if dmtoken in self._registered_tokens.keys():
1061       del self._registered_tokens[dmtoken]
1062       self.WriteClientState()
1063
1064   def WriteClientState(self):
1065     """Writes the client state back to the file."""
1066     if self.client_state_file is not None:
1067       json_data = json.dumps(self._registered_tokens)
1068       open(self.client_state_file, 'w').write(json_data)
1069
1070   def GetBaseFilename(self, policy_selector):
1071     """Returns the base filename for the given policy_selector.
1072
1073     Args:
1074       policy_selector: The policy type and settings entity id, joined by '/'.
1075
1076     Returns:
1077       The filename corresponding to the policy_selector, without a file
1078       extension.
1079     """
1080     sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector)
1081     return os.path.join(self.data_dir or '',
1082                         'policy_%s' % sanitized_policy_selector)
1083
1084   def ListMatchingComponents(self, policy_type):
1085     """Returns a list of settings entity IDs that have a configuration file.
1086
1087     Args:
1088       policy_type: The policy type to look for. Only settings entity IDs for
1089       file selectors That match this policy_type will be returned.
1090
1091     Returns:
1092       A list of settings entity IDs for the given |policy_type| that have a
1093       configuration file in this server (either as a .bin, .txt or .data file).
1094     """
1095     base_name = self.GetBaseFilename(policy_type)
1096     files = glob.glob('%s_*.*' % base_name)
1097     len_base_name = len(base_name) + 1
1098     return [ file[len_base_name:file.rfind('.')] for file in files ]
1099
1100   def ReadPolicyFromDataDir(self, policy_selector, proto_message):
1101     """Tries to read policy payload from a file in the data directory.
1102
1103     First checks for a binary rendition of the policy protobuf in
1104     <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns
1105     it. If that file doesn't exist, tries
1106     <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a
1107     protobuf using proto_message. If that fails as well, returns None.
1108
1109     Args:
1110       policy_selector: Selects which policy to read.
1111       proto_message: Optional protobuf message object used for decoding the
1112           proto text format.
1113
1114     Returns:
1115       The binary payload message, or None if not found.
1116     """
1117     base_filename = self.GetBaseFilename(policy_selector)
1118
1119     # Try the binary payload file first.
1120     try:
1121       return open(base_filename + '.bin').read()
1122     except IOError:
1123       pass
1124
1125     # If that fails, try the text version instead.
1126     if proto_message is None:
1127       return None
1128
1129     try:
1130       text = open(base_filename + '.txt').read()
1131       google.protobuf.text_format.Merge(text, proto_message)
1132       return proto_message.SerializeToString()
1133     except IOError:
1134       return None
1135     except google.protobuf.text_format.ParseError:
1136       return None
1137
1138   def ReadPolicyDataFromDataDir(self, policy_selector):
1139     """Returns the external policy data for |policy_selector| if found.
1140
1141     Args:
1142       policy_selector: Selects which policy to read.
1143
1144     Returns:
1145       The data for the corresponding policy type and entity id, if found.
1146     """
1147     base_filename = self.GetBaseFilename(policy_selector)
1148     try:
1149       return open(base_filename + '.data').read()
1150     except IOError:
1151       return None
1152
1153   def GetBaseURL(self):
1154     """Returns the server base URL.
1155
1156     Respects the |server_base_url| configuration parameter, if present. Falls
1157     back to construct the URL from the server hostname and port otherwise.
1158
1159     Returns:
1160       The URL to use for constructing URLs that get returned to clients.
1161     """
1162     base_url = self.server_base_url
1163     if base_url is None:
1164       base_url = 'http://%s:%s' % self.server_address[:2]
1165
1166     return base_url
1167
1168
1169 class PolicyServerRunner(testserver_base.TestServerRunner):
1170
1171   def __init__(self):
1172     super(PolicyServerRunner, self).__init__()
1173
1174   def create_server(self, server_data):
1175     data_dir = self.options.data_dir or ''
1176     config_file = (self.options.config_file or
1177                    os.path.join(data_dir, 'device_management'))
1178     server = PolicyTestServer((self.options.host, self.options.port),
1179                               data_dir, config_file,
1180                               self.options.client_state_file,
1181                               self.options.policy_keys,
1182                               self.options.server_base_url)
1183     server_data['port'] = server.server_port
1184     return server
1185
1186   def add_options(self):
1187     testserver_base.TestServerRunner.add_options(self)
1188     self.option_parser.add_option('--client-state', dest='client_state_file',
1189                                   help='File that client state should be '
1190                                   'persisted to. This allows the server to be '
1191                                   'seeded by a list of pre-registered clients '
1192                                   'and restarts without abandoning registered '
1193                                   'clients.')
1194     self.option_parser.add_option('--policy-key', action='append',
1195                                   dest='policy_keys',
1196                                   help='Specify a path to a PEM-encoded '
1197                                   'private key to use for policy signing. May '
1198                                   'be specified multiple times in order to '
1199                                   'load multiple keys into the server. If the '
1200                                   'server has multiple keys, it will rotate '
1201                                   'through them in at each request in a '
1202                                   'round-robin fashion. The server will '
1203                                   'use a canned key if none is specified '
1204                                   'on the command line. The test server will '
1205                                   'also look for a verification signature file '
1206                                   'in the same location: <filename>.sig and if '
1207                                   'present will add the signature to the '
1208                                   'policy blob as appropriate via the '
1209                                   'new_public_key_verification_signature '
1210                                   'field.')
1211     self.option_parser.add_option('--log-level', dest='log_level',
1212                                   default='WARN',
1213                                   help='Log level threshold to use.')
1214     self.option_parser.add_option('--config-file', dest='config_file',
1215                                   help='Specify a configuration file to use '
1216                                   'instead of the default '
1217                                   '<data_dir>/device_management')
1218     self.option_parser.add_option('--server-base-url', dest='server_base_url',
1219                                   help='The server base URL to use when '
1220                                   'constructing URLs to return to the client.')
1221
1222   def run_server(self):
1223     logger = logging.getLogger()
1224     logger.setLevel(getattr(logging, str(self.options.log_level).upper()))
1225     if (self.options.log_to_console):
1226       logger.addHandler(logging.StreamHandler())
1227     if (self.options.log_file):
1228       logger.addHandler(logging.FileHandler(self.options.log_file))
1229
1230     testserver_base.TestServerRunner.run_server(self)
1231
1232
1233 if __name__ == '__main__':
1234   sys.exit(PolicyServerRunner().main())