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