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