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