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