testserver.py revision eb525c5499e34cc9c4b825d6d9e75bb07cc06ace
1#!/usr/bin/env python
2# Copyright 2013 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/FTP/TCP/UDP/BASIC_AUTH_PROXY/WEBSOCKET server used for
7testing Chrome.
8
9It supports several test URLs, as specified by the handlers in TestPageHandler.
10By default, it listens on an ephemeral port and sends the port number back to
11the originating process over a pipe. The originating process can specify an
12explicit port if necessary.
13It can use https if you specify the flag --https=CERT where CERT is the path
14to a pem file containing the certificate and private key that should be used.
15"""
16
17import base64
18import BaseHTTPServer
19import cgi
20import hashlib
21import logging
22import minica
23import os
24import random
25import re
26import select
27import socket
28import SocketServer
29import struct
30import sys
31import threading
32import time
33import urllib
34import urlparse
35import zlib
36
37import echo_message
38import pyftpdlib.ftpserver
39import testserver_base
40import tlslite
41import tlslite.api
42
43BASE_DIR = os.path.dirname(os.path.abspath(__file__))
44sys.path.insert(
45    0, os.path.join(BASE_DIR, '..', '..', '..', 'third_party/pywebsocket/src'))
46from mod_pywebsocket.standalone import WebSocketServer
47
48SERVER_HTTP = 0
49SERVER_FTP = 1
50SERVER_TCP_ECHO = 2
51SERVER_UDP_ECHO = 3
52SERVER_BASIC_AUTH_PROXY = 4
53SERVER_WEBSOCKET = 5
54
55# Default request queue size for WebSocketServer.
56_DEFAULT_REQUEST_QUEUE_SIZE = 128
57
58class WebSocketOptions:
59  """Holds options for WebSocketServer."""
60
61  def __init__(self, host, port, data_dir):
62    self.request_queue_size = _DEFAULT_REQUEST_QUEUE_SIZE
63    self.server_host = host
64    self.port = port
65    self.websock_handlers = data_dir
66    self.scan_dir = None
67    self.allow_handlers_outside_root_dir = False
68    self.websock_handlers_map_file = None
69    self.cgi_directories = []
70    self.is_executable_method = None
71    self.allow_draft75 = False
72    self.strict = True
73
74    self.use_tls = False
75    self.private_key = None
76    self.certificate = None
77    self.tls_client_auth = False
78    self.tls_client_ca = None
79    self.use_basic_auth = False
80
81
82class RecordingSSLSessionCache(object):
83  """RecordingSSLSessionCache acts as a TLS session cache and maintains a log of
84  lookups and inserts in order to test session cache behaviours."""
85
86  def __init__(self):
87    self.log = []
88
89  def __getitem__(self, sessionID):
90    self.log.append(('lookup', sessionID))
91    raise KeyError()
92
93  def __setitem__(self, sessionID, session):
94    self.log.append(('insert', sessionID))
95
96
97class HTTPServer(testserver_base.ClientRestrictingServerMixIn,
98                 testserver_base.BrokenPipeHandlerMixIn,
99                 testserver_base.StoppableHTTPServer):
100  """This is a specialization of StoppableHTTPServer that adds client
101  verification."""
102
103  pass
104
105class OCSPServer(testserver_base.ClientRestrictingServerMixIn,
106                 testserver_base.BrokenPipeHandlerMixIn,
107                 BaseHTTPServer.HTTPServer):
108  """This is a specialization of HTTPServer that serves an
109  OCSP response"""
110
111  def serve_forever_on_thread(self):
112    self.thread = threading.Thread(target = self.serve_forever,
113                                   name = "OCSPServerThread")
114    self.thread.start()
115
116  def stop_serving(self):
117    self.shutdown()
118    self.thread.join()
119
120
121class HTTPSServer(tlslite.api.TLSSocketServerMixIn,
122                  testserver_base.ClientRestrictingServerMixIn,
123                  testserver_base.BrokenPipeHandlerMixIn,
124                  testserver_base.StoppableHTTPServer):
125  """This is a specialization of StoppableHTTPServer that add https support and
126  client verification."""
127
128  def __init__(self, server_address, request_hander_class, pem_cert_and_key,
129               ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers,
130               record_resume_info, tls_intolerant):
131    self.cert_chain = tlslite.api.X509CertChain().parseChain(pem_cert_and_key)
132    # Force using only python implementation - otherwise behavior is different
133    # depending on whether m2crypto Python module is present (error is thrown
134    # when it is). m2crypto uses a C (based on OpenSSL) implementation under
135    # the hood.
136    self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key,
137                                               private=True,
138                                               implementations=['python'])
139    self.ssl_client_auth = ssl_client_auth
140    self.ssl_client_cas = []
141    self.tls_intolerant = tls_intolerant
142
143    for ca_file in ssl_client_cas:
144      s = open(ca_file).read()
145      x509 = tlslite.api.X509()
146      x509.parse(s)
147      self.ssl_client_cas.append(x509.subject)
148    self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
149    if ssl_bulk_ciphers is not None:
150      self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
151
152    if record_resume_info:
153      # If record_resume_info is true then we'll replace the session cache with
154      # an object that records the lookups and inserts that it sees.
155      self.session_cache = RecordingSSLSessionCache()
156    else:
157      self.session_cache = tlslite.api.SessionCache()
158    testserver_base.StoppableHTTPServer.__init__(self,
159                                                 server_address,
160                                                 request_hander_class)
161
162  def handshake(self, tlsConnection):
163    """Creates the SSL connection."""
164
165    try:
166      self.tlsConnection = tlsConnection
167      tlsConnection.handshakeServer(certChain=self.cert_chain,
168                                    privateKey=self.private_key,
169                                    sessionCache=self.session_cache,
170                                    reqCert=self.ssl_client_auth,
171                                    settings=self.ssl_handshake_settings,
172                                    reqCAs=self.ssl_client_cas,
173                                    tlsIntolerant=self.tls_intolerant)
174      tlsConnection.ignoreAbruptClose = True
175      return True
176    except tlslite.api.TLSAbruptCloseError:
177      # Ignore abrupt close.
178      return True
179    except tlslite.api.TLSError, error:
180      print "Handshake failure:", str(error)
181      return False
182
183
184class FTPServer(testserver_base.ClientRestrictingServerMixIn,
185                pyftpdlib.ftpserver.FTPServer):
186  """This is a specialization of FTPServer that adds client verification."""
187
188  pass
189
190
191class TCPEchoServer(testserver_base.ClientRestrictingServerMixIn,
192                    SocketServer.TCPServer):
193  """A TCP echo server that echoes back what it has received."""
194
195  def server_bind(self):
196    """Override server_bind to store the server name."""
197
198    SocketServer.TCPServer.server_bind(self)
199    host, port = self.socket.getsockname()[:2]
200    self.server_name = socket.getfqdn(host)
201    self.server_port = port
202
203  def serve_forever(self):
204    self.stop = False
205    self.nonce_time = None
206    while not self.stop:
207      self.handle_request()
208    self.socket.close()
209
210
211class UDPEchoServer(testserver_base.ClientRestrictingServerMixIn,
212                    SocketServer.UDPServer):
213  """A UDP echo server that echoes back what it has received."""
214
215  def server_bind(self):
216    """Override server_bind to store the server name."""
217
218    SocketServer.UDPServer.server_bind(self)
219    host, port = self.socket.getsockname()[:2]
220    self.server_name = socket.getfqdn(host)
221    self.server_port = port
222
223  def serve_forever(self):
224    self.stop = False
225    self.nonce_time = None
226    while not self.stop:
227      self.handle_request()
228    self.socket.close()
229
230
231class TestPageHandler(testserver_base.BasePageHandler):
232  # Class variables to allow for persistence state between page handler
233  # invocations
234  rst_limits = {}
235  fail_precondition = {}
236
237  def __init__(self, request, client_address, socket_server):
238    connect_handlers = [
239      self.RedirectConnectHandler,
240      self.ServerAuthConnectHandler,
241      self.DefaultConnectResponseHandler]
242    get_handlers = [
243      self.NoCacheMaxAgeTimeHandler,
244      self.NoCacheTimeHandler,
245      self.CacheTimeHandler,
246      self.CacheExpiresHandler,
247      self.CacheProxyRevalidateHandler,
248      self.CachePrivateHandler,
249      self.CachePublicHandler,
250      self.CacheSMaxAgeHandler,
251      self.CacheMustRevalidateHandler,
252      self.CacheMustRevalidateMaxAgeHandler,
253      self.CacheNoStoreHandler,
254      self.CacheNoStoreMaxAgeHandler,
255      self.CacheNoTransformHandler,
256      self.DownloadHandler,
257      self.DownloadFinishHandler,
258      self.EchoHeader,
259      self.EchoHeaderCache,
260      self.EchoAllHandler,
261      self.ZipFileHandler,
262      self.FileHandler,
263      self.SetCookieHandler,
264      self.SetManyCookiesHandler,
265      self.ExpectAndSetCookieHandler,
266      self.SetHeaderHandler,
267      self.AuthBasicHandler,
268      self.AuthDigestHandler,
269      self.SlowServerHandler,
270      self.ChunkedServerHandler,
271      self.ContentTypeHandler,
272      self.NoContentHandler,
273      self.ServerRedirectHandler,
274      self.ClientRedirectHandler,
275      self.MultipartHandler,
276      self.GetSSLSessionCacheHandler,
277      self.SSLManySmallRecords,
278      self.GetChannelID,
279      self.CloseSocketHandler,
280      self.RangeResetHandler,
281      self.DefaultResponseHandler]
282    post_handlers = [
283      self.EchoTitleHandler,
284      self.EchoHandler,
285      self.PostOnlyFileHandler] + get_handlers
286    put_handlers = [
287      self.EchoTitleHandler,
288      self.EchoHandler] + get_handlers
289    head_handlers = [
290      self.FileHandler,
291      self.DefaultResponseHandler]
292
293    self._mime_types = {
294      'crx' : 'application/x-chrome-extension',
295      'exe' : 'application/octet-stream',
296      'gif': 'image/gif',
297      'jpeg' : 'image/jpeg',
298      'jpg' : 'image/jpeg',
299      'json': 'application/json',
300      'pdf' : 'application/pdf',
301      'wav' : 'audio/wav',
302      'xml' : 'text/xml'
303    }
304    self._default_mime_type = 'text/html'
305
306    testserver_base.BasePageHandler.__init__(self, request, client_address,
307                                             socket_server, connect_handlers,
308                                             get_handlers, head_handlers,
309                                             post_handlers, put_handlers)
310
311  def GetMIMETypeFromName(self, file_name):
312    """Returns the mime type for the specified file_name. So far it only looks
313    at the file extension."""
314
315    (_shortname, extension) = os.path.splitext(file_name.split("?")[0])
316    if len(extension) == 0:
317      # no extension.
318      return self._default_mime_type
319
320    # extension starts with a dot, so we need to remove it
321    return self._mime_types.get(extension[1:], self._default_mime_type)
322
323  def NoCacheMaxAgeTimeHandler(self):
324    """This request handler yields a page with the title set to the current
325    system time, and no caching requested."""
326
327    if not self._ShouldHandleRequest("/nocachetime/maxage"):
328      return False
329
330    self.send_response(200)
331    self.send_header('Cache-Control', 'max-age=0')
332    self.send_header('Content-Type', 'text/html')
333    self.end_headers()
334
335    self.wfile.write('<html><head><title>%s</title></head></html>' %
336                     time.time())
337
338    return True
339
340  def NoCacheTimeHandler(self):
341    """This request handler yields a page with the title set to the current
342    system time, and no caching requested."""
343
344    if not self._ShouldHandleRequest("/nocachetime"):
345      return False
346
347    self.send_response(200)
348    self.send_header('Cache-Control', 'no-cache')
349    self.send_header('Content-Type', 'text/html')
350    self.end_headers()
351
352    self.wfile.write('<html><head><title>%s</title></head></html>' %
353                     time.time())
354
355    return True
356
357  def CacheTimeHandler(self):
358    """This request handler yields a page with the title set to the current
359    system time, and allows caching for one minute."""
360
361    if not self._ShouldHandleRequest("/cachetime"):
362      return False
363
364    self.send_response(200)
365    self.send_header('Cache-Control', 'max-age=60')
366    self.send_header('Content-Type', 'text/html')
367    self.end_headers()
368
369    self.wfile.write('<html><head><title>%s</title></head></html>' %
370                     time.time())
371
372    return True
373
374  def CacheExpiresHandler(self):
375    """This request handler yields a page with the title set to the current
376    system time, and set the page to expire on 1 Jan 2099."""
377
378    if not self._ShouldHandleRequest("/cache/expires"):
379      return False
380
381    self.send_response(200)
382    self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
383    self.send_header('Content-Type', 'text/html')
384    self.end_headers()
385
386    self.wfile.write('<html><head><title>%s</title></head></html>' %
387                     time.time())
388
389    return True
390
391  def CacheProxyRevalidateHandler(self):
392    """This request handler yields a page with the title set to the current
393    system time, and allows caching for 60 seconds"""
394
395    if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
396      return False
397
398    self.send_response(200)
399    self.send_header('Content-Type', 'text/html')
400    self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
401    self.end_headers()
402
403    self.wfile.write('<html><head><title>%s</title></head></html>' %
404                     time.time())
405
406    return True
407
408  def CachePrivateHandler(self):
409    """This request handler yields a page with the title set to the current
410    system time, and allows caching for 5 seconds."""
411
412    if not self._ShouldHandleRequest("/cache/private"):
413      return False
414
415    self.send_response(200)
416    self.send_header('Content-Type', 'text/html')
417    self.send_header('Cache-Control', 'max-age=3, private')
418    self.end_headers()
419
420    self.wfile.write('<html><head><title>%s</title></head></html>' %
421                     time.time())
422
423    return True
424
425  def CachePublicHandler(self):
426    """This request handler yields a page with the title set to the current
427    system time, and allows caching for 5 seconds."""
428
429    if not self._ShouldHandleRequest("/cache/public"):
430      return False
431
432    self.send_response(200)
433    self.send_header('Content-Type', 'text/html')
434    self.send_header('Cache-Control', 'max-age=3, public')
435    self.end_headers()
436
437    self.wfile.write('<html><head><title>%s</title></head></html>' %
438                     time.time())
439
440    return True
441
442  def CacheSMaxAgeHandler(self):
443    """This request handler yields a page with the title set to the current
444    system time, and does not allow for caching."""
445
446    if not self._ShouldHandleRequest("/cache/s-maxage"):
447      return False
448
449    self.send_response(200)
450    self.send_header('Content-Type', 'text/html')
451    self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
452    self.end_headers()
453
454    self.wfile.write('<html><head><title>%s</title></head></html>' %
455                     time.time())
456
457    return True
458
459  def CacheMustRevalidateHandler(self):
460    """This request handler yields a page with the title set to the current
461    system time, and does not allow caching."""
462
463    if not self._ShouldHandleRequest("/cache/must-revalidate"):
464      return False
465
466    self.send_response(200)
467    self.send_header('Content-Type', 'text/html')
468    self.send_header('Cache-Control', 'must-revalidate')
469    self.end_headers()
470
471    self.wfile.write('<html><head><title>%s</title></head></html>' %
472                     time.time())
473
474    return True
475
476  def CacheMustRevalidateMaxAgeHandler(self):
477    """This request handler yields a page with the title set to the current
478    system time, and does not allow caching event though max-age of 60
479    seconds is specified."""
480
481    if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
482      return False
483
484    self.send_response(200)
485    self.send_header('Content-Type', 'text/html')
486    self.send_header('Cache-Control', 'max-age=60, must-revalidate')
487    self.end_headers()
488
489    self.wfile.write('<html><head><title>%s</title></head></html>' %
490                     time.time())
491
492    return True
493
494  def CacheNoStoreHandler(self):
495    """This request handler yields a page with the title set to the current
496    system time, and does not allow the page to be stored."""
497
498    if not self._ShouldHandleRequest("/cache/no-store"):
499      return False
500
501    self.send_response(200)
502    self.send_header('Content-Type', 'text/html')
503    self.send_header('Cache-Control', 'no-store')
504    self.end_headers()
505
506    self.wfile.write('<html><head><title>%s</title></head></html>' %
507                     time.time())
508
509    return True
510
511  def CacheNoStoreMaxAgeHandler(self):
512    """This request handler yields a page with the title set to the current
513    system time, and does not allow the page to be stored even though max-age
514    of 60 seconds is specified."""
515
516    if not self._ShouldHandleRequest("/cache/no-store/max-age"):
517      return False
518
519    self.send_response(200)
520    self.send_header('Content-Type', 'text/html')
521    self.send_header('Cache-Control', 'max-age=60, no-store')
522    self.end_headers()
523
524    self.wfile.write('<html><head><title>%s</title></head></html>' %
525                     time.time())
526
527    return True
528
529
530  def CacheNoTransformHandler(self):
531    """This request handler yields a page with the title set to the current
532    system time, and does not allow the content to transformed during
533    user-agent caching"""
534
535    if not self._ShouldHandleRequest("/cache/no-transform"):
536      return False
537
538    self.send_response(200)
539    self.send_header('Content-Type', 'text/html')
540    self.send_header('Cache-Control', 'no-transform')
541    self.end_headers()
542
543    self.wfile.write('<html><head><title>%s</title></head></html>' %
544                     time.time())
545
546    return True
547
548  def EchoHeader(self):
549    """This handler echoes back the value of a specific request header."""
550
551    return self.EchoHeaderHelper("/echoheader")
552
553  def EchoHeaderCache(self):
554    """This function echoes back the value of a specific request header while
555    allowing caching for 16 hours."""
556
557    return self.EchoHeaderHelper("/echoheadercache")
558
559  def EchoHeaderHelper(self, echo_header):
560    """This function echoes back the value of the request header passed in."""
561
562    if not self._ShouldHandleRequest(echo_header):
563      return False
564
565    query_char = self.path.find('?')
566    if query_char != -1:
567      header_name = self.path[query_char+1:]
568
569    self.send_response(200)
570    self.send_header('Content-Type', 'text/plain')
571    if echo_header == '/echoheadercache':
572      self.send_header('Cache-control', 'max-age=60000')
573    else:
574      self.send_header('Cache-control', 'no-cache')
575    # insert a vary header to properly indicate that the cachability of this
576    # request is subject to value of the request header being echoed.
577    if len(header_name) > 0:
578      self.send_header('Vary', header_name)
579    self.end_headers()
580
581    if len(header_name) > 0:
582      self.wfile.write(self.headers.getheader(header_name))
583
584    return True
585
586  def ReadRequestBody(self):
587    """This function reads the body of the current HTTP request, handling
588    both plain and chunked transfer encoded requests."""
589
590    if self.headers.getheader('transfer-encoding') != 'chunked':
591      length = int(self.headers.getheader('content-length'))
592      return self.rfile.read(length)
593
594    # Read the request body as chunks.
595    body = ""
596    while True:
597      line = self.rfile.readline()
598      length = int(line, 16)
599      if length == 0:
600        self.rfile.readline()
601        break
602      body += self.rfile.read(length)
603      self.rfile.read(2)
604    return body
605
606  def EchoHandler(self):
607    """This handler just echoes back the payload of the request, for testing
608    form submission."""
609
610    if not self._ShouldHandleRequest("/echo"):
611      return False
612
613    self.send_response(200)
614    self.send_header('Content-Type', 'text/html')
615    self.end_headers()
616    self.wfile.write(self.ReadRequestBody())
617    return True
618
619  def EchoTitleHandler(self):
620    """This handler is like Echo, but sets the page title to the request."""
621
622    if not self._ShouldHandleRequest("/echotitle"):
623      return False
624
625    self.send_response(200)
626    self.send_header('Content-Type', 'text/html')
627    self.end_headers()
628    request = self.ReadRequestBody()
629    self.wfile.write('<html><head><title>')
630    self.wfile.write(request)
631    self.wfile.write('</title></head></html>')
632    return True
633
634  def EchoAllHandler(self):
635    """This handler yields a (more) human-readable page listing information
636    about the request header & contents."""
637
638    if not self._ShouldHandleRequest("/echoall"):
639      return False
640
641    self.send_response(200)
642    self.send_header('Content-Type', 'text/html')
643    self.end_headers()
644    self.wfile.write('<html><head><style>'
645      'pre { border: 1px solid black; margin: 5px; padding: 5px }'
646      '</style></head><body>'
647      '<div style="float: right">'
648      '<a href="/echo">back to referring page</a></div>'
649      '<h1>Request Body:</h1><pre>')
650
651    if self.command == 'POST' or self.command == 'PUT':
652      qs = self.ReadRequestBody()
653      params = cgi.parse_qs(qs, keep_blank_values=1)
654
655      for param in params:
656        self.wfile.write('%s=%s\n' % (param, params[param][0]))
657
658    self.wfile.write('</pre>')
659
660    self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
661
662    self.wfile.write('</body></html>')
663    return True
664
665  def DownloadHandler(self):
666    """This handler sends a downloadable file with or without reporting
667    the size (6K)."""
668
669    if self.path.startswith("/download-unknown-size"):
670      send_length = False
671    elif self.path.startswith("/download-known-size"):
672      send_length = True
673    else:
674      return False
675
676    #
677    # The test which uses this functionality is attempting to send
678    # small chunks of data to the client.  Use a fairly large buffer
679    # so that we'll fill chrome's IO buffer enough to force it to
680    # actually write the data.
681    # See also the comments in the client-side of this test in
682    # download_uitest.cc
683    #
684    size_chunk1 = 35*1024
685    size_chunk2 = 10*1024
686
687    self.send_response(200)
688    self.send_header('Content-Type', 'application/octet-stream')
689    self.send_header('Cache-Control', 'max-age=0')
690    if send_length:
691      self.send_header('Content-Length', size_chunk1 + size_chunk2)
692    self.end_headers()
693
694    # First chunk of data:
695    self.wfile.write("*" * size_chunk1)
696    self.wfile.flush()
697
698    # handle requests until one of them clears this flag.
699    self.server.wait_for_download = True
700    while self.server.wait_for_download:
701      self.server.handle_request()
702
703    # Second chunk of data:
704    self.wfile.write("*" * size_chunk2)
705    return True
706
707  def DownloadFinishHandler(self):
708    """This handler just tells the server to finish the current download."""
709
710    if not self._ShouldHandleRequest("/download-finish"):
711      return False
712
713    self.server.wait_for_download = False
714    self.send_response(200)
715    self.send_header('Content-Type', 'text/html')
716    self.send_header('Cache-Control', 'max-age=0')
717    self.end_headers()
718    return True
719
720  def _ReplaceFileData(self, data, query_parameters):
721    """Replaces matching substrings in a file.
722
723    If the 'replace_text' URL query parameter is present, it is expected to be
724    of the form old_text:new_text, which indicates that any old_text strings in
725    the file are replaced with new_text. Multiple 'replace_text' parameters may
726    be specified.
727
728    If the parameters are not present, |data| is returned.
729    """
730
731    query_dict = cgi.parse_qs(query_parameters)
732    replace_text_values = query_dict.get('replace_text', [])
733    for replace_text_value in replace_text_values:
734      replace_text_args = replace_text_value.split(':')
735      if len(replace_text_args) != 2:
736        raise ValueError(
737          'replace_text must be of form old_text:new_text. Actual value: %s' %
738          replace_text_value)
739      old_text_b64, new_text_b64 = replace_text_args
740      old_text = base64.urlsafe_b64decode(old_text_b64)
741      new_text = base64.urlsafe_b64decode(new_text_b64)
742      data = data.replace(old_text, new_text)
743    return data
744
745  def ZipFileHandler(self):
746    """This handler sends the contents of the requested file in compressed form.
747    Can pass in a parameter that specifies that the content length be
748    C - the compressed size (OK),
749    U - the uncompressed size (Non-standard, but handled),
750    S - less than compressed (OK because we keep going),
751    M - larger than compressed but less than uncompressed (an error),
752    L - larger than uncompressed (an error)
753    Example: compressedfiles/Picture_1.doc?C
754    """
755
756    prefix = "/compressedfiles/"
757    if not self.path.startswith(prefix):
758      return False
759
760    # Consume a request body if present.
761    if self.command == 'POST' or self.command == 'PUT' :
762      self.ReadRequestBody()
763
764    _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
765
766    if not query in ('C', 'U', 'S', 'M', 'L'):
767      return False
768
769    sub_path = url_path[len(prefix):]
770    entries = sub_path.split('/')
771    file_path = os.path.join(self.server.data_dir, *entries)
772    if os.path.isdir(file_path):
773      file_path = os.path.join(file_path, 'index.html')
774
775    if not os.path.isfile(file_path):
776      print "File not found " + sub_path + " full path:" + file_path
777      self.send_error(404)
778      return True
779
780    f = open(file_path, "rb")
781    data = f.read()
782    uncompressed_len = len(data)
783    f.close()
784
785    # Compress the data.
786    data = zlib.compress(data)
787    compressed_len = len(data)
788
789    content_length = compressed_len
790    if query == 'U':
791      content_length = uncompressed_len
792    elif query == 'S':
793      content_length = compressed_len / 2
794    elif query == 'M':
795      content_length = (compressed_len + uncompressed_len) / 2
796    elif query == 'L':
797      content_length = compressed_len + uncompressed_len
798
799    self.send_response(200)
800    self.send_header('Content-Type', 'application/msword')
801    self.send_header('Content-encoding', 'deflate')
802    self.send_header('Connection', 'close')
803    self.send_header('Content-Length', content_length)
804    self.send_header('ETag', '\'' + file_path + '\'')
805    self.end_headers()
806
807    self.wfile.write(data)
808
809    return True
810
811  def FileHandler(self):
812    """This handler sends the contents of the requested file.  Wow, it's like
813    a real webserver!"""
814
815    prefix = self.server.file_root_url
816    if not self.path.startswith(prefix):
817      return False
818    return self._FileHandlerHelper(prefix)
819
820  def PostOnlyFileHandler(self):
821    """This handler sends the contents of the requested file on a POST."""
822
823    prefix = urlparse.urljoin(self.server.file_root_url, 'post/')
824    if not self.path.startswith(prefix):
825      return False
826    return self._FileHandlerHelper(prefix)
827
828  def _FileHandlerHelper(self, prefix):
829    request_body = ''
830    if self.command == 'POST' or self.command == 'PUT':
831      # Consume a request body if present.
832      request_body = self.ReadRequestBody()
833
834    _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
835    query_dict = cgi.parse_qs(query)
836
837    expected_body = query_dict.get('expected_body', [])
838    if expected_body and request_body not in expected_body:
839      self.send_response(404)
840      self.end_headers()
841      self.wfile.write('')
842      return True
843
844    expected_headers = query_dict.get('expected_headers', [])
845    for expected_header in expected_headers:
846      header_name, expected_value = expected_header.split(':')
847      if self.headers.getheader(header_name) != expected_value:
848        self.send_response(404)
849        self.end_headers()
850        self.wfile.write('')
851        return True
852
853    sub_path = url_path[len(prefix):]
854    entries = sub_path.split('/')
855    file_path = os.path.join(self.server.data_dir, *entries)
856    if os.path.isdir(file_path):
857      file_path = os.path.join(file_path, 'index.html')
858
859    if not os.path.isfile(file_path):
860      print "File not found " + sub_path + " full path:" + file_path
861      self.send_error(404)
862      return True
863
864    f = open(file_path, "rb")
865    data = f.read()
866    f.close()
867
868    data = self._ReplaceFileData(data, query)
869
870    old_protocol_version = self.protocol_version
871
872    # If file.mock-http-headers exists, it contains the headers we
873    # should send.  Read them in and parse them.
874    headers_path = file_path + '.mock-http-headers'
875    if os.path.isfile(headers_path):
876      f = open(headers_path, "r")
877
878      # "HTTP/1.1 200 OK"
879      response = f.readline()
880      http_major, http_minor, status_code = re.findall(
881          'HTTP/(\d+).(\d+) (\d+)', response)[0]
882      self.protocol_version = "HTTP/%s.%s" % (http_major, http_minor)
883      self.send_response(int(status_code))
884
885      for line in f:
886        header_values = re.findall('(\S+):\s*(.*)', line)
887        if len(header_values) > 0:
888          # "name: value"
889          name, value = header_values[0]
890          self.send_header(name, value)
891      f.close()
892    else:
893      # Could be more generic once we support mime-type sniffing, but for
894      # now we need to set it explicitly.
895
896      range_header = self.headers.get('Range')
897      if range_header and range_header.startswith('bytes='):
898        # Note this doesn't handle all valid byte range_header values (i.e.
899        # left open ended ones), just enough for what we needed so far.
900        range_header = range_header[6:].split('-')
901        start = int(range_header[0])
902        if range_header[1]:
903          end = int(range_header[1])
904        else:
905          end = len(data) - 1
906
907        self.send_response(206)
908        content_range = ('bytes ' + str(start) + '-' + str(end) + '/' +
909                         str(len(data)))
910        self.send_header('Content-Range', content_range)
911        data = data[start: end + 1]
912      else:
913        self.send_response(200)
914
915      self.send_header('Content-Type', self.GetMIMETypeFromName(file_path))
916      self.send_header('Accept-Ranges', 'bytes')
917      self.send_header('Content-Length', len(data))
918      self.send_header('ETag', '\'' + file_path + '\'')
919    self.end_headers()
920
921    if (self.command != 'HEAD'):
922      self.wfile.write(data)
923
924    self.protocol_version = old_protocol_version
925    return True
926
927  def SetCookieHandler(self):
928    """This handler just sets a cookie, for testing cookie handling."""
929
930    if not self._ShouldHandleRequest("/set-cookie"):
931      return False
932
933    query_char = self.path.find('?')
934    if query_char != -1:
935      cookie_values = self.path[query_char + 1:].split('&')
936    else:
937      cookie_values = ("",)
938    self.send_response(200)
939    self.send_header('Content-Type', 'text/html')
940    for cookie_value in cookie_values:
941      self.send_header('Set-Cookie', '%s' % cookie_value)
942    self.end_headers()
943    for cookie_value in cookie_values:
944      self.wfile.write('%s' % cookie_value)
945    return True
946
947  def SetManyCookiesHandler(self):
948    """This handler just sets a given number of cookies, for testing handling
949       of large numbers of cookies."""
950
951    if not self._ShouldHandleRequest("/set-many-cookies"):
952      return False
953
954    query_char = self.path.find('?')
955    if query_char != -1:
956      num_cookies = int(self.path[query_char + 1:])
957    else:
958      num_cookies = 0
959    self.send_response(200)
960    self.send_header('', 'text/html')
961    for _i in range(0, num_cookies):
962      self.send_header('Set-Cookie', 'a=')
963    self.end_headers()
964    self.wfile.write('%d cookies were sent' % num_cookies)
965    return True
966
967  def ExpectAndSetCookieHandler(self):
968    """Expects some cookies to be sent, and if they are, sets more cookies.
969
970    The expect parameter specifies a required cookie.  May be specified multiple
971    times.
972    The set parameter specifies a cookie to set if all required cookies are
973    preset.  May be specified multiple times.
974    The data parameter specifies the response body data to be returned."""
975
976    if not self._ShouldHandleRequest("/expect-and-set-cookie"):
977      return False
978
979    _, _, _, _, query, _ = urlparse.urlparse(self.path)
980    query_dict = cgi.parse_qs(query)
981    cookies = set()
982    if 'Cookie' in self.headers:
983      cookie_header = self.headers.getheader('Cookie')
984      cookies.update([s.strip() for s in cookie_header.split(';')])
985    got_all_expected_cookies = True
986    for expected_cookie in query_dict.get('expect', []):
987      if expected_cookie not in cookies:
988        got_all_expected_cookies = False
989    self.send_response(200)
990    self.send_header('Content-Type', 'text/html')
991    if got_all_expected_cookies:
992      for cookie_value in query_dict.get('set', []):
993        self.send_header('Set-Cookie', '%s' % cookie_value)
994    self.end_headers()
995    for data_value in query_dict.get('data', []):
996      self.wfile.write(data_value)
997    return True
998
999  def SetHeaderHandler(self):
1000    """This handler sets a response header. Parameters are in the
1001    key%3A%20value&key2%3A%20value2 format."""
1002
1003    if not self._ShouldHandleRequest("/set-header"):
1004      return False
1005
1006    query_char = self.path.find('?')
1007    if query_char != -1:
1008      headers_values = self.path[query_char + 1:].split('&')
1009    else:
1010      headers_values = ("",)
1011    self.send_response(200)
1012    self.send_header('Content-Type', 'text/html')
1013    for header_value in headers_values:
1014      header_value = urllib.unquote(header_value)
1015      (key, value) = header_value.split(': ', 1)
1016      self.send_header(key, value)
1017    self.end_headers()
1018    for header_value in headers_values:
1019      self.wfile.write('%s' % header_value)
1020    return True
1021
1022  def AuthBasicHandler(self):
1023    """This handler tests 'Basic' authentication.  It just sends a page with
1024    title 'user/pass' if you succeed."""
1025
1026    if not self._ShouldHandleRequest("/auth-basic"):
1027      return False
1028
1029    username = userpass = password = b64str = ""
1030    expected_password = 'secret'
1031    realm = 'testrealm'
1032    set_cookie_if_challenged = False
1033
1034    _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1035    query_params = cgi.parse_qs(query, True)
1036    if 'set-cookie-if-challenged' in query_params:
1037      set_cookie_if_challenged = True
1038    if 'password' in query_params:
1039      expected_password = query_params['password'][0]
1040    if 'realm' in query_params:
1041      realm = query_params['realm'][0]
1042
1043    auth = self.headers.getheader('authorization')
1044    try:
1045      if not auth:
1046        raise Exception('no auth')
1047      b64str = re.findall(r'Basic (\S+)', auth)[0]
1048      userpass = base64.b64decode(b64str)
1049      username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
1050      if password != expected_password:
1051        raise Exception('wrong password')
1052    except Exception, e:
1053      # Authentication failed.
1054      self.send_response(401)
1055      self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
1056      self.send_header('Content-Type', 'text/html')
1057      if set_cookie_if_challenged:
1058        self.send_header('Set-Cookie', 'got_challenged=true')
1059      self.end_headers()
1060      self.wfile.write('<html><head>')
1061      self.wfile.write('<title>Denied: %s</title>' % e)
1062      self.wfile.write('</head><body>')
1063      self.wfile.write('auth=%s<p>' % auth)
1064      self.wfile.write('b64str=%s<p>' % b64str)
1065      self.wfile.write('username: %s<p>' % username)
1066      self.wfile.write('userpass: %s<p>' % userpass)
1067      self.wfile.write('password: %s<p>' % password)
1068      self.wfile.write('You sent:<br>%s<p>' % self.headers)
1069      self.wfile.write('</body></html>')
1070      return True
1071
1072    # Authentication successful.  (Return a cachable response to allow for
1073    # testing cached pages that require authentication.)
1074    old_protocol_version = self.protocol_version
1075    self.protocol_version = "HTTP/1.1"
1076
1077    if_none_match = self.headers.getheader('if-none-match')
1078    if if_none_match == "abc":
1079      self.send_response(304)
1080      self.end_headers()
1081    elif url_path.endswith(".gif"):
1082      # Using chrome/test/data/google/logo.gif as the test image
1083      test_image_path = ['google', 'logo.gif']
1084      gif_path = os.path.join(self.server.data_dir, *test_image_path)
1085      if not os.path.isfile(gif_path):
1086        self.send_error(404)
1087        self.protocol_version = old_protocol_version
1088        return True
1089
1090      f = open(gif_path, "rb")
1091      data = f.read()
1092      f.close()
1093
1094      self.send_response(200)
1095      self.send_header('Content-Type', 'image/gif')
1096      self.send_header('Cache-control', 'max-age=60000')
1097      self.send_header('Etag', 'abc')
1098      self.end_headers()
1099      self.wfile.write(data)
1100    else:
1101      self.send_response(200)
1102      self.send_header('Content-Type', 'text/html')
1103      self.send_header('Cache-control', 'max-age=60000')
1104      self.send_header('Etag', 'abc')
1105      self.end_headers()
1106      self.wfile.write('<html><head>')
1107      self.wfile.write('<title>%s/%s</title>' % (username, password))
1108      self.wfile.write('</head><body>')
1109      self.wfile.write('auth=%s<p>' % auth)
1110      self.wfile.write('You sent:<br>%s<p>' % self.headers)
1111      self.wfile.write('</body></html>')
1112
1113    self.protocol_version = old_protocol_version
1114    return True
1115
1116  def GetNonce(self, force_reset=False):
1117    """Returns a nonce that's stable per request path for the server's lifetime.
1118    This is a fake implementation. A real implementation would only use a given
1119    nonce a single time (hence the name n-once). However, for the purposes of
1120    unittesting, we don't care about the security of the nonce.
1121
1122    Args:
1123      force_reset: Iff set, the nonce will be changed. Useful for testing the
1124          "stale" response.
1125    """
1126
1127    if force_reset or not self.server.nonce_time:
1128      self.server.nonce_time = time.time()
1129    return hashlib.md5('privatekey%s%d' %
1130                       (self.path, self.server.nonce_time)).hexdigest()
1131
1132  def AuthDigestHandler(self):
1133    """This handler tests 'Digest' authentication.
1134
1135    It just sends a page with title 'user/pass' if you succeed.
1136
1137    A stale response is sent iff "stale" is present in the request path.
1138    """
1139
1140    if not self._ShouldHandleRequest("/auth-digest"):
1141      return False
1142
1143    stale = 'stale' in self.path
1144    nonce = self.GetNonce(force_reset=stale)
1145    opaque = hashlib.md5('opaque').hexdigest()
1146    password = 'secret'
1147    realm = 'testrealm'
1148
1149    auth = self.headers.getheader('authorization')
1150    pairs = {}
1151    try:
1152      if not auth:
1153        raise Exception('no auth')
1154      if not auth.startswith('Digest'):
1155        raise Exception('not digest')
1156      # Pull out all the name="value" pairs as a dictionary.
1157      pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1158
1159      # Make sure it's all valid.
1160      if pairs['nonce'] != nonce:
1161        raise Exception('wrong nonce')
1162      if pairs['opaque'] != opaque:
1163        raise Exception('wrong opaque')
1164
1165      # Check the 'response' value and make sure it matches our magic hash.
1166      # See http://www.ietf.org/rfc/rfc2617.txt
1167      hash_a1 = hashlib.md5(
1168          ':'.join([pairs['username'], realm, password])).hexdigest()
1169      hash_a2 = hashlib.md5(':'.join([self.command, pairs['uri']])).hexdigest()
1170      if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
1171        response = hashlib.md5(':'.join([hash_a1, nonce, pairs['nc'],
1172            pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1173      else:
1174        response = hashlib.md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
1175
1176      if pairs['response'] != response:
1177        raise Exception('wrong password')
1178    except Exception, e:
1179      # Authentication failed.
1180      self.send_response(401)
1181      hdr = ('Digest '
1182             'realm="%s", '
1183             'domain="/", '
1184             'qop="auth", '
1185             'algorithm=MD5, '
1186             'nonce="%s", '
1187             'opaque="%s"') % (realm, nonce, opaque)
1188      if stale:
1189        hdr += ', stale="TRUE"'
1190      self.send_header('WWW-Authenticate', hdr)
1191      self.send_header('Content-Type', 'text/html')
1192      self.end_headers()
1193      self.wfile.write('<html><head>')
1194      self.wfile.write('<title>Denied: %s</title>' % e)
1195      self.wfile.write('</head><body>')
1196      self.wfile.write('auth=%s<p>' % auth)
1197      self.wfile.write('pairs=%s<p>' % pairs)
1198      self.wfile.write('You sent:<br>%s<p>' % self.headers)
1199      self.wfile.write('We are replying:<br>%s<p>' % hdr)
1200      self.wfile.write('</body></html>')
1201      return True
1202
1203    # Authentication successful.
1204    self.send_response(200)
1205    self.send_header('Content-Type', 'text/html')
1206    self.end_headers()
1207    self.wfile.write('<html><head>')
1208    self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1209    self.wfile.write('</head><body>')
1210    self.wfile.write('auth=%s<p>' % auth)
1211    self.wfile.write('pairs=%s<p>' % pairs)
1212    self.wfile.write('</body></html>')
1213
1214    return True
1215
1216  def SlowServerHandler(self):
1217    """Wait for the user suggested time before responding. The syntax is
1218    /slow?0.5 to wait for half a second."""
1219
1220    if not self._ShouldHandleRequest("/slow"):
1221      return False
1222    query_char = self.path.find('?')
1223    wait_sec = 1.0
1224    if query_char >= 0:
1225      try:
1226        wait_sec = int(self.path[query_char + 1:])
1227      except ValueError:
1228        pass
1229    time.sleep(wait_sec)
1230    self.send_response(200)
1231    self.send_header('Content-Type', 'text/plain')
1232    self.end_headers()
1233    self.wfile.write("waited %d seconds" % wait_sec)
1234    return True
1235
1236  def ChunkedServerHandler(self):
1237    """Send chunked response. Allows to specify chunks parameters:
1238     - waitBeforeHeaders - ms to wait before sending headers
1239     - waitBetweenChunks - ms to wait between chunks
1240     - chunkSize - size of each chunk in bytes
1241     - chunksNumber - number of chunks
1242    Example: /chunked?waitBeforeHeaders=1000&chunkSize=5&chunksNumber=5
1243    waits one second, then sends headers and five chunks five bytes each."""
1244
1245    if not self._ShouldHandleRequest("/chunked"):
1246      return False
1247    query_char = self.path.find('?')
1248    chunkedSettings = {'waitBeforeHeaders' : 0,
1249                       'waitBetweenChunks' : 0,
1250                       'chunkSize' : 5,
1251                       'chunksNumber' : 5}
1252    if query_char >= 0:
1253      params = self.path[query_char + 1:].split('&')
1254      for param in params:
1255        keyValue = param.split('=')
1256        if len(keyValue) == 2:
1257          try:
1258            chunkedSettings[keyValue[0]] = int(keyValue[1])
1259          except ValueError:
1260            pass
1261    time.sleep(0.001 * chunkedSettings['waitBeforeHeaders'])
1262    self.protocol_version = 'HTTP/1.1' # Needed for chunked encoding
1263    self.send_response(200)
1264    self.send_header('Content-Type', 'text/plain')
1265    self.send_header('Connection', 'close')
1266    self.send_header('Transfer-Encoding', 'chunked')
1267    self.end_headers()
1268    # Chunked encoding: sending all chunks, then final zero-length chunk and
1269    # then final CRLF.
1270    for i in range(0, chunkedSettings['chunksNumber']):
1271      if i > 0:
1272        time.sleep(0.001 * chunkedSettings['waitBetweenChunks'])
1273      self.sendChunkHelp('*' * chunkedSettings['chunkSize'])
1274      self.wfile.flush() # Keep in mind that we start flushing only after 1kb.
1275    self.sendChunkHelp('')
1276    return True
1277
1278  def ContentTypeHandler(self):
1279    """Returns a string of html with the given content type.  E.g.,
1280    /contenttype?text/css returns an html file with the Content-Type
1281    header set to text/css."""
1282
1283    if not self._ShouldHandleRequest("/contenttype"):
1284      return False
1285    query_char = self.path.find('?')
1286    content_type = self.path[query_char + 1:].strip()
1287    if not content_type:
1288      content_type = 'text/html'
1289    self.send_response(200)
1290    self.send_header('Content-Type', content_type)
1291    self.end_headers()
1292    self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n")
1293    return True
1294
1295  def NoContentHandler(self):
1296    """Returns a 204 No Content response."""
1297
1298    if not self._ShouldHandleRequest("/nocontent"):
1299      return False
1300    self.send_response(204)
1301    self.end_headers()
1302    return True
1303
1304  def ServerRedirectHandler(self):
1305    """Sends a server redirect to the given URL. The syntax is
1306    '/server-redirect?http://foo.bar/asdf' to redirect to
1307    'http://foo.bar/asdf'"""
1308
1309    test_name = "/server-redirect"
1310    if not self._ShouldHandleRequest(test_name):
1311      return False
1312
1313    query_char = self.path.find('?')
1314    if query_char < 0 or len(self.path) <= query_char + 1:
1315      self.sendRedirectHelp(test_name)
1316      return True
1317    dest = self.path[query_char + 1:]
1318
1319    self.send_response(301)  # moved permanently
1320    self.send_header('Location', dest)
1321    self.send_header('Content-Type', 'text/html')
1322    self.end_headers()
1323    self.wfile.write('<html><head>')
1324    self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1325
1326    return True
1327
1328  def ClientRedirectHandler(self):
1329    """Sends a client redirect to the given URL. The syntax is
1330    '/client-redirect?http://foo.bar/asdf' to redirect to
1331    'http://foo.bar/asdf'"""
1332
1333    test_name = "/client-redirect"
1334    if not self._ShouldHandleRequest(test_name):
1335      return False
1336
1337    query_char = self.path.find('?')
1338    if query_char < 0 or len(self.path) <= query_char + 1:
1339      self.sendRedirectHelp(test_name)
1340      return True
1341    dest = self.path[query_char + 1:]
1342
1343    self.send_response(200)
1344    self.send_header('Content-Type', 'text/html')
1345    self.end_headers()
1346    self.wfile.write('<html><head>')
1347    self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1348    self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1349
1350    return True
1351
1352  def MultipartHandler(self):
1353    """Send a multipart response (10 text/html pages)."""
1354
1355    test_name = '/multipart'
1356    if not self._ShouldHandleRequest(test_name):
1357      return False
1358
1359    num_frames = 10
1360    bound = '12345'
1361    self.send_response(200)
1362    self.send_header('Content-Type',
1363                     'multipart/x-mixed-replace;boundary=' + bound)
1364    self.end_headers()
1365
1366    for i in xrange(num_frames):
1367      self.wfile.write('--' + bound + '\r\n')
1368      self.wfile.write('Content-Type: text/html\r\n\r\n')
1369      self.wfile.write('<title>page ' + str(i) + '</title>')
1370      self.wfile.write('page ' + str(i))
1371
1372    self.wfile.write('--' + bound + '--')
1373    return True
1374
1375  def GetSSLSessionCacheHandler(self):
1376    """Send a reply containing a log of the session cache operations."""
1377
1378    if not self._ShouldHandleRequest('/ssl-session-cache'):
1379      return False
1380
1381    self.send_response(200)
1382    self.send_header('Content-Type', 'text/plain')
1383    self.end_headers()
1384    try:
1385      for (action, sessionID) in self.server.session_cache.log:
1386        self.wfile.write('%s\t%s\n' % (action, sessionID.encode('hex')))
1387    except AttributeError:
1388      self.wfile.write('Pass --https-record-resume in order to use' +
1389                       ' this request')
1390    return True
1391
1392  def SSLManySmallRecords(self):
1393    """Sends a reply consisting of a variety of small writes. These will be
1394    translated into a series of small SSL records when used over an HTTPS
1395    server."""
1396
1397    if not self._ShouldHandleRequest('/ssl-many-small-records'):
1398      return False
1399
1400    self.send_response(200)
1401    self.send_header('Content-Type', 'text/plain')
1402    self.end_headers()
1403
1404    # Write ~26K of data, in 1350 byte chunks
1405    for i in xrange(20):
1406      self.wfile.write('*' * 1350)
1407      self.wfile.flush()
1408    return True
1409
1410  def GetChannelID(self):
1411    """Send a reply containing the hashed ChannelID that the client provided."""
1412
1413    if not self._ShouldHandleRequest('/channel-id'):
1414      return False
1415
1416    self.send_response(200)
1417    self.send_header('Content-Type', 'text/plain')
1418    self.end_headers()
1419    channel_id = self.server.tlsConnection.channel_id.tostring()
1420    self.wfile.write(hashlib.sha256(channel_id).digest().encode('base64'))
1421    return True
1422
1423  def CloseSocketHandler(self):
1424    """Closes the socket without sending anything."""
1425
1426    if not self._ShouldHandleRequest('/close-socket'):
1427      return False
1428
1429    self.wfile.close()
1430    return True
1431
1432  def RangeResetHandler(self):
1433    """Send data broken up by connection resets every N (default 4K) bytes.
1434    Support range requests.  If the data requested doesn't straddle a reset
1435    boundary, it will all be sent.  Used for testing resuming downloads."""
1436
1437    def DataForRange(start, end):
1438      """Data to be provided for a particular range of bytes."""
1439      # Offset and scale to avoid too obvious (and hence potentially
1440      # collidable) data.
1441      return ''.join([chr(y % 256)
1442                      for y in range(start * 2 + 15, end * 2 + 15, 2)])
1443
1444    if not self._ShouldHandleRequest('/rangereset'):
1445      return False
1446
1447    _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1448
1449    # Defaults
1450    size = 8000
1451    # Note that the rst is sent just before sending the rst_boundary byte.
1452    rst_boundary = 4000
1453    respond_to_range = True
1454    hold_for_signal = False
1455    rst_limit = -1
1456    token = 'DEFAULT'
1457    fail_precondition = 0
1458    send_verifiers = True
1459
1460    # Parse the query
1461    qdict = urlparse.parse_qs(query, True)
1462    if 'size' in qdict:
1463      size = int(qdict['size'][0])
1464    if 'rst_boundary' in qdict:
1465      rst_boundary = int(qdict['rst_boundary'][0])
1466    if 'token' in qdict:
1467      # Identifying token for stateful tests.
1468      token = qdict['token'][0]
1469    if 'rst_limit' in qdict:
1470      # Max number of rsts for a given token.
1471      rst_limit = int(qdict['rst_limit'][0])
1472    if 'bounce_range' in qdict:
1473      respond_to_range = False
1474    if 'hold' in qdict:
1475      # Note that hold_for_signal will not work with null range requests;
1476      # see TODO below.
1477      hold_for_signal = True
1478    if 'no_verifiers' in qdict:
1479      send_verifiers = False
1480    if 'fail_precondition' in qdict:
1481      fail_precondition = int(qdict['fail_precondition'][0])
1482
1483    # Record already set information, or set it.
1484    rst_limit = TestPageHandler.rst_limits.setdefault(token, rst_limit)
1485    if rst_limit != 0:
1486      TestPageHandler.rst_limits[token] -= 1
1487    fail_precondition = TestPageHandler.fail_precondition.setdefault(
1488      token, fail_precondition)
1489    if fail_precondition != 0:
1490      TestPageHandler.fail_precondition[token] -= 1
1491
1492    first_byte = 0
1493    last_byte = size - 1
1494
1495    # Does that define what we want to return, or do we need to apply
1496    # a range?
1497    range_response = False
1498    range_header = self.headers.getheader('range')
1499    if range_header and respond_to_range:
1500      mo = re.match("bytes=(\d*)-(\d*)", range_header)
1501      if mo.group(1):
1502        first_byte = int(mo.group(1))
1503      if mo.group(2):
1504        last_byte = int(mo.group(2))
1505      if last_byte > size - 1:
1506        last_byte = size - 1
1507      range_response = True
1508      if last_byte < first_byte:
1509        return False
1510
1511    if (fail_precondition and
1512        (self.headers.getheader('If-Modified-Since') or
1513         self.headers.getheader('If-Match'))):
1514      self.send_response(412)
1515      self.end_headers()
1516      return True
1517
1518    if range_response:
1519      self.send_response(206)
1520      self.send_header('Content-Range',
1521                       'bytes %d-%d/%d' % (first_byte, last_byte, size))
1522    else:
1523      self.send_response(200)
1524    self.send_header('Content-Type', 'application/octet-stream')
1525    self.send_header('Content-Length', last_byte - first_byte + 1)
1526    if send_verifiers:
1527      self.send_header('Etag', '"XYZZY"')
1528      self.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST')
1529    self.end_headers()
1530
1531    if hold_for_signal:
1532      # TODO(rdsmith/phajdan.jr): http://crbug.com/169519: Without writing
1533      # a single byte, the self.server.handle_request() below hangs
1534      # without processing new incoming requests.
1535      self.wfile.write(DataForRange(first_byte, first_byte + 1))
1536      first_byte = first_byte + 1
1537      # handle requests until one of them clears this flag.
1538      self.server.wait_for_download = True
1539      while self.server.wait_for_download:
1540        self.server.handle_request()
1541
1542    possible_rst = ((first_byte / rst_boundary) + 1) * rst_boundary
1543    if possible_rst >= last_byte or rst_limit == 0:
1544      # No RST has been requested in this range, so we don't need to
1545      # do anything fancy; just write the data and let the python
1546      # infrastructure close the connection.
1547      self.wfile.write(DataForRange(first_byte, last_byte + 1))
1548      self.wfile.flush()
1549      return True
1550
1551    # We're resetting the connection part way in; go to the RST
1552    # boundary and then send an RST.
1553    # Because socket semantics do not guarantee that all the data will be
1554    # sent when using the linger semantics to hard close a socket,
1555    # we send the data and then wait for our peer to release us
1556    # before sending the reset.
1557    data = DataForRange(first_byte, possible_rst)
1558    self.wfile.write(data)
1559    self.wfile.flush()
1560    self.server.wait_for_download = True
1561    while self.server.wait_for_download:
1562      self.server.handle_request()
1563    l_onoff = 1  # Linger is active.
1564    l_linger = 0  # Seconds to linger for.
1565    self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
1566                 struct.pack('ii', l_onoff, l_linger))
1567
1568    # Close all duplicates of the underlying socket to force the RST.
1569    self.wfile.close()
1570    self.rfile.close()
1571    self.connection.close()
1572
1573    return True
1574
1575  def DefaultResponseHandler(self):
1576    """This is the catch-all response handler for requests that aren't handled
1577    by one of the special handlers above.
1578    Note that we specify the content-length as without it the https connection
1579    is not closed properly (and the browser keeps expecting data)."""
1580
1581    contents = "Default response given for path: " + self.path
1582    self.send_response(200)
1583    self.send_header('Content-Type', 'text/html')
1584    self.send_header('Content-Length', len(contents))
1585    self.end_headers()
1586    if (self.command != 'HEAD'):
1587      self.wfile.write(contents)
1588    return True
1589
1590  def RedirectConnectHandler(self):
1591    """Sends a redirect to the CONNECT request for www.redirect.com. This
1592    response is not specified by the RFC, so the browser should not follow
1593    the redirect."""
1594
1595    if (self.path.find("www.redirect.com") < 0):
1596      return False
1597
1598    dest = "http://www.destination.com/foo.js"
1599
1600    self.send_response(302)  # moved temporarily
1601    self.send_header('Location', dest)
1602    self.send_header('Connection', 'close')
1603    self.end_headers()
1604    return True
1605
1606  def ServerAuthConnectHandler(self):
1607    """Sends a 401 to the CONNECT request for www.server-auth.com. This
1608    response doesn't make sense because the proxy server cannot request
1609    server authentication."""
1610
1611    if (self.path.find("www.server-auth.com") < 0):
1612      return False
1613
1614    challenge = 'Basic realm="WallyWorld"'
1615
1616    self.send_response(401)  # unauthorized
1617    self.send_header('WWW-Authenticate', challenge)
1618    self.send_header('Connection', 'close')
1619    self.end_headers()
1620    return True
1621
1622  def DefaultConnectResponseHandler(self):
1623    """This is the catch-all response handler for CONNECT requests that aren't
1624    handled by one of the special handlers above.  Real Web servers respond
1625    with 400 to CONNECT requests."""
1626
1627    contents = "Your client has issued a malformed or illegal request."
1628    self.send_response(400)  # bad request
1629    self.send_header('Content-Type', 'text/html')
1630    self.send_header('Content-Length', len(contents))
1631    self.end_headers()
1632    self.wfile.write(contents)
1633    return True
1634
1635  # called by the redirect handling function when there is no parameter
1636  def sendRedirectHelp(self, redirect_name):
1637    self.send_response(200)
1638    self.send_header('Content-Type', 'text/html')
1639    self.end_headers()
1640    self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1641    self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1642    self.wfile.write('</body></html>')
1643
1644  # called by chunked handling function
1645  def sendChunkHelp(self, chunk):
1646    # Each chunk consists of: chunk size (hex), CRLF, chunk body, CRLF
1647    self.wfile.write('%X\r\n' % len(chunk))
1648    self.wfile.write(chunk)
1649    self.wfile.write('\r\n')
1650
1651
1652class OCSPHandler(testserver_base.BasePageHandler):
1653  def __init__(self, request, client_address, socket_server):
1654    handlers = [self.OCSPResponse]
1655    self.ocsp_response = socket_server.ocsp_response
1656    testserver_base.BasePageHandler.__init__(self, request, client_address,
1657                                             socket_server, [], handlers, [],
1658                                             handlers, [])
1659
1660  def OCSPResponse(self):
1661    self.send_response(200)
1662    self.send_header('Content-Type', 'application/ocsp-response')
1663    self.send_header('Content-Length', str(len(self.ocsp_response)))
1664    self.end_headers()
1665
1666    self.wfile.write(self.ocsp_response)
1667
1668
1669class TCPEchoHandler(SocketServer.BaseRequestHandler):
1670  """The RequestHandler class for TCP echo server.
1671
1672  It is instantiated once per connection to the server, and overrides the
1673  handle() method to implement communication to the client.
1674  """
1675
1676  def handle(self):
1677    """Handles the request from the client and constructs a response."""
1678
1679    data = self.request.recv(65536).strip()
1680    # Verify the "echo request" message received from the client. Send back
1681    # "echo response" message if "echo request" message is valid.
1682    try:
1683      return_data = echo_message.GetEchoResponseData(data)
1684      if not return_data:
1685        return
1686    except ValueError:
1687      return
1688
1689    self.request.send(return_data)
1690
1691
1692class UDPEchoHandler(SocketServer.BaseRequestHandler):
1693  """The RequestHandler class for UDP echo server.
1694
1695  It is instantiated once per connection to the server, and overrides the
1696  handle() method to implement communication to the client.
1697  """
1698
1699  def handle(self):
1700    """Handles the request from the client and constructs a response."""
1701
1702    data = self.request[0].strip()
1703    request_socket = self.request[1]
1704    # Verify the "echo request" message received from the client. Send back
1705    # "echo response" message if "echo request" message is valid.
1706    try:
1707      return_data = echo_message.GetEchoResponseData(data)
1708      if not return_data:
1709        return
1710    except ValueError:
1711      return
1712    request_socket.sendto(return_data, self.client_address)
1713
1714
1715class BasicAuthProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
1716  """A request handler that behaves as a proxy server which requires
1717  basic authentication. Only CONNECT, GET and HEAD is supported for now.
1718  """
1719
1720  _AUTH_CREDENTIAL = 'Basic Zm9vOmJhcg==' # foo:bar
1721
1722  def parse_request(self):
1723    """Overrides parse_request to check credential."""
1724
1725    if not BaseHTTPServer.BaseHTTPRequestHandler.parse_request(self):
1726      return False
1727
1728    auth = self.headers.getheader('Proxy-Authorization')
1729    if auth != self._AUTH_CREDENTIAL:
1730      self.send_response(407)
1731      self.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"')
1732      self.end_headers()
1733      return False
1734
1735    return True
1736
1737  def _start_read_write(self, sock):
1738    sock.setblocking(0)
1739    self.request.setblocking(0)
1740    rlist = [self.request, sock]
1741    while True:
1742      ready_sockets, _unused, errors = select.select(rlist, [], [])
1743      if errors:
1744        self.send_response(500)
1745        self.end_headers()
1746        return
1747      for s in ready_sockets:
1748        received = s.recv(1024)
1749        if len(received) == 0:
1750          return
1751        if s == self.request:
1752          other = sock
1753        else:
1754          other = self.request
1755        other.send(received)
1756
1757  def _do_common_method(self):
1758    url = urlparse.urlparse(self.path)
1759    port = url.port
1760    if not port:
1761      if url.scheme == 'http':
1762        port = 80
1763      elif url.scheme == 'https':
1764        port = 443
1765    if not url.hostname or not port:
1766      self.send_response(400)
1767      self.end_headers()
1768      return
1769
1770    if len(url.path) == 0:
1771      path = '/'
1772    else:
1773      path = url.path
1774    if len(url.query) > 0:
1775      path = '%s?%s' % (url.path, url.query)
1776
1777    sock = None
1778    try:
1779      sock = socket.create_connection((url.hostname, port))
1780      sock.send('%s %s %s\r\n' % (
1781          self.command, path, self.protocol_version))
1782      for header in self.headers.headers:
1783        header = header.strip()
1784        if (header.lower().startswith('connection') or
1785            header.lower().startswith('proxy')):
1786          continue
1787        sock.send('%s\r\n' % header)
1788      sock.send('\r\n')
1789      self._start_read_write(sock)
1790    except Exception:
1791      self.send_response(500)
1792      self.end_headers()
1793    finally:
1794      if sock is not None:
1795        sock.close()
1796
1797  def do_CONNECT(self):
1798    try:
1799      pos = self.path.rfind(':')
1800      host = self.path[:pos]
1801      port = int(self.path[pos+1:])
1802    except Exception:
1803      self.send_response(400)
1804      self.end_headers()
1805
1806    try:
1807      sock = socket.create_connection((host, port))
1808      self.send_response(200, 'Connection established')
1809      self.end_headers()
1810      self._start_read_write(sock)
1811    except Exception:
1812      self.send_response(500)
1813      self.end_headers()
1814    finally:
1815      sock.close()
1816
1817  def do_GET(self):
1818    self._do_common_method()
1819
1820  def do_HEAD(self):
1821    self._do_common_method()
1822
1823
1824class ServerRunner(testserver_base.TestServerRunner):
1825  """TestServerRunner for the net test servers."""
1826
1827  def __init__(self):
1828    super(ServerRunner, self).__init__()
1829    self.__ocsp_server = None
1830
1831  def __make_data_dir(self):
1832    if self.options.data_dir:
1833      if not os.path.isdir(self.options.data_dir):
1834        raise testserver_base.OptionError('specified data dir not found: ' +
1835            self.options.data_dir + ' exiting...')
1836      my_data_dir = self.options.data_dir
1837    else:
1838      # Create the default path to our data dir, relative to the exe dir.
1839      my_data_dir = os.path.join(BASE_DIR, "..", "..", "..", "..",
1840                                 "test", "data")
1841
1842      #TODO(ibrar): Must use Find* funtion defined in google\tools
1843      #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1844
1845    return my_data_dir
1846
1847  def create_server(self, server_data):
1848    port = self.options.port
1849    host = self.options.host
1850
1851    if self.options.server_type == SERVER_HTTP:
1852      if self.options.https:
1853        pem_cert_and_key = None
1854        if self.options.cert_and_key_file:
1855          if not os.path.isfile(self.options.cert_and_key_file):
1856            raise testserver_base.OptionError(
1857                'specified server cert file not found: ' +
1858                self.options.cert_and_key_file + ' exiting...')
1859          pem_cert_and_key = file(self.options.cert_and_key_file, 'r').read()
1860        else:
1861          # generate a new certificate and run an OCSP server for it.
1862          self.__ocsp_server = OCSPServer((host, 0), OCSPHandler)
1863          print ('OCSP server started on %s:%d...' %
1864              (host, self.__ocsp_server.server_port))
1865
1866          ocsp_der = None
1867          ocsp_state = None
1868
1869          if self.options.ocsp == 'ok':
1870            ocsp_state = minica.OCSP_STATE_GOOD
1871          elif self.options.ocsp == 'revoked':
1872            ocsp_state = minica.OCSP_STATE_REVOKED
1873          elif self.options.ocsp == 'invalid':
1874            ocsp_state = minica.OCSP_STATE_INVALID
1875          elif self.options.ocsp == 'unauthorized':
1876            ocsp_state = minica.OCSP_STATE_UNAUTHORIZED
1877          elif self.options.ocsp == 'unknown':
1878            ocsp_state = minica.OCSP_STATE_UNKNOWN
1879          else:
1880            raise testserver_base.OptionError('unknown OCSP status: ' +
1881                self.options.ocsp_status)
1882
1883          (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP(
1884              subject = "127.0.0.1",
1885              ocsp_url = ("http://%s:%d/ocsp" %
1886                  (host, self.__ocsp_server.server_port)),
1887              ocsp_state = ocsp_state)
1888
1889          self.__ocsp_server.ocsp_response = ocsp_der
1890
1891        for ca_cert in self.options.ssl_client_ca:
1892          if not os.path.isfile(ca_cert):
1893            raise testserver_base.OptionError(
1894                'specified trusted client CA file not found: ' + ca_cert +
1895                ' exiting...')
1896        server = HTTPSServer((host, port), TestPageHandler, pem_cert_and_key,
1897                             self.options.ssl_client_auth,
1898                             self.options.ssl_client_ca,
1899                             self.options.ssl_bulk_cipher,
1900                             self.options.record_resume,
1901                             self.options.tls_intolerant)
1902        print 'HTTPS server started on %s:%d...' % (host, server.server_port)
1903      else:
1904        server = HTTPServer((host, port), TestPageHandler)
1905        print 'HTTP server started on %s:%d...' % (host, server.server_port)
1906
1907      server.data_dir = self.__make_data_dir()
1908      server.file_root_url = self.options.file_root_url
1909      server_data['port'] = server.server_port
1910    elif self.options.server_type == SERVER_WEBSOCKET:
1911      # Launch pywebsocket via WebSocketServer.
1912      logger = logging.getLogger()
1913      logger.addHandler(logging.StreamHandler())
1914      # TODO(toyoshim): Remove following os.chdir. Currently this operation
1915      # is required to work correctly. It should be fixed from pywebsocket side.
1916      os.chdir(self.__make_data_dir())
1917      websocket_options = WebSocketOptions(host, port, '.')
1918      if self.options.cert_and_key_file:
1919        websocket_options.use_tls = True
1920        websocket_options.private_key = self.options.cert_and_key_file
1921        websocket_options.certificate = self.options.cert_and_key_file
1922      if self.options.ssl_client_auth:
1923        websocket_options.tls_client_auth = True
1924        if len(self.options.ssl_client_ca) != 1:
1925          raise testserver_base.OptionError(
1926              'one trusted client CA file should be specified')
1927        if not os.path.isfile(self.options.ssl_client_ca[0]):
1928          raise testserver_base.OptionError(
1929              'specified trusted client CA file not found: ' +
1930              self.options.ssl_client_ca[0] + ' exiting...')
1931        websocket_options.tls_client_ca = self.options.ssl_client_ca[0]
1932      server = WebSocketServer(websocket_options)
1933      print 'WebSocket server started on %s:%d...' % (host, server.server_port)
1934      server_data['port'] = server.server_port
1935    elif self.options.server_type == SERVER_TCP_ECHO:
1936      # Used for generating the key (randomly) that encodes the "echo request"
1937      # message.
1938      random.seed()
1939      server = TCPEchoServer((host, port), TCPEchoHandler)
1940      print 'Echo TCP server started on port %d...' % server.server_port
1941      server_data['port'] = server.server_port
1942    elif self.options.server_type == SERVER_UDP_ECHO:
1943      # Used for generating the key (randomly) that encodes the "echo request"
1944      # message.
1945      random.seed()
1946      server = UDPEchoServer((host, port), UDPEchoHandler)
1947      print 'Echo UDP server started on port %d...' % server.server_port
1948      server_data['port'] = server.server_port
1949    elif self.options.server_type == SERVER_BASIC_AUTH_PROXY:
1950      server = HTTPServer((host, port), BasicAuthProxyRequestHandler)
1951      print 'BasicAuthProxy server started on port %d...' % server.server_port
1952      server_data['port'] = server.server_port
1953    elif self.options.server_type == SERVER_FTP:
1954      my_data_dir = self.__make_data_dir()
1955
1956      # Instantiate a dummy authorizer for managing 'virtual' users
1957      authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1958
1959      # Define a new user having full r/w permissions and a read-only
1960      # anonymous user
1961      authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1962
1963      authorizer.add_anonymous(my_data_dir)
1964
1965      # Instantiate FTP handler class
1966      ftp_handler = pyftpdlib.ftpserver.FTPHandler
1967      ftp_handler.authorizer = authorizer
1968
1969      # Define a customized banner (string returned when client connects)
1970      ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1971                            pyftpdlib.ftpserver.__ver__)
1972
1973      # Instantiate FTP server class and listen to address:port
1974      server = pyftpdlib.ftpserver.FTPServer((host, port), ftp_handler)
1975      server_data['port'] = server.socket.getsockname()[1]
1976      print 'FTP server started on port %d...' % server_data['port']
1977    else:
1978      raise testserver_base.OptionError('unknown server type' +
1979          self.options.server_type)
1980
1981    return server
1982
1983  def run_server(self):
1984    if self.__ocsp_server:
1985      self.__ocsp_server.serve_forever_on_thread()
1986
1987    testserver_base.TestServerRunner.run_server(self)
1988
1989    if self.__ocsp_server:
1990      self.__ocsp_server.stop_serving()
1991
1992  def add_options(self):
1993    testserver_base.TestServerRunner.add_options(self)
1994    self.option_parser.add_option('-f', '--ftp', action='store_const',
1995                                  const=SERVER_FTP, default=SERVER_HTTP,
1996                                  dest='server_type',
1997                                  help='start up an FTP server.')
1998    self.option_parser.add_option('--tcp-echo', action='store_const',
1999                                  const=SERVER_TCP_ECHO, default=SERVER_HTTP,
2000                                  dest='server_type',
2001                                  help='start up a tcp echo server.')
2002    self.option_parser.add_option('--udp-echo', action='store_const',
2003                                  const=SERVER_UDP_ECHO, default=SERVER_HTTP,
2004                                  dest='server_type',
2005                                  help='start up a udp echo server.')
2006    self.option_parser.add_option('--basic-auth-proxy', action='store_const',
2007                                  const=SERVER_BASIC_AUTH_PROXY,
2008                                  default=SERVER_HTTP, dest='server_type',
2009                                  help='start up a proxy server which requires '
2010                                  'basic authentication.')
2011    self.option_parser.add_option('--websocket', action='store_const',
2012                                  const=SERVER_WEBSOCKET, default=SERVER_HTTP,
2013                                  dest='server_type',
2014                                  help='start up a WebSocket server.')
2015    self.option_parser.add_option('--https', action='store_true',
2016                                  dest='https', help='Specify that https '
2017                                  'should be used.')
2018    self.option_parser.add_option('--cert-and-key-file',
2019                                  dest='cert_and_key_file', help='specify the '
2020                                  'path to the file containing the certificate '
2021                                  'and private key for the server in PEM '
2022                                  'format')
2023    self.option_parser.add_option('--ocsp', dest='ocsp', default='ok',
2024                                  help='The type of OCSP response generated '
2025                                  'for the automatically generated '
2026                                  'certificate. One of [ok,revoked,invalid]')
2027    self.option_parser.add_option('--tls-intolerant', dest='tls_intolerant',
2028                                  default='0', type='int',
2029                                  help='If nonzero, certain TLS connections '
2030                                  'will be aborted in order to test version '
2031                                  'fallback. 1 means all TLS versions will be '
2032                                  'aborted. 2 means TLS 1.1 or higher will be '
2033                                  'aborted. 3 means TLS 1.2 or higher will be '
2034                                  'aborted.')
2035    self.option_parser.add_option('--https-record-resume',
2036                                  dest='record_resume', const=True,
2037                                  default=False, action='store_const',
2038                                  help='Record resumption cache events rather '
2039                                  'than resuming as normal. Allows the use of '
2040                                  'the /ssl-session-cache request')
2041    self.option_parser.add_option('--ssl-client-auth', action='store_true',
2042                                  help='Require SSL client auth on every '
2043                                  'connection.')
2044    self.option_parser.add_option('--ssl-client-ca', action='append',
2045                                  default=[], help='Specify that the client '
2046                                  'certificate request should include the CA '
2047                                  'named in the subject of the DER-encoded '
2048                                  'certificate contained in the specified '
2049                                  'file. This option may appear multiple '
2050                                  'times, indicating multiple CA names should '
2051                                  'be sent in the request.')
2052    self.option_parser.add_option('--ssl-bulk-cipher', action='append',
2053                                  help='Specify the bulk encryption '
2054                                  'algorithm(s) that will be accepted by the '
2055                                  'SSL server. Valid values are "aes256", '
2056                                  '"aes128", "3des", "rc4". If omitted, all '
2057                                  'algorithms will be used. This option may '
2058                                  'appear multiple times, indicating '
2059                                  'multiple algorithms should be enabled.');
2060    self.option_parser.add_option('--file-root-url', default='/files/',
2061                                  help='Specify a root URL for files served.')
2062
2063
2064if __name__ == '__main__':
2065  sys.exit(ServerRunner().main())
2066