testserver.py revision ddb351dbec246cf1fab5ec20d2d5520909041de1
1#!/usr/bin/python2.4
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""This is a simple HTTP server used for testing Chrome.
7
8It supports several test URLs, as specified by the handlers in TestPageHandler.
9By default, it listens on an ephemeral port and sends the port number back to
10the originating process over a pipe. The originating process can specify an
11explicit port if necessary.
12It can use https if you specify the flag --https=CERT where CERT is the path
13to a pem file containing the certificate and private key that should be used.
14"""
15
16import asyncore
17import base64
18import BaseHTTPServer
19import cgi
20import errno
21import optparse
22import os
23import re
24import select
25import simplejson
26import SocketServer
27import socket
28import sys
29import struct
30import time
31import urlparse
32import warnings
33
34# Ignore deprecation warnings, they make our output more cluttered.
35warnings.filterwarnings("ignore", category=DeprecationWarning)
36
37import pyftpdlib.ftpserver
38import tlslite
39import tlslite.api
40
41try:
42  import hashlib
43  _new_md5 = hashlib.md5
44except ImportError:
45  import md5
46  _new_md5 = md5.new
47
48if sys.platform == 'win32':
49  import msvcrt
50
51SERVER_HTTP = 0
52SERVER_FTP = 1
53SERVER_SYNC = 2
54
55# Using debug() seems to cause hangs on XP: see http://crbug.com/64515 .
56debug_output = sys.stderr
57def debug(str):
58  debug_output.write(str + "\n")
59  debug_output.flush()
60
61class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
62  """This is a specialization of of BaseHTTPServer to allow it
63  to be exited cleanly (by setting its "stop" member to True)."""
64
65  def serve_forever(self):
66    self.stop = False
67    self.nonce_time = None
68    while not self.stop:
69      self.handle_request()
70    self.socket.close()
71
72class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
73  """This is a specialization of StoppableHTTPerver that add https support."""
74
75  def __init__(self, server_address, request_hander_class, cert_path,
76               ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers):
77    s = open(cert_path).read()
78    x509 = tlslite.api.X509()
79    x509.parse(s)
80    self.cert_chain = tlslite.api.X509CertChain([x509])
81    s = open(cert_path).read()
82    self.private_key = tlslite.api.parsePEMKey(s, private=True)
83    self.ssl_client_auth = ssl_client_auth
84    self.ssl_client_cas = []
85    for ca_file in ssl_client_cas:
86        s = open(ca_file).read()
87        x509 = tlslite.api.X509()
88        x509.parse(s)
89        self.ssl_client_cas.append(x509.subject)
90    self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
91    if ssl_bulk_ciphers is not None:
92      self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
93
94    self.session_cache = tlslite.api.SessionCache()
95    StoppableHTTPServer.__init__(self, server_address, request_hander_class)
96
97  def handshake(self, tlsConnection):
98    """Creates the SSL connection."""
99    try:
100      tlsConnection.handshakeServer(certChain=self.cert_chain,
101                                    privateKey=self.private_key,
102                                    sessionCache=self.session_cache,
103                                    reqCert=self.ssl_client_auth,
104                                    settings=self.ssl_handshake_settings,
105                                    reqCAs=self.ssl_client_cas)
106      tlsConnection.ignoreAbruptClose = True
107      return True
108    except tlslite.api.TLSAbruptCloseError:
109      # Ignore abrupt close.
110      return True
111    except tlslite.api.TLSError, error:
112      print "Handshake failure:", str(error)
113      return False
114
115
116class SyncHTTPServer(StoppableHTTPServer):
117  """An HTTP server that handles sync commands."""
118
119  def __init__(self, server_address, request_handler_class):
120    # We import here to avoid pulling in chromiumsync's dependencies
121    # unless strictly necessary.
122    import chromiumsync
123    import xmppserver
124    StoppableHTTPServer.__init__(self, server_address, request_handler_class)
125    self._sync_handler = chromiumsync.TestServer()
126    self._xmpp_socket_map = {}
127    self._xmpp_server = xmppserver.XmppServer(
128      self._xmpp_socket_map, ('localhost', 0))
129    self.xmpp_port = self._xmpp_server.getsockname()[1]
130
131  def HandleCommand(self, query, raw_request):
132    return self._sync_handler.HandleCommand(query, raw_request)
133
134  def HandleRequestNoBlock(self):
135    """Handles a single request.
136
137    Copied from SocketServer._handle_request_noblock().
138    """
139    try:
140      request, client_address = self.get_request()
141    except socket.error:
142      return
143    if self.verify_request(request, client_address):
144      try:
145        self.process_request(request, client_address)
146      except:
147        self.handle_error(request, client_address)
148        self.close_request(request)
149
150  def serve_forever(self):
151    """This is a merge of asyncore.loop() and SocketServer.serve_forever().
152    """
153
154    def HandleXmppSocket(fd, socket_map, handler):
155      """Runs the handler for the xmpp connection for fd.
156
157      Adapted from asyncore.read() et al.
158      """
159      xmpp_connection = socket_map.get(fd)
160      # This could happen if a previous handler call caused fd to get
161      # removed from socket_map.
162      if xmpp_connection is None:
163        return
164      try:
165        handler(xmpp_connection)
166      except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
167        raise
168      except:
169        xmpp_connection.handle_error()
170
171    while True:
172      read_fds = [ self.fileno() ]
173      write_fds = []
174      exceptional_fds = []
175
176      for fd, xmpp_connection in self._xmpp_socket_map.items():
177        is_r = xmpp_connection.readable()
178        is_w = xmpp_connection.writable()
179        if is_r:
180          read_fds.append(fd)
181        if is_w:
182          write_fds.append(fd)
183        if is_r or is_w:
184          exceptional_fds.append(fd)
185
186      try:
187        read_fds, write_fds, exceptional_fds = (
188          select.select(read_fds, write_fds, exceptional_fds))
189      except select.error, err:
190        if err.args[0] != errno.EINTR:
191          raise
192        else:
193          continue
194
195      for fd in read_fds:
196        if fd == self.fileno():
197          self.HandleRequestNoBlock()
198          continue
199        HandleXmppSocket(fd, self._xmpp_socket_map,
200                         asyncore.dispatcher.handle_read_event)
201
202      for fd in write_fds:
203        HandleXmppSocket(fd, self._xmpp_socket_map,
204                         asyncore.dispatcher.handle_write_event)
205
206      for fd in exceptional_fds:
207        HandleXmppSocket(fd, self._xmpp_socket_map,
208                         asyncore.dispatcher.handle_expt_event)
209
210
211class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
212
213  def __init__(self, request, client_address, socket_server,
214               connect_handlers, get_handlers, post_handlers, put_handlers):
215    self._connect_handlers = connect_handlers
216    self._get_handlers = get_handlers
217    self._post_handlers = post_handlers
218    self._put_handlers = put_handlers
219    BaseHTTPServer.BaseHTTPRequestHandler.__init__(
220      self, request, client_address, socket_server)
221
222  def log_request(self, *args, **kwargs):
223    # Disable request logging to declutter test log output.
224    pass
225
226  def _ShouldHandleRequest(self, handler_name):
227    """Determines if the path can be handled by the handler.
228
229    We consider a handler valid if the path begins with the
230    handler name. It can optionally be followed by "?*", "/*".
231    """
232
233    pattern = re.compile('%s($|\?|/).*' % handler_name)
234    return pattern.match(self.path)
235
236  def do_CONNECT(self):
237    for handler in self._connect_handlers:
238      if handler():
239        return
240
241  def do_GET(self):
242    for handler in self._get_handlers:
243      if handler():
244        return
245
246  def do_POST(self):
247    for handler in self._post_handlers:
248      if handler():
249        return
250
251  def do_PUT(self):
252    for handler in self._put_handlers:
253      if handler():
254        return
255
256
257class TestPageHandler(BasePageHandler):
258
259  def __init__(self, request, client_address, socket_server):
260    connect_handlers = [
261      self.RedirectConnectHandler,
262      self.ServerAuthConnectHandler,
263      self.DefaultConnectResponseHandler]
264    get_handlers = [
265      self.NoCacheMaxAgeTimeHandler,
266      self.NoCacheTimeHandler,
267      self.CacheTimeHandler,
268      self.CacheExpiresHandler,
269      self.CacheProxyRevalidateHandler,
270      self.CachePrivateHandler,
271      self.CachePublicHandler,
272      self.CacheSMaxAgeHandler,
273      self.CacheMustRevalidateHandler,
274      self.CacheMustRevalidateMaxAgeHandler,
275      self.CacheNoStoreHandler,
276      self.CacheNoStoreMaxAgeHandler,
277      self.CacheNoTransformHandler,
278      self.DownloadHandler,
279      self.DownloadFinishHandler,
280      self.EchoHeader,
281      self.EchoHeaderCache,
282      self.EchoAllHandler,
283      self.FileHandler,
284      self.SetCookieHandler,
285      self.AuthBasicHandler,
286      self.AuthDigestHandler,
287      self.SlowServerHandler,
288      self.ContentTypeHandler,
289      self.NoContentHandler,
290      self.ServerRedirectHandler,
291      self.ClientRedirectHandler,
292      self.MultipartHandler,
293      self.DefaultResponseHandler]
294    post_handlers = [
295      self.EchoTitleHandler,
296      self.EchoAllHandler,
297      self.EchoHandler,
298      self.DeviceManagementHandler] + get_handlers
299    put_handlers = [
300      self.EchoTitleHandler,
301      self.EchoAllHandler,
302      self.EchoHandler] + get_handlers
303
304    self._mime_types = {
305      'crx' : 'application/x-chrome-extension',
306      'exe' : 'application/octet-stream',
307      'gif': 'image/gif',
308      'jpeg' : 'image/jpeg',
309      'jpg' : 'image/jpeg',
310      'pdf' : 'application/pdf',
311      'xml' : 'text/xml'
312    }
313    self._default_mime_type = 'text/html'
314
315    BasePageHandler.__init__(self, request, client_address, socket_server,
316                             connect_handlers, get_handlers, post_handlers,
317                             put_handlers)
318
319  def GetMIMETypeFromName(self, file_name):
320    """Returns the mime type for the specified file_name. So far it only looks
321    at the file extension."""
322
323    (shortname, extension) = os.path.splitext(file_name.split("?")[0])
324    if len(extension) == 0:
325      # no extension.
326      return self._default_mime_type
327
328    # extension starts with a dot, so we need to remove it
329    return self._mime_types.get(extension[1:], self._default_mime_type)
330
331  def NoCacheMaxAgeTimeHandler(self):
332    """This request handler yields a page with the title set to the current
333    system time, and no caching requested."""
334
335    if not self._ShouldHandleRequest("/nocachetime/maxage"):
336      return False
337
338    self.send_response(200)
339    self.send_header('Cache-Control', 'max-age=0')
340    self.send_header('Content-type', 'text/html')
341    self.end_headers()
342
343    self.wfile.write('<html><head><title>%s</title></head></html>' %
344                     time.time())
345
346    return True
347
348  def NoCacheTimeHandler(self):
349    """This request handler yields a page with the title set to the current
350    system time, and no caching requested."""
351
352    if not self._ShouldHandleRequest("/nocachetime"):
353      return False
354
355    self.send_response(200)
356    self.send_header('Cache-Control', 'no-cache')
357    self.send_header('Content-type', 'text/html')
358    self.end_headers()
359
360    self.wfile.write('<html><head><title>%s</title></head></html>' %
361                     time.time())
362
363    return True
364
365  def CacheTimeHandler(self):
366    """This request handler yields a page with the title set to the current
367    system time, and allows caching for one minute."""
368
369    if not self._ShouldHandleRequest("/cachetime"):
370      return False
371
372    self.send_response(200)
373    self.send_header('Cache-Control', 'max-age=60')
374    self.send_header('Content-type', 'text/html')
375    self.end_headers()
376
377    self.wfile.write('<html><head><title>%s</title></head></html>' %
378                     time.time())
379
380    return True
381
382  def CacheExpiresHandler(self):
383    """This request handler yields a page with the title set to the current
384    system time, and set the page to expire on 1 Jan 2099."""
385
386    if not self._ShouldHandleRequest("/cache/expires"):
387      return False
388
389    self.send_response(200)
390    self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
391    self.send_header('Content-type', 'text/html')
392    self.end_headers()
393
394    self.wfile.write('<html><head><title>%s</title></head></html>' %
395                     time.time())
396
397    return True
398
399  def CacheProxyRevalidateHandler(self):
400    """This request handler yields a page with the title set to the current
401    system time, and allows caching for 60 seconds"""
402
403    if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
404      return False
405
406    self.send_response(200)
407    self.send_header('Content-type', 'text/html')
408    self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
409    self.end_headers()
410
411    self.wfile.write('<html><head><title>%s</title></head></html>' %
412                     time.time())
413
414    return True
415
416  def CachePrivateHandler(self):
417    """This request handler yields a page with the title set to the current
418    system time, and allows caching for 5 seconds."""
419
420    if not self._ShouldHandleRequest("/cache/private"):
421      return False
422
423    self.send_response(200)
424    self.send_header('Content-type', 'text/html')
425    self.send_header('Cache-Control', 'max-age=3, private')
426    self.end_headers()
427
428    self.wfile.write('<html><head><title>%s</title></head></html>' %
429                     time.time())
430
431    return True
432
433  def CachePublicHandler(self):
434    """This request handler yields a page with the title set to the current
435    system time, and allows caching for 5 seconds."""
436
437    if not self._ShouldHandleRequest("/cache/public"):
438      return False
439
440    self.send_response(200)
441    self.send_header('Content-type', 'text/html')
442    self.send_header('Cache-Control', 'max-age=3, public')
443    self.end_headers()
444
445    self.wfile.write('<html><head><title>%s</title></head></html>' %
446                     time.time())
447
448    return True
449
450  def CacheSMaxAgeHandler(self):
451    """This request handler yields a page with the title set to the current
452    system time, and does not allow for caching."""
453
454    if not self._ShouldHandleRequest("/cache/s-maxage"):
455      return False
456
457    self.send_response(200)
458    self.send_header('Content-type', 'text/html')
459    self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
460    self.end_headers()
461
462    self.wfile.write('<html><head><title>%s</title></head></html>' %
463                     time.time())
464
465    return True
466
467  def CacheMustRevalidateHandler(self):
468    """This request handler yields a page with the title set to the current
469    system time, and does not allow caching."""
470
471    if not self._ShouldHandleRequest("/cache/must-revalidate"):
472      return False
473
474    self.send_response(200)
475    self.send_header('Content-type', 'text/html')
476    self.send_header('Cache-Control', 'must-revalidate')
477    self.end_headers()
478
479    self.wfile.write('<html><head><title>%s</title></head></html>' %
480                     time.time())
481
482    return True
483
484  def CacheMustRevalidateMaxAgeHandler(self):
485    """This request handler yields a page with the title set to the current
486    system time, and does not allow caching event though max-age of 60
487    seconds is specified."""
488
489    if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
490      return False
491
492    self.send_response(200)
493    self.send_header('Content-type', 'text/html')
494    self.send_header('Cache-Control', 'max-age=60, must-revalidate')
495    self.end_headers()
496
497    self.wfile.write('<html><head><title>%s</title></head></html>' %
498                     time.time())
499
500    return True
501
502  def CacheNoStoreHandler(self):
503    """This request handler yields a page with the title set to the current
504    system time, and does not allow the page to be stored."""
505
506    if not self._ShouldHandleRequest("/cache/no-store"):
507      return False
508
509    self.send_response(200)
510    self.send_header('Content-type', 'text/html')
511    self.send_header('Cache-Control', 'no-store')
512    self.end_headers()
513
514    self.wfile.write('<html><head><title>%s</title></head></html>' %
515                     time.time())
516
517    return True
518
519  def CacheNoStoreMaxAgeHandler(self):
520    """This request handler yields a page with the title set to the current
521    system time, and does not allow the page to be stored even though max-age
522    of 60 seconds is specified."""
523
524    if not self._ShouldHandleRequest("/cache/no-store/max-age"):
525      return False
526
527    self.send_response(200)
528    self.send_header('Content-type', 'text/html')
529    self.send_header('Cache-Control', 'max-age=60, no-store')
530    self.end_headers()
531
532    self.wfile.write('<html><head><title>%s</title></head></html>' %
533                     time.time())
534
535    return True
536
537
538  def CacheNoTransformHandler(self):
539    """This request handler yields a page with the title set to the current
540    system time, and does not allow the content to transformed during
541    user-agent caching"""
542
543    if not self._ShouldHandleRequest("/cache/no-transform"):
544      return False
545
546    self.send_response(200)
547    self.send_header('Content-type', 'text/html')
548    self.send_header('Cache-Control', 'no-transform')
549    self.end_headers()
550
551    self.wfile.write('<html><head><title>%s</title></head></html>' %
552                     time.time())
553
554    return True
555
556  def EchoHeader(self):
557    """This handler echoes back the value of a specific request header."""
558    return self.EchoHeaderHelper("/echoheader")
559
560    """This function echoes back the value of a specific request header"""
561    """while allowing caching for 16 hours."""
562  def EchoHeaderCache(self):
563    return self.EchoHeaderHelper("/echoheadercache")
564
565  def EchoHeaderHelper(self, echo_header):
566    """This function echoes back the value of the request header passed in."""
567    if not self._ShouldHandleRequest(echo_header):
568      return False
569
570    query_char = self.path.find('?')
571    if query_char != -1:
572      header_name = self.path[query_char+1:]
573
574    self.send_response(200)
575    self.send_header('Content-type', 'text/plain')
576    if echo_header == '/echoheadercache':
577      self.send_header('Cache-control', 'max-age=60000')
578    else:
579      self.send_header('Cache-control', 'no-cache')
580    # insert a vary header to properly indicate that the cachability of this
581    # request is subject to value of the request header being echoed.
582    if len(header_name) > 0:
583      self.send_header('Vary', header_name)
584    self.end_headers()
585
586    if len(header_name) > 0:
587      self.wfile.write(self.headers.getheader(header_name))
588
589    return True
590
591  def ReadRequestBody(self):
592    """This function reads the body of the current HTTP request, handling
593    both plain and chunked transfer encoded requests."""
594
595    if self.headers.getheader('transfer-encoding') != 'chunked':
596      length = int(self.headers.getheader('content-length'))
597      return self.rfile.read(length)
598
599    # Read the request body as chunks.
600    body = ""
601    while True:
602      line = self.rfile.readline()
603      length = int(line, 16)
604      if length == 0:
605        self.rfile.readline()
606        break
607      body += self.rfile.read(length)
608      self.rfile.read(2)
609    return body
610
611  def EchoHandler(self):
612    """This handler just echoes back the payload of the request, for testing
613    form submission."""
614
615    if not self._ShouldHandleRequest("/echo"):
616      return False
617
618    self.send_response(200)
619    self.send_header('Content-type', 'text/html')
620    self.end_headers()
621    self.wfile.write(self.ReadRequestBody())
622    return True
623
624  def EchoTitleHandler(self):
625    """This handler is like Echo, but sets the page title to the request."""
626
627    if not self._ShouldHandleRequest("/echotitle"):
628      return False
629
630    self.send_response(200)
631    self.send_header('Content-type', 'text/html')
632    self.end_headers()
633    request = self.ReadRequestBody()
634    self.wfile.write('<html><head><title>')
635    self.wfile.write(request)
636    self.wfile.write('</title></head></html>')
637    return True
638
639  def EchoAllHandler(self):
640    """This handler yields a (more) human-readable page listing information
641    about the request header & contents."""
642
643    if not self._ShouldHandleRequest("/echoall"):
644      return False
645
646    self.send_response(200)
647    self.send_header('Content-type', 'text/html')
648    self.end_headers()
649    self.wfile.write('<html><head><style>'
650      'pre { border: 1px solid black; margin: 5px; padding: 5px }'
651      '</style></head><body>'
652      '<div style="float: right">'
653      '<a href="/echo">back to referring page</a></div>'
654      '<h1>Request Body:</h1><pre>')
655
656    if self.command == 'POST' or self.command == 'PUT':
657      qs = self.ReadRequestBody()
658      params = cgi.parse_qs(qs, keep_blank_values=1)
659
660      for param in params:
661        self.wfile.write('%s=%s\n' % (param, params[param][0]))
662
663    self.wfile.write('</pre>')
664
665    self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
666
667    self.wfile.write('</body></html>')
668    return True
669
670  def DownloadHandler(self):
671    """This handler sends a downloadable file with or without reporting
672    the size (6K)."""
673
674    if self.path.startswith("/download-unknown-size"):
675      send_length = False
676    elif self.path.startswith("/download-known-size"):
677      send_length = True
678    else:
679      return False
680
681    #
682    # The test which uses this functionality is attempting to send
683    # small chunks of data to the client.  Use a fairly large buffer
684    # so that we'll fill chrome's IO buffer enough to force it to
685    # actually write the data.
686    # See also the comments in the client-side of this test in
687    # download_uitest.cc
688    #
689    size_chunk1 = 35*1024
690    size_chunk2 = 10*1024
691
692    self.send_response(200)
693    self.send_header('Content-type', 'application/octet-stream')
694    self.send_header('Cache-Control', 'max-age=0')
695    if send_length:
696      self.send_header('Content-Length', size_chunk1 + size_chunk2)
697    self.end_headers()
698
699    # First chunk of data:
700    self.wfile.write("*" * size_chunk1)
701    self.wfile.flush()
702
703    # handle requests until one of them clears this flag.
704    self.server.waitForDownload = True
705    while self.server.waitForDownload:
706      self.server.handle_request()
707
708    # Second chunk of data:
709    self.wfile.write("*" * size_chunk2)
710    return True
711
712  def DownloadFinishHandler(self):
713    """This handler just tells the server to finish the current download."""
714
715    if not self._ShouldHandleRequest("/download-finish"):
716      return False
717
718    self.server.waitForDownload = False
719    self.send_response(200)
720    self.send_header('Content-type', 'text/html')
721    self.send_header('Cache-Control', 'max-age=0')
722    self.end_headers()
723    return True
724
725  def _ReplaceFileData(self, data, query_parameters):
726    """Replaces matching substrings in a file.
727
728    If the 'replace_text' URL query parameter is present, it is expected to be
729    of the form old_text:new_text, which indicates that any old_text strings in
730    the file are replaced with new_text. Multiple 'replace_text' parameters may
731    be specified.
732
733    If the parameters are not present, |data| is returned.
734    """
735    query_dict = cgi.parse_qs(query_parameters)
736    replace_text_values = query_dict.get('replace_text', [])
737    for replace_text_value in replace_text_values:
738      replace_text_args = replace_text_value.split(':')
739      if len(replace_text_args) != 2:
740        raise ValueError(
741          'replace_text must be of form old_text:new_text. Actual value: %s' %
742          replace_text_value)
743      old_text_b64, new_text_b64 = replace_text_args
744      old_text = base64.urlsafe_b64decode(old_text_b64)
745      new_text = base64.urlsafe_b64decode(new_text_b64)
746      data = data.replace(old_text, new_text)
747    return data
748
749  def FileHandler(self):
750    """This handler sends the contents of the requested file.  Wow, it's like
751    a real webserver!"""
752
753    prefix = self.server.file_root_url
754    if not self.path.startswith(prefix):
755      return False
756
757    # Consume a request body if present.
758    if self.command == 'POST' or self.command == 'PUT' :
759      self.ReadRequestBody()
760
761    _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
762    sub_path = url_path[len(prefix):]
763    entries = sub_path.split('/')
764    file_path = os.path.join(self.server.data_dir, *entries)
765    if os.path.isdir(file_path):
766      file_path = os.path.join(file_path, 'index.html')
767
768    if not os.path.isfile(file_path):
769      print "File not found " + sub_path + " full path:" + file_path
770      self.send_error(404)
771      return True
772
773    f = open(file_path, "rb")
774    data = f.read()
775    f.close()
776
777    data = self._ReplaceFileData(data, query)
778
779    # If file.mock-http-headers exists, it contains the headers we
780    # should send.  Read them in and parse them.
781    headers_path = file_path + '.mock-http-headers'
782    if os.path.isfile(headers_path):
783      f = open(headers_path, "r")
784
785      # "HTTP/1.1 200 OK"
786      response = f.readline()
787      status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
788      self.send_response(int(status_code))
789
790      for line in f:
791        header_values = re.findall('(\S+):\s*(.*)', line)
792        if len(header_values) > 0:
793          # "name: value"
794          name, value = header_values[0]
795          self.send_header(name, value)
796      f.close()
797    else:
798      # Could be more generic once we support mime-type sniffing, but for
799      # now we need to set it explicitly.
800
801      range = self.headers.get('Range')
802      if range and range.startswith('bytes='):
803        # Note this doesn't handle all valid byte range values (i.e. open ended
804        # ones), just enough for what we needed so far.
805        range = range[6:].split('-')
806        start = int(range[0])
807        end = int(range[1])
808
809        self.send_response(206)
810        content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
811                        str(len(data))
812        self.send_header('Content-Range', content_range)
813        data = data[start: end + 1]
814      else:
815        self.send_response(200)
816
817      self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
818      self.send_header('Accept-Ranges', 'bytes')
819      self.send_header('Content-Length', len(data))
820      self.send_header('ETag', '\'' + file_path + '\'')
821    self.end_headers()
822
823    self.wfile.write(data)
824
825    return True
826
827  def SetCookieHandler(self):
828    """This handler just sets a cookie, for testing cookie handling."""
829
830    if not self._ShouldHandleRequest("/set-cookie"):
831      return False
832
833    query_char = self.path.find('?')
834    if query_char != -1:
835      cookie_values = self.path[query_char + 1:].split('&')
836    else:
837      cookie_values = ("",)
838    self.send_response(200)
839    self.send_header('Content-type', 'text/html')
840    for cookie_value in cookie_values:
841      self.send_header('Set-Cookie', '%s' % cookie_value)
842    self.end_headers()
843    for cookie_value in cookie_values:
844      self.wfile.write('%s' % cookie_value)
845    return True
846
847  def AuthBasicHandler(self):
848    """This handler tests 'Basic' authentication.  It just sends a page with
849    title 'user/pass' if you succeed."""
850
851    if not self._ShouldHandleRequest("/auth-basic"):
852      return False
853
854    username = userpass = password = b64str = ""
855    expected_password = 'secret'
856    realm = 'testrealm'
857    set_cookie_if_challenged = False
858
859    _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
860    query_params = cgi.parse_qs(query, True)
861    if 'set-cookie-if-challenged' in query_params:
862      set_cookie_if_challenged = True
863    if 'password' in query_params:
864      expected_password = query_params['password'][0]
865    if 'realm' in query_params:
866      realm = query_params['realm'][0]
867
868    auth = self.headers.getheader('authorization')
869    try:
870      if not auth:
871        raise Exception('no auth')
872      b64str = re.findall(r'Basic (\S+)', auth)[0]
873      userpass = base64.b64decode(b64str)
874      username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
875      if password != expected_password:
876        raise Exception('wrong password')
877    except Exception, e:
878      # Authentication failed.
879      self.send_response(401)
880      self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
881      self.send_header('Content-type', 'text/html')
882      if set_cookie_if_challenged:
883        self.send_header('Set-Cookie', 'got_challenged=true')
884      self.end_headers()
885      self.wfile.write('<html><head>')
886      self.wfile.write('<title>Denied: %s</title>' % e)
887      self.wfile.write('</head><body>')
888      self.wfile.write('auth=%s<p>' % auth)
889      self.wfile.write('b64str=%s<p>' % b64str)
890      self.wfile.write('username: %s<p>' % username)
891      self.wfile.write('userpass: %s<p>' % userpass)
892      self.wfile.write('password: %s<p>' % password)
893      self.wfile.write('You sent:<br>%s<p>' % self.headers)
894      self.wfile.write('</body></html>')
895      return True
896
897    # Authentication successful.  (Return a cachable response to allow for
898    # testing cached pages that require authentication.)
899    if_none_match = self.headers.getheader('if-none-match')
900    if if_none_match == "abc":
901      self.send_response(304)
902      self.end_headers()
903    elif url_path.endswith(".gif"):
904      # Using chrome/test/data/google/logo.gif as the test image
905      test_image_path = ['google', 'logo.gif']
906      gif_path = os.path.join(self.server.data_dir, *test_image_path)
907      if not os.path.isfile(gif_path):
908        self.send_error(404)
909        return True
910
911      f = open(gif_path, "rb")
912      data = f.read()
913      f.close()
914
915      self.send_response(200)
916      self.send_header('Content-type', 'image/gif')
917      self.send_header('Cache-control', 'max-age=60000')
918      self.send_header('Etag', 'abc')
919      self.end_headers()
920      self.wfile.write(data)
921    else:
922      self.send_response(200)
923      self.send_header('Content-type', 'text/html')
924      self.send_header('Cache-control', 'max-age=60000')
925      self.send_header('Etag', 'abc')
926      self.end_headers()
927      self.wfile.write('<html><head>')
928      self.wfile.write('<title>%s/%s</title>' % (username, password))
929      self.wfile.write('</head><body>')
930      self.wfile.write('auth=%s<p>' % auth)
931      self.wfile.write('You sent:<br>%s<p>' % self.headers)
932      self.wfile.write('</body></html>')
933
934    return True
935
936  def GetNonce(self, force_reset=False):
937   """Returns a nonce that's stable per request path for the server's lifetime.
938
939   This is a fake implementation. A real implementation would only use a given
940   nonce a single time (hence the name n-once). However, for the purposes of
941   unittesting, we don't care about the security of the nonce.
942
943   Args:
944     force_reset: Iff set, the nonce will be changed. Useful for testing the
945         "stale" response.
946   """
947   if force_reset or not self.server.nonce_time:
948     self.server.nonce_time = time.time()
949   return _new_md5('privatekey%s%d' %
950                   (self.path, self.server.nonce_time)).hexdigest()
951
952  def AuthDigestHandler(self):
953    """This handler tests 'Digest' authentication.
954
955    It just sends a page with title 'user/pass' if you succeed.
956
957    A stale response is sent iff "stale" is present in the request path.
958    """
959    if not self._ShouldHandleRequest("/auth-digest"):
960      return False
961
962    stale = 'stale' in self.path
963    nonce = self.GetNonce(force_reset=stale)
964    opaque = _new_md5('opaque').hexdigest()
965    password = 'secret'
966    realm = 'testrealm'
967
968    auth = self.headers.getheader('authorization')
969    pairs = {}
970    try:
971      if not auth:
972        raise Exception('no auth')
973      if not auth.startswith('Digest'):
974        raise Exception('not digest')
975      # Pull out all the name="value" pairs as a dictionary.
976      pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
977
978      # Make sure it's all valid.
979      if pairs['nonce'] != nonce:
980        raise Exception('wrong nonce')
981      if pairs['opaque'] != opaque:
982        raise Exception('wrong opaque')
983
984      # Check the 'response' value and make sure it matches our magic hash.
985      # See http://www.ietf.org/rfc/rfc2617.txt
986      hash_a1 = _new_md5(
987          ':'.join([pairs['username'], realm, password])).hexdigest()
988      hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
989      if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
990        response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
991            pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
992      else:
993        response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
994
995      if pairs['response'] != response:
996        raise Exception('wrong password')
997    except Exception, e:
998      # Authentication failed.
999      self.send_response(401)
1000      hdr = ('Digest '
1001             'realm="%s", '
1002             'domain="/", '
1003             'qop="auth", '
1004             'algorithm=MD5, '
1005             'nonce="%s", '
1006             'opaque="%s"') % (realm, nonce, opaque)
1007      if stale:
1008        hdr += ', stale="TRUE"'
1009      self.send_header('WWW-Authenticate', hdr)
1010      self.send_header('Content-type', 'text/html')
1011      self.end_headers()
1012      self.wfile.write('<html><head>')
1013      self.wfile.write('<title>Denied: %s</title>' % e)
1014      self.wfile.write('</head><body>')
1015      self.wfile.write('auth=%s<p>' % auth)
1016      self.wfile.write('pairs=%s<p>' % pairs)
1017      self.wfile.write('You sent:<br>%s<p>' % self.headers)
1018      self.wfile.write('We are replying:<br>%s<p>' % hdr)
1019      self.wfile.write('</body></html>')
1020      return True
1021
1022    # Authentication successful.
1023    self.send_response(200)
1024    self.send_header('Content-type', 'text/html')
1025    self.end_headers()
1026    self.wfile.write('<html><head>')
1027    self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1028    self.wfile.write('</head><body>')
1029    self.wfile.write('auth=%s<p>' % auth)
1030    self.wfile.write('pairs=%s<p>' % pairs)
1031    self.wfile.write('</body></html>')
1032
1033    return True
1034
1035  def SlowServerHandler(self):
1036    """Wait for the user suggested time before responding. The syntax is
1037    /slow?0.5 to wait for half a second."""
1038    if not self._ShouldHandleRequest("/slow"):
1039      return False
1040    query_char = self.path.find('?')
1041    wait_sec = 1.0
1042    if query_char >= 0:
1043      try:
1044        wait_sec = int(self.path[query_char + 1:])
1045      except ValueError:
1046        pass
1047    time.sleep(wait_sec)
1048    self.send_response(200)
1049    self.send_header('Content-type', 'text/plain')
1050    self.end_headers()
1051    self.wfile.write("waited %d seconds" % wait_sec)
1052    return True
1053
1054  def ContentTypeHandler(self):
1055    """Returns a string of html with the given content type.  E.g.,
1056    /contenttype?text/css returns an html file with the Content-Type
1057    header set to text/css."""
1058    if not self._ShouldHandleRequest("/contenttype"):
1059      return False
1060    query_char = self.path.find('?')
1061    content_type = self.path[query_char + 1:].strip()
1062    if not content_type:
1063      content_type = 'text/html'
1064    self.send_response(200)
1065    self.send_header('Content-Type', content_type)
1066    self.end_headers()
1067    self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1068    return True
1069
1070  def NoContentHandler(self):
1071    """Returns a 204 No Content response."""
1072    if not self._ShouldHandleRequest("/nocontent"):
1073      return False
1074    self.send_response(204)
1075    self.end_headers()
1076    return True
1077
1078  def ServerRedirectHandler(self):
1079    """Sends a server redirect to the given URL. The syntax is
1080    '/server-redirect?http://foo.bar/asdf' to redirect to
1081    'http://foo.bar/asdf'"""
1082
1083    test_name = "/server-redirect"
1084    if not self._ShouldHandleRequest(test_name):
1085      return False
1086
1087    query_char = self.path.find('?')
1088    if query_char < 0 or len(self.path) <= query_char + 1:
1089      self.sendRedirectHelp(test_name)
1090      return True
1091    dest = self.path[query_char + 1:]
1092
1093    self.send_response(301)  # moved permanently
1094    self.send_header('Location', dest)
1095    self.send_header('Content-type', 'text/html')
1096    self.end_headers()
1097    self.wfile.write('<html><head>')
1098    self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1099
1100    return True
1101
1102  def ClientRedirectHandler(self):
1103    """Sends a client redirect to the given URL. The syntax is
1104    '/client-redirect?http://foo.bar/asdf' to redirect to
1105    'http://foo.bar/asdf'"""
1106
1107    test_name = "/client-redirect"
1108    if not self._ShouldHandleRequest(test_name):
1109      return False
1110
1111    query_char = self.path.find('?');
1112    if query_char < 0 or len(self.path) <= query_char + 1:
1113      self.sendRedirectHelp(test_name)
1114      return True
1115    dest = self.path[query_char + 1:]
1116
1117    self.send_response(200)
1118    self.send_header('Content-type', 'text/html')
1119    self.end_headers()
1120    self.wfile.write('<html><head>')
1121    self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1122    self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1123
1124    return True
1125
1126  def MultipartHandler(self):
1127    """Send a multipart response (10 text/html pages)."""
1128    test_name = "/multipart"
1129    if not self._ShouldHandleRequest(test_name):
1130      return False
1131
1132    num_frames = 10
1133    bound = '12345'
1134    self.send_response(200)
1135    self.send_header('Content-type',
1136                     'multipart/x-mixed-replace;boundary=' + bound)
1137    self.end_headers()
1138
1139    for i in xrange(num_frames):
1140      self.wfile.write('--' + bound + '\r\n')
1141      self.wfile.write('Content-type: text/html\r\n\r\n')
1142      self.wfile.write('<title>page ' + str(i) + '</title>')
1143      self.wfile.write('page ' + str(i))
1144
1145    self.wfile.write('--' + bound + '--')
1146    return True
1147
1148  def DefaultResponseHandler(self):
1149    """This is the catch-all response handler for requests that aren't handled
1150    by one of the special handlers above.
1151    Note that we specify the content-length as without it the https connection
1152    is not closed properly (and the browser keeps expecting data)."""
1153
1154    contents = "Default response given for path: " + self.path
1155    self.send_response(200)
1156    self.send_header('Content-type', 'text/html')
1157    self.send_header("Content-Length", len(contents))
1158    self.end_headers()
1159    self.wfile.write(contents)
1160    return True
1161
1162  def RedirectConnectHandler(self):
1163    """Sends a redirect to the CONNECT request for www.redirect.com. This
1164    response is not specified by the RFC, so the browser should not follow
1165    the redirect."""
1166
1167    if (self.path.find("www.redirect.com") < 0):
1168      return False
1169
1170    dest = "http://www.destination.com/foo.js"
1171
1172    self.send_response(302)  # moved temporarily
1173    self.send_header('Location', dest)
1174    self.send_header('Connection', 'close')
1175    self.end_headers()
1176    return True
1177
1178  def ServerAuthConnectHandler(self):
1179    """Sends a 401 to the CONNECT request for www.server-auth.com. This
1180    response doesn't make sense because the proxy server cannot request
1181    server authentication."""
1182
1183    if (self.path.find("www.server-auth.com") < 0):
1184      return False
1185
1186    challenge = 'Basic realm="WallyWorld"'
1187
1188    self.send_response(401)  # unauthorized
1189    self.send_header('WWW-Authenticate', challenge)
1190    self.send_header('Connection', 'close')
1191    self.end_headers()
1192    return True
1193
1194  def DefaultConnectResponseHandler(self):
1195    """This is the catch-all response handler for CONNECT requests that aren't
1196    handled by one of the special handlers above.  Real Web servers respond
1197    with 400 to CONNECT requests."""
1198
1199    contents = "Your client has issued a malformed or illegal request."
1200    self.send_response(400)  # bad request
1201    self.send_header('Content-type', 'text/html')
1202    self.send_header("Content-Length", len(contents))
1203    self.end_headers()
1204    self.wfile.write(contents)
1205    return True
1206
1207  def DeviceManagementHandler(self):
1208    """Delegates to the device management service used for cloud policy."""
1209    if not self._ShouldHandleRequest("/device_management"):
1210      return False
1211
1212    raw_request = self.ReadRequestBody()
1213
1214    if not self.server._device_management_handler:
1215      import device_management
1216      policy_path = os.path.join(self.server.data_dir, 'device_management')
1217      self.server._device_management_handler = (
1218          device_management.TestServer(policy_path,
1219                                       self.server.policy_keys,
1220                                       self.server.policy_user))
1221
1222    http_response, raw_reply = (
1223        self.server._device_management_handler.HandleRequest(self.path,
1224                                                             self.headers,
1225                                                             raw_request))
1226    self.send_response(http_response)
1227    self.end_headers()
1228    self.wfile.write(raw_reply)
1229    return True
1230
1231  # called by the redirect handling function when there is no parameter
1232  def sendRedirectHelp(self, redirect_name):
1233    self.send_response(200)
1234    self.send_header('Content-type', 'text/html')
1235    self.end_headers()
1236    self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1237    self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1238    self.wfile.write('</body></html>')
1239
1240
1241class SyncPageHandler(BasePageHandler):
1242  """Handler for the main HTTP sync server."""
1243
1244  def __init__(self, request, client_address, sync_http_server):
1245    get_handlers = [self.ChromiumSyncTimeHandler]
1246    post_handlers = [self.ChromiumSyncCommandHandler]
1247    BasePageHandler.__init__(self, request, client_address,
1248                             sync_http_server, [], get_handlers,
1249                             post_handlers, [])
1250
1251  def ChromiumSyncTimeHandler(self):
1252    """Handle Chromium sync .../time requests.
1253
1254    The syncer sometimes checks server reachability by examining /time.
1255    """
1256    test_name = "/chromiumsync/time"
1257    if not self._ShouldHandleRequest(test_name):
1258      return False
1259
1260    self.send_response(200)
1261    self.send_header('Content-type', 'text/html')
1262    self.end_headers()
1263    return True
1264
1265  def ChromiumSyncCommandHandler(self):
1266    """Handle a chromiumsync command arriving via http.
1267
1268    This covers all sync protocol commands: authentication, getupdates, and
1269    commit.
1270    """
1271    test_name = "/chromiumsync/command"
1272    if not self._ShouldHandleRequest(test_name):
1273      return False
1274
1275    length = int(self.headers.getheader('content-length'))
1276    raw_request = self.rfile.read(length)
1277
1278    http_response, raw_reply = self.server.HandleCommand(
1279        self.path, raw_request)
1280    self.send_response(http_response)
1281    self.end_headers()
1282    self.wfile.write(raw_reply)
1283    return True
1284
1285
1286def MakeDataDir():
1287  if options.data_dir:
1288    if not os.path.isdir(options.data_dir):
1289      print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1290      return None
1291    my_data_dir = options.data_dir
1292  else:
1293    # Create the default path to our data dir, relative to the exe dir.
1294    my_data_dir = os.path.dirname(sys.argv[0])
1295    my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
1296                               "test", "data")
1297
1298    #TODO(ibrar): Must use Find* funtion defined in google\tools
1299    #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1300
1301  return my_data_dir
1302
1303class FileMultiplexer:
1304  def __init__(self, fd1, fd2) :
1305    self.__fd1 = fd1
1306    self.__fd2 = fd2
1307
1308  def __del__(self) :
1309    if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1310      self.__fd1.close()
1311    if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1312      self.__fd2.close()
1313
1314  def write(self, text) :
1315    self.__fd1.write(text)
1316    self.__fd2.write(text)
1317
1318  def flush(self) :
1319    self.__fd1.flush()
1320    self.__fd2.flush()
1321
1322def main(options, args):
1323  logfile = open('testserver.log', 'w')
1324  sys.stderr = FileMultiplexer(sys.stderr, logfile)
1325  if options.log_to_console:
1326    sys.stdout = FileMultiplexer(sys.stdout, logfile)
1327  else:
1328    sys.stdout = logfile
1329
1330  port = options.port
1331
1332  server_data = {}
1333
1334  if options.server_type == SERVER_HTTP:
1335    if options.cert:
1336      # let's make sure the cert file exists.
1337      if not os.path.isfile(options.cert):
1338        print 'specified server cert file not found: ' + options.cert + \
1339              ' exiting...'
1340        return
1341      for ca_cert in options.ssl_client_ca:
1342        if not os.path.isfile(ca_cert):
1343          print 'specified trusted client CA file not found: ' + ca_cert + \
1344                ' exiting...'
1345          return
1346      server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
1347                           options.ssl_client_auth, options.ssl_client_ca,
1348                           options.ssl_bulk_cipher)
1349      print 'HTTPS server started on port %d...' % server.server_port
1350    else:
1351      server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
1352      print 'HTTP server started on port %d...' % server.server_port
1353
1354    server.data_dir = MakeDataDir()
1355    server.file_root_url = options.file_root_url
1356    server_data['port'] = server.server_port
1357    server._device_management_handler = None
1358    server.policy_keys = options.policy_keys
1359    server.policy_user = options.policy_user
1360  elif options.server_type == SERVER_SYNC:
1361    server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1362    print 'Sync HTTP server started on port %d...' % server.server_port
1363    print 'Sync XMPP server started on port %d...' % server.xmpp_port
1364    server_data['port'] = server.server_port
1365    server_data['xmpp_port'] = server.xmpp_port
1366  # means FTP Server
1367  else:
1368    my_data_dir = MakeDataDir()
1369
1370    # Instantiate a dummy authorizer for managing 'virtual' users
1371    authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1372
1373    # Define a new user having full r/w permissions and a read-only
1374    # anonymous user
1375    authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1376
1377    authorizer.add_anonymous(my_data_dir)
1378
1379    # Instantiate FTP handler class
1380    ftp_handler = pyftpdlib.ftpserver.FTPHandler
1381    ftp_handler.authorizer = authorizer
1382
1383    # Define a customized banner (string returned when client connects)
1384    ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1385                          pyftpdlib.ftpserver.__ver__)
1386
1387    # Instantiate FTP server class and listen to 127.0.0.1:port
1388    address = ('127.0.0.1', port)
1389    server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
1390    server_data['port'] = server.socket.getsockname()[1]
1391    print 'FTP server started on port %d...' % server_data['port']
1392
1393  # Notify the parent that we've started. (BaseServer subclasses
1394  # bind their sockets on construction.)
1395  if options.startup_pipe is not None:
1396    server_data_json = simplejson.dumps(server_data)
1397    server_data_len = len(server_data_json)
1398    print 'sending server_data: %s (%d bytes)' % (
1399      server_data_json, server_data_len)
1400    if sys.platform == 'win32':
1401      fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1402    else:
1403      fd = options.startup_pipe
1404    startup_pipe = os.fdopen(fd, "w")
1405    # First write the data length as an unsigned 4-byte value.  This
1406    # is _not_ using network byte ordering since the other end of the
1407    # pipe is on the same machine.
1408    startup_pipe.write(struct.pack('=L', server_data_len))
1409    startup_pipe.write(server_data_json)
1410    startup_pipe.close()
1411
1412  try:
1413    server.serve_forever()
1414  except KeyboardInterrupt:
1415    print 'shutting down server'
1416    server.stop = True
1417
1418if __name__ == '__main__':
1419  option_parser = optparse.OptionParser()
1420  option_parser.add_option("-f", '--ftp', action='store_const',
1421                           const=SERVER_FTP, default=SERVER_HTTP,
1422                           dest='server_type',
1423                           help='start up an FTP server.')
1424  option_parser.add_option('', '--sync', action='store_const',
1425                           const=SERVER_SYNC, default=SERVER_HTTP,
1426                           dest='server_type',
1427                           help='start up a sync server.')
1428  option_parser.add_option('', '--log-to-console', action='store_const',
1429                           const=True, default=False,
1430                           dest='log_to_console',
1431                           help='Enables or disables sys.stdout logging to '
1432                           'the console.')
1433  option_parser.add_option('', '--port', default='0', type='int',
1434                           help='Port used by the server. If unspecified, the '
1435                           'server will listen on an ephemeral port.')
1436  option_parser.add_option('', '--data-dir', dest='data_dir',
1437                           help='Directory from which to read the files.')
1438  option_parser.add_option('', '--https', dest='cert',
1439                           help='Specify that https should be used, specify '
1440                           'the path to the cert containing the private key '
1441                           'the server should use.')
1442  option_parser.add_option('', '--ssl-client-auth', action='store_true',
1443                           help='Require SSL client auth on every connection.')
1444  option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1445                           help='Specify that the client certificate request '
1446                           'should include the CA named in the subject of '
1447                           'the DER-encoded certificate contained in the '
1448                           'specified file. This option may appear multiple '
1449                           'times, indicating multiple CA names should be '
1450                           'sent in the request.')
1451  option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1452                           help='Specify the bulk encryption algorithm(s)'
1453                           'that will be accepted by the SSL server. Valid '
1454                           'values are "aes256", "aes128", "3des", "rc4". If '
1455                           'omitted, all algorithms will be used. This '
1456                           'option may appear multiple times, indicating '
1457                           'multiple algorithms should be enabled.');
1458  option_parser.add_option('', '--file-root-url', default='/files/',
1459                           help='Specify a root URL for files served.')
1460  option_parser.add_option('', '--startup-pipe', type='int',
1461                           dest='startup_pipe',
1462                           help='File handle of pipe to parent process')
1463  option_parser.add_option('', '--policy-key', action='append',
1464                           dest='policy_keys',
1465                           help='Specify a path to a PEM-encoded private key '
1466                           'to use for policy signing. May be specified '
1467                           'multiple times in order to load multipe keys into '
1468                           'the server. If ther server has multiple keys, it '
1469                           'will rotate through them in at each request a '
1470                           'round-robin fashion. The server will generate a '
1471                           'random key if none is specified on the command '
1472                           'line.')
1473  option_parser.add_option('', '--policy-user', default='user@example.com',
1474                           dest='policy_user',
1475                           help='Specify the user name the server should '
1476                           'report back to the client as the user owning the '
1477                           'token used for making the policy request.')
1478  options, args = option_parser.parse_args()
1479
1480  sys.exit(main(options, args))
1481