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