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