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