Home | History | Annotate | Download | only in testserver
      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 
      8 It supports several test URLs, as specified by the handlers in TestPageHandler.
      9 By default, it listens on an ephemeral port and sends the port number back to
     10 the originating process over a pipe. The originating process can specify an
     11 explicit port if necessary.
     12 It can use https if you specify the flag --https=CERT where CERT is the path
     13 to a pem file containing the certificate and private key that should be used.
     14 """
     15 
     16 import asyncore
     17 import base64
     18 import BaseHTTPServer
     19 import cgi
     20 import errno
     21 import optparse
     22 import os
     23 import re
     24 import select
     25 import simplejson
     26 import SocketServer
     27 import socket
     28 import sys
     29 import struct
     30 import time
     31 import urlparse
     32 import warnings
     33 
     34 # Ignore deprecation warnings, they make our output more cluttered.
     35 warnings.filterwarnings("ignore", category=DeprecationWarning)
     36 
     37 import pyftpdlib.ftpserver
     38 import tlslite
     39 import tlslite.api
     40 
     41 try:
     42   import hashlib
     43   _new_md5 = hashlib.md5
     44 except ImportError:
     45   import md5
     46   _new_md5 = md5.new
     47 
     48 if sys.platform == 'win32':
     49   import msvcrt
     50 
     51 SERVER_HTTP = 0
     52 SERVER_FTP = 1
     53 SERVER_SYNC = 2
     54 
     55 # Using debug() seems to cause hangs on XP: see http://crbug.com/64515 .
     56 debug_output = sys.stderr
     57 def debug(str):
     58   debug_output.write(str + "\n")
     59   debug_output.flush()
     60 
     61 class 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 
     72 class 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 
    116 class 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 
    211 class 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 
    257 class 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 
   1241 class 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 
   1286 def 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 
   1303 class 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 
   1322 def 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 
   1418 if __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 (at] 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