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