testserver.py revision 513209b27ff55e2841eac0e4120199c23acce758
1#!/usr/bin/python2.4
2# Copyright (c) 2006-2010 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 server used for testing Chrome.
7
8It supports several test URLs, as specified by the handlers in TestPageHandler.
9It defaults to living on localhost:8888.
10It can use https if you specify the flag --https=CERT where CERT is the path
11to a pem file containing the certificate and private key that should be used.
12"""
13
14import base64
15import BaseHTTPServer
16import cgi
17import optparse
18import os
19import re
20import shutil
21import SocketServer
22import sys
23import time
24import urlparse
25import warnings
26
27# Ignore deprecation warnings, they make our output more cluttered.
28warnings.filterwarnings("ignore", category=DeprecationWarning)
29
30import pyftpdlib.ftpserver
31import tlslite
32import tlslite.api
33
34try:
35  import hashlib
36  _new_md5 = hashlib.md5
37except ImportError:
38  import md5
39  _new_md5 = md5.new
40
41if sys.platform == 'win32':
42  import msvcrt
43
44SERVER_HTTP = 0
45SERVER_FTP = 1
46
47debug_output = sys.stderr
48def debug(str):
49  debug_output.write(str + "\n")
50  debug_output.flush()
51
52class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
53  """This is a specialization of of BaseHTTPServer to allow it
54  to be exited cleanly (by setting its "stop" member to True)."""
55
56  def serve_forever(self):
57    self.stop = False
58    self.nonce_time = None
59    while not self.stop:
60      self.handle_request()
61    self.socket.close()
62
63class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
64  """This is a specialization of StoppableHTTPerver that add https support."""
65
66  def __init__(self, server_address, request_hander_class, cert_path,
67               ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers):
68    s = open(cert_path).read()
69    x509 = tlslite.api.X509()
70    x509.parse(s)
71    self.cert_chain = tlslite.api.X509CertChain([x509])
72    s = open(cert_path).read()
73    self.private_key = tlslite.api.parsePEMKey(s, private=True)
74    self.ssl_client_auth = ssl_client_auth
75    self.ssl_client_cas = []
76    for ca_file in ssl_client_cas:
77        s = open(ca_file).read()
78        x509 = tlslite.api.X509()
79        x509.parse(s)
80        self.ssl_client_cas.append(x509.subject)
81    self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
82    if ssl_bulk_ciphers is not None:
83      self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
84
85    self.session_cache = tlslite.api.SessionCache()
86    StoppableHTTPServer.__init__(self, server_address, request_hander_class)
87
88  def handshake(self, tlsConnection):
89    """Creates the SSL connection."""
90    try:
91      tlsConnection.handshakeServer(certChain=self.cert_chain,
92                                    privateKey=self.private_key,
93                                    sessionCache=self.session_cache,
94                                    reqCert=self.ssl_client_auth,
95                                    settings=self.ssl_handshake_settings,
96                                    reqCAs=self.ssl_client_cas)
97      tlsConnection.ignoreAbruptClose = True
98      return True
99    except tlslite.api.TLSAbruptCloseError:
100      # Ignore abrupt close.
101      return True
102    except tlslite.api.TLSError, error:
103      print "Handshake failure:", str(error)
104      return False
105
106class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
107
108  def __init__(self, request, client_address, socket_server):
109    self._connect_handlers = [
110      self.RedirectConnectHandler,
111      self.ServerAuthConnectHandler,
112      self.DefaultConnectResponseHandler]
113    self._get_handlers = [
114      self.NoCacheMaxAgeTimeHandler,
115      self.NoCacheTimeHandler,
116      self.CacheTimeHandler,
117      self.CacheExpiresHandler,
118      self.CacheProxyRevalidateHandler,
119      self.CachePrivateHandler,
120      self.CachePublicHandler,
121      self.CacheSMaxAgeHandler,
122      self.CacheMustRevalidateHandler,
123      self.CacheMustRevalidateMaxAgeHandler,
124      self.CacheNoStoreHandler,
125      self.CacheNoStoreMaxAgeHandler,
126      self.CacheNoTransformHandler,
127      self.DownloadHandler,
128      self.DownloadFinishHandler,
129      self.EchoHeader,
130      self.EchoHeaderOverride,
131      self.EchoAllHandler,
132      self.FileHandler,
133      self.RealFileWithCommonHeaderHandler,
134      self.RealBZ2FileWithCommonHeaderHandler,
135      self.SetCookieHandler,
136      self.AuthBasicHandler,
137      self.AuthDigestHandler,
138      self.SlowServerHandler,
139      self.ContentTypeHandler,
140      self.ServerRedirectHandler,
141      self.ClientRedirectHandler,
142      self.ChromiumSyncTimeHandler,
143      self.MultipartHandler,
144      self.DefaultResponseHandler]
145    self._post_handlers = [
146      self.EchoTitleHandler,
147      self.EchoAllHandler,
148      self.ChromiumSyncCommandHandler,
149      self.EchoHandler] + self._get_handlers
150    self._put_handlers = [
151      self.EchoTitleHandler,
152      self.EchoAllHandler,
153      self.EchoHandler] + self._get_handlers
154
155    self._mime_types = {
156      'crx' : 'application/x-chrome-extension',
157      'gif': 'image/gif',
158      'jpeg' : 'image/jpeg',
159      'jpg' : 'image/jpeg',
160      'xml' : 'text/xml'
161    }
162    self._default_mime_type = 'text/html'
163
164    BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
165                                                   client_address,
166                                                   socket_server)
167
168  def log_request(self, *args, **kwargs):
169    # Disable request logging to declutter test log output.
170    pass
171
172  def _ShouldHandleRequest(self, handler_name):
173    """Determines if the path can be handled by the handler.
174
175    We consider a handler valid if the path begins with the
176    handler name. It can optionally be followed by "?*", "/*".
177    """
178
179    pattern = re.compile('%s($|\?|/).*' % handler_name)
180    return pattern.match(self.path)
181
182  def GetMIMETypeFromName(self, file_name):
183    """Returns the mime type for the specified file_name. So far it only looks
184    at the file extension."""
185
186    (shortname, extension) = os.path.splitext(file_name.split("?")[0])
187    if len(extension) == 0:
188      # no extension.
189      return self._default_mime_type
190
191    # extension starts with a dot, so we need to remove it
192    return self._mime_types.get(extension[1:], self._default_mime_type)
193
194  def NoCacheMaxAgeTimeHandler(self):
195    """This request handler yields a page with the title set to the current
196    system time, and no caching requested."""
197
198    if not self._ShouldHandleRequest("/nocachetime/maxage"):
199      return False
200
201    self.send_response(200)
202    self.send_header('Cache-Control', 'max-age=0')
203    self.send_header('Content-type', 'text/html')
204    self.end_headers()
205
206    self.wfile.write('<html><head><title>%s</title></head></html>' %
207                     time.time())
208
209    return True
210
211  def NoCacheTimeHandler(self):
212    """This request handler yields a page with the title set to the current
213    system time, and no caching requested."""
214
215    if not self._ShouldHandleRequest("/nocachetime"):
216      return False
217
218    self.send_response(200)
219    self.send_header('Cache-Control', 'no-cache')
220    self.send_header('Content-type', 'text/html')
221    self.end_headers()
222
223    self.wfile.write('<html><head><title>%s</title></head></html>' %
224                     time.time())
225
226    return True
227
228  def CacheTimeHandler(self):
229    """This request handler yields a page with the title set to the current
230    system time, and allows caching for one minute."""
231
232    if not self._ShouldHandleRequest("/cachetime"):
233      return False
234
235    self.send_response(200)
236    self.send_header('Cache-Control', 'max-age=60')
237    self.send_header('Content-type', 'text/html')
238    self.end_headers()
239
240    self.wfile.write('<html><head><title>%s</title></head></html>' %
241                     time.time())
242
243    return True
244
245  def CacheExpiresHandler(self):
246    """This request handler yields a page with the title set to the current
247    system time, and set the page to expire on 1 Jan 2099."""
248
249    if not self._ShouldHandleRequest("/cache/expires"):
250      return False
251
252    self.send_response(200)
253    self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
254    self.send_header('Content-type', 'text/html')
255    self.end_headers()
256
257    self.wfile.write('<html><head><title>%s</title></head></html>' %
258                     time.time())
259
260    return True
261
262  def CacheProxyRevalidateHandler(self):
263    """This request handler yields a page with the title set to the current
264    system time, and allows caching for 60 seconds"""
265
266    if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
267      return False
268
269    self.send_response(200)
270    self.send_header('Content-type', 'text/html')
271    self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
272    self.end_headers()
273
274    self.wfile.write('<html><head><title>%s</title></head></html>' %
275                     time.time())
276
277    return True
278
279  def CachePrivateHandler(self):
280    """This request handler yields a page with the title set to the current
281    system time, and allows caching for 5 seconds."""
282
283    if not self._ShouldHandleRequest("/cache/private"):
284      return False
285
286    self.send_response(200)
287    self.send_header('Content-type', 'text/html')
288    self.send_header('Cache-Control', 'max-age=3, private')
289    self.end_headers()
290
291    self.wfile.write('<html><head><title>%s</title></head></html>' %
292                     time.time())
293
294    return True
295
296  def CachePublicHandler(self):
297    """This request handler yields a page with the title set to the current
298    system time, and allows caching for 5 seconds."""
299
300    if not self._ShouldHandleRequest("/cache/public"):
301      return False
302
303    self.send_response(200)
304    self.send_header('Content-type', 'text/html')
305    self.send_header('Cache-Control', 'max-age=3, public')
306    self.end_headers()
307
308    self.wfile.write('<html><head><title>%s</title></head></html>' %
309                     time.time())
310
311    return True
312
313  def CacheSMaxAgeHandler(self):
314    """This request handler yields a page with the title set to the current
315    system time, and does not allow for caching."""
316
317    if not self._ShouldHandleRequest("/cache/s-maxage"):
318      return False
319
320    self.send_response(200)
321    self.send_header('Content-type', 'text/html')
322    self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
323    self.end_headers()
324
325    self.wfile.write('<html><head><title>%s</title></head></html>' %
326                     time.time())
327
328    return True
329
330  def CacheMustRevalidateHandler(self):
331    """This request handler yields a page with the title set to the current
332    system time, and does not allow caching."""
333
334    if not self._ShouldHandleRequest("/cache/must-revalidate"):
335      return False
336
337    self.send_response(200)
338    self.send_header('Content-type', 'text/html')
339    self.send_header('Cache-Control', 'must-revalidate')
340    self.end_headers()
341
342    self.wfile.write('<html><head><title>%s</title></head></html>' %
343                     time.time())
344
345    return True
346
347  def CacheMustRevalidateMaxAgeHandler(self):
348    """This request handler yields a page with the title set to the current
349    system time, and does not allow caching event though max-age of 60
350    seconds is specified."""
351
352    if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
353      return False
354
355    self.send_response(200)
356    self.send_header('Content-type', 'text/html')
357    self.send_header('Cache-Control', 'max-age=60, must-revalidate')
358    self.end_headers()
359
360    self.wfile.write('<html><head><title>%s</title></head></html>' %
361                     time.time())
362
363    return True
364
365  def CacheNoStoreHandler(self):
366    """This request handler yields a page with the title set to the current
367    system time, and does not allow the page to be stored."""
368
369    if not self._ShouldHandleRequest("/cache/no-store"):
370      return False
371
372    self.send_response(200)
373    self.send_header('Content-type', 'text/html')
374    self.send_header('Cache-Control', 'no-store')
375    self.end_headers()
376
377    self.wfile.write('<html><head><title>%s</title></head></html>' %
378                     time.time())
379
380    return True
381
382  def CacheNoStoreMaxAgeHandler(self):
383    """This request handler yields a page with the title set to the current
384    system time, and does not allow the page to be stored even though max-age
385    of 60 seconds is specified."""
386
387    if not self._ShouldHandleRequest("/cache/no-store/max-age"):
388      return False
389
390    self.send_response(200)
391    self.send_header('Content-type', 'text/html')
392    self.send_header('Cache-Control', 'max-age=60, no-store')
393    self.end_headers()
394
395    self.wfile.write('<html><head><title>%s</title></head></html>' %
396                     time.time())
397
398    return True
399
400
401  def CacheNoTransformHandler(self):
402    """This request handler yields a page with the title set to the current
403    system time, and does not allow the content to transformed during
404    user-agent caching"""
405
406    if not self._ShouldHandleRequest("/cache/no-transform"):
407      return False
408
409    self.send_response(200)
410    self.send_header('Content-type', 'text/html')
411    self.send_header('Cache-Control', 'no-transform')
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 EchoHeader(self):
420    """This handler echoes back the value of a specific request header."""
421    """The only difference between this function and the EchoHeaderOverride"""
422    """function is in the parameter being passed to the helper function"""
423    return self.EchoHeaderHelper("/echoheader")
424
425  def EchoHeaderOverride(self):
426    """This handler echoes back the value of a specific request header."""
427    """The UrlRequest unit tests also execute for ChromeFrame which uses"""
428    """IE to issue HTTP requests using the host network stack."""
429    """The Accept and Charset tests which expect the server to echo back"""
430    """the corresponding headers fail here as IE returns cached responses"""
431    """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
432    """treats this request as a new request and does not cache it."""
433    return self.EchoHeaderHelper("/echoheaderoverride")
434
435  def EchoHeaderHelper(self, echo_header):
436    """This function echoes back the value of the request header passed in."""
437    if not self._ShouldHandleRequest(echo_header):
438      return False
439
440    query_char = self.path.find('?')
441    if query_char != -1:
442      header_name = self.path[query_char+1:]
443
444    self.send_response(200)
445    self.send_header('Content-type', 'text/plain')
446    self.send_header('Cache-control', 'max-age=60000')
447    # insert a vary header to properly indicate that the cachability of this
448    # request is subject to value of the request header being echoed.
449    if len(header_name) > 0:
450      self.send_header('Vary', header_name)
451    self.end_headers()
452
453    if len(header_name) > 0:
454      self.wfile.write(self.headers.getheader(header_name))
455
456    return True
457
458  def EchoHandler(self):
459    """This handler just echoes back the payload of the request, for testing
460    form submission."""
461
462    if not self._ShouldHandleRequest("/echo"):
463      return False
464
465    self.send_response(200)
466    self.send_header('Content-type', 'text/html')
467    self.end_headers()
468    length = int(self.headers.getheader('content-length'))
469    request = self.rfile.read(length)
470    self.wfile.write(request)
471    return True
472
473  def EchoTitleHandler(self):
474    """This handler is like Echo, but sets the page title to the request."""
475
476    if not self._ShouldHandleRequest("/echotitle"):
477      return False
478
479    self.send_response(200)
480    self.send_header('Content-type', 'text/html')
481    self.end_headers()
482    length = int(self.headers.getheader('content-length'))
483    request = self.rfile.read(length)
484    self.wfile.write('<html><head><title>')
485    self.wfile.write(request)
486    self.wfile.write('</title></head></html>')
487    return True
488
489  def EchoAllHandler(self):
490    """This handler yields a (more) human-readable page listing information
491    about the request header & contents."""
492
493    if not self._ShouldHandleRequest("/echoall"):
494      return False
495
496    self.send_response(200)
497    self.send_header('Content-type', 'text/html')
498    self.end_headers()
499    self.wfile.write('<html><head><style>'
500      'pre { border: 1px solid black; margin: 5px; padding: 5px }'
501      '</style></head><body>'
502      '<div style="float: right">'
503      '<a href="http://localhost:8888/echo">back to referring page</a></div>'
504      '<h1>Request Body:</h1><pre>')
505
506    if self.command == 'POST' or self.command == 'PUT':
507      length = int(self.headers.getheader('content-length'))
508      qs = self.rfile.read(length)
509      params = cgi.parse_qs(qs, keep_blank_values=1)
510
511      for param in params:
512        self.wfile.write('%s=%s\n' % (param, params[param][0]))
513
514    self.wfile.write('</pre>')
515
516    self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
517
518    self.wfile.write('</body></html>')
519    return True
520
521  def DownloadHandler(self):
522    """This handler sends a downloadable file with or without reporting
523    the size (6K)."""
524
525    if self.path.startswith("/download-unknown-size"):
526      send_length = False
527    elif self.path.startswith("/download-known-size"):
528      send_length = True
529    else:
530      return False
531
532    #
533    # The test which uses this functionality is attempting to send
534    # small chunks of data to the client.  Use a fairly large buffer
535    # so that we'll fill chrome's IO buffer enough to force it to
536    # actually write the data.
537    # See also the comments in the client-side of this test in
538    # download_uitest.cc
539    #
540    size_chunk1 = 35*1024
541    size_chunk2 = 10*1024
542
543    self.send_response(200)
544    self.send_header('Content-type', 'application/octet-stream')
545    self.send_header('Cache-Control', 'max-age=0')
546    if send_length:
547      self.send_header('Content-Length', size_chunk1 + size_chunk2)
548    self.end_headers()
549
550    # First chunk of data:
551    self.wfile.write("*" * size_chunk1)
552    self.wfile.flush()
553
554    # handle requests until one of them clears this flag.
555    self.server.waitForDownload = True
556    while self.server.waitForDownload:
557      self.server.handle_request()
558
559    # Second chunk of data:
560    self.wfile.write("*" * size_chunk2)
561    return True
562
563  def DownloadFinishHandler(self):
564    """This handler just tells the server to finish the current download."""
565
566    if not self._ShouldHandleRequest("/download-finish"):
567      return False
568
569    self.server.waitForDownload = False
570    self.send_response(200)
571    self.send_header('Content-type', 'text/html')
572    self.send_header('Cache-Control', 'max-age=0')
573    self.end_headers()
574    return True
575
576  def _ReplaceFileData(self, data, query_parameters):
577    """Replaces matching substrings in a file.
578
579    If the 'replace_orig' and 'replace_new' URL query parameters are present,
580    a new string is returned with all occasions of the 'replace_orig' value
581    replaced by the 'replace_new' value.
582
583    If the parameters are not present, |data| is returned.
584    """
585    query_dict = cgi.parse_qs(query_parameters)
586    orig_values = query_dict.get('replace_orig', [])
587    new_values = query_dict.get('replace_new', [])
588    if not orig_values or not new_values:
589      return data
590    orig_value = orig_values[0]
591    new_value = new_values[0]
592    return data.replace(orig_value, new_value)
593
594  def FileHandler(self):
595    """This handler sends the contents of the requested file.  Wow, it's like
596    a real webserver!"""
597
598    prefix = self.server.file_root_url
599    if not self.path.startswith(prefix):
600      return False
601
602    # Consume a request body if present.
603    if self.command == 'POST' or self.command == 'PUT' :
604      self.rfile.read(int(self.headers.getheader('content-length')))
605
606    _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
607    sub_path = url_path[len(prefix):]
608    entries = sub_path.split('/')
609    file_path = os.path.join(self.server.data_dir, *entries)
610    if os.path.isdir(file_path):
611      file_path = os.path.join(file_path, 'index.html')
612
613    if not os.path.isfile(file_path):
614      print "File not found " + sub_path + " full path:" + file_path
615      self.send_error(404)
616      return True
617
618    f = open(file_path, "rb")
619    data = f.read()
620    f.close()
621
622    data = self._ReplaceFileData(data, query)
623
624    # If file.mock-http-headers exists, it contains the headers we
625    # should send.  Read them in and parse them.
626    headers_path = file_path + '.mock-http-headers'
627    if os.path.isfile(headers_path):
628      f = open(headers_path, "r")
629
630      # "HTTP/1.1 200 OK"
631      response = f.readline()
632      status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
633      self.send_response(int(status_code))
634
635      for line in f:
636        header_values = re.findall('(\S+):\s*(.*)', line)
637        if len(header_values) > 0:
638          # "name: value"
639          name, value = header_values[0]
640          self.send_header(name, value)
641      f.close()
642    else:
643      # Could be more generic once we support mime-type sniffing, but for
644      # now we need to set it explicitly.
645      self.send_response(200)
646      self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
647      self.send_header('Content-Length', len(data))
648    self.end_headers()
649
650    self.wfile.write(data)
651
652    return True
653
654  def RealFileWithCommonHeaderHandler(self):
655    """This handler sends the contents of the requested file without the pseudo
656    http head!"""
657
658    prefix='/realfiles/'
659    if not self.path.startswith(prefix):
660      return False
661
662    file = self.path[len(prefix):]
663    path = os.path.join(self.server.data_dir, file)
664
665    try:
666      f = open(path, "rb")
667      data = f.read()
668      f.close()
669
670      # just simply set the MIME as octal stream
671      self.send_response(200)
672      self.send_header('Content-type', 'application/octet-stream')
673      self.end_headers()
674
675      self.wfile.write(data)
676    except:
677      self.send_error(404)
678
679    return True
680
681  def RealBZ2FileWithCommonHeaderHandler(self):
682    """This handler sends the bzip2 contents of the requested file with
683     corresponding Content-Encoding field in http head!"""
684
685    prefix='/realbz2files/'
686    if not self.path.startswith(prefix):
687      return False
688
689    parts = self.path.split('?')
690    file = parts[0][len(prefix):]
691    path = os.path.join(self.server.data_dir, file) + '.bz2'
692
693    if len(parts) > 1:
694      options = parts[1]
695    else:
696      options = ''
697
698    try:
699      self.send_response(200)
700      accept_encoding = self.headers.get("Accept-Encoding")
701      if accept_encoding.find("bzip2") != -1:
702        f = open(path, "rb")
703        data = f.read()
704        f.close()
705        self.send_header('Content-Encoding', 'bzip2')
706        self.send_header('Content-type', 'application/x-bzip2')
707        self.end_headers()
708        if options == 'incremental-header':
709          self.wfile.write(data[:1])
710          self.wfile.flush()
711          time.sleep(1.0)
712          self.wfile.write(data[1:])
713        else:
714          self.wfile.write(data)
715      else:
716        """client do not support bzip2 format, send pseudo content
717        """
718        self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
719        self.end_headers()
720        self.wfile.write("you do not support bzip2 encoding")
721    except:
722      self.send_error(404)
723
724    return True
725
726  def SetCookieHandler(self):
727    """This handler just sets a cookie, for testing cookie handling."""
728
729    if not self._ShouldHandleRequest("/set-cookie"):
730      return False
731
732    query_char = self.path.find('?')
733    if query_char != -1:
734      cookie_values = self.path[query_char + 1:].split('&')
735    else:
736      cookie_values = ("",)
737    self.send_response(200)
738    self.send_header('Content-type', 'text/html')
739    for cookie_value in cookie_values:
740      self.send_header('Set-Cookie', '%s' % cookie_value)
741    self.end_headers()
742    for cookie_value in cookie_values:
743      self.wfile.write('%s' % cookie_value)
744    return True
745
746  def AuthBasicHandler(self):
747    """This handler tests 'Basic' authentication.  It just sends a page with
748    title 'user/pass' if you succeed."""
749
750    if not self._ShouldHandleRequest("/auth-basic"):
751      return False
752
753    username = userpass = password = b64str = ""
754
755    set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
756
757    auth = self.headers.getheader('authorization')
758    try:
759      if not auth:
760        raise Exception('no auth')
761      b64str = re.findall(r'Basic (\S+)', auth)[0]
762      userpass = base64.b64decode(b64str)
763      username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
764      if password != 'secret':
765        raise Exception('wrong password')
766    except Exception, e:
767      # Authentication failed.
768      self.send_response(401)
769      self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
770      self.send_header('Content-type', 'text/html')
771      if set_cookie_if_challenged:
772        self.send_header('Set-Cookie', 'got_challenged=true')
773      self.end_headers()
774      self.wfile.write('<html><head>')
775      self.wfile.write('<title>Denied: %s</title>' % e)
776      self.wfile.write('</head><body>')
777      self.wfile.write('auth=%s<p>' % auth)
778      self.wfile.write('b64str=%s<p>' % b64str)
779      self.wfile.write('username: %s<p>' % username)
780      self.wfile.write('userpass: %s<p>' % userpass)
781      self.wfile.write('password: %s<p>' % password)
782      self.wfile.write('You sent:<br>%s<p>' % self.headers)
783      self.wfile.write('</body></html>')
784      return True
785
786    # Authentication successful.  (Return a cachable response to allow for
787    # testing cached pages that require authentication.)
788    if_none_match = self.headers.getheader('if-none-match')
789    if if_none_match == "abc":
790      self.send_response(304)
791      self.end_headers()
792    else:
793      self.send_response(200)
794      self.send_header('Content-type', 'text/html')
795      self.send_header('Cache-control', 'max-age=60000')
796      self.send_header('Etag', 'abc')
797      self.end_headers()
798      self.wfile.write('<html><head>')
799      self.wfile.write('<title>%s/%s</title>' % (username, password))
800      self.wfile.write('</head><body>')
801      self.wfile.write('auth=%s<p>' % auth)
802      self.wfile.write('You sent:<br>%s<p>' % self.headers)
803      self.wfile.write('</body></html>')
804
805    return True
806
807  def GetNonce(self, force_reset=False):
808   """Returns a nonce that's stable per request path for the server's lifetime.
809
810   This is a fake implementation. A real implementation would only use a given
811   nonce a single time (hence the name n-once). However, for the purposes of
812   unittesting, we don't care about the security of the nonce.
813
814   Args:
815     force_reset: Iff set, the nonce will be changed. Useful for testing the
816         "stale" response.
817   """
818   if force_reset or not self.server.nonce_time:
819     self.server.nonce_time = time.time()
820   return _new_md5('privatekey%s%d' %
821                   (self.path, self.server.nonce_time)).hexdigest()
822
823  def AuthDigestHandler(self):
824    """This handler tests 'Digest' authentication.
825
826    It just sends a page with title 'user/pass' if you succeed.
827
828    A stale response is sent iff "stale" is present in the request path.
829    """
830    if not self._ShouldHandleRequest("/auth-digest"):
831      return False
832
833    stale = 'stale' in self.path
834    nonce = self.GetNonce(force_reset=stale)
835    opaque = _new_md5('opaque').hexdigest()
836    password = 'secret'
837    realm = 'testrealm'
838
839    auth = self.headers.getheader('authorization')
840    pairs = {}
841    try:
842      if not auth:
843        raise Exception('no auth')
844      if not auth.startswith('Digest'):
845        raise Exception('not digest')
846      # Pull out all the name="value" pairs as a dictionary.
847      pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
848
849      # Make sure it's all valid.
850      if pairs['nonce'] != nonce:
851        raise Exception('wrong nonce')
852      if pairs['opaque'] != opaque:
853        raise Exception('wrong opaque')
854
855      # Check the 'response' value and make sure it matches our magic hash.
856      # See http://www.ietf.org/rfc/rfc2617.txt
857      hash_a1 = _new_md5(
858          ':'.join([pairs['username'], realm, password])).hexdigest()
859      hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
860      if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
861        response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
862            pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
863      else:
864        response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
865
866      if pairs['response'] != response:
867        raise Exception('wrong password')
868    except Exception, e:
869      # Authentication failed.
870      self.send_response(401)
871      hdr = ('Digest '
872             'realm="%s", '
873             'domain="/", '
874             'qop="auth", '
875             'algorithm=MD5, '
876             'nonce="%s", '
877             'opaque="%s"') % (realm, nonce, opaque)
878      if stale:
879        hdr += ', stale="TRUE"'
880      self.send_header('WWW-Authenticate', hdr)
881      self.send_header('Content-type', 'text/html')
882      self.end_headers()
883      self.wfile.write('<html><head>')
884      self.wfile.write('<title>Denied: %s</title>' % e)
885      self.wfile.write('</head><body>')
886      self.wfile.write('auth=%s<p>' % auth)
887      self.wfile.write('pairs=%s<p>' % pairs)
888      self.wfile.write('You sent:<br>%s<p>' % self.headers)
889      self.wfile.write('We are replying:<br>%s<p>' % hdr)
890      self.wfile.write('</body></html>')
891      return True
892
893    # Authentication successful.
894    self.send_response(200)
895    self.send_header('Content-type', 'text/html')
896    self.end_headers()
897    self.wfile.write('<html><head>')
898    self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
899    self.wfile.write('</head><body>')
900    self.wfile.write('auth=%s<p>' % auth)
901    self.wfile.write('pairs=%s<p>' % pairs)
902    self.wfile.write('</body></html>')
903
904    return True
905
906  def SlowServerHandler(self):
907    """Wait for the user suggested time before responding. The syntax is
908    /slow?0.5 to wait for half a second."""
909    if not self._ShouldHandleRequest("/slow"):
910      return False
911    query_char = self.path.find('?')
912    wait_sec = 1.0
913    if query_char >= 0:
914      try:
915        wait_sec = int(self.path[query_char + 1:])
916      except ValueError:
917        pass
918    time.sleep(wait_sec)
919    self.send_response(200)
920    self.send_header('Content-type', 'text/plain')
921    self.end_headers()
922    self.wfile.write("waited %d seconds" % wait_sec)
923    return True
924
925  def ContentTypeHandler(self):
926    """Returns a string of html with the given content type.  E.g.,
927    /contenttype?text/css returns an html file with the Content-Type
928    header set to text/css."""
929    if not self._ShouldHandleRequest("/contenttype"):
930      return False
931    query_char = self.path.find('?')
932    content_type = self.path[query_char + 1:].strip()
933    if not content_type:
934      content_type = 'text/html'
935    self.send_response(200)
936    self.send_header('Content-Type', content_type)
937    self.end_headers()
938    self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
939    return True
940
941  def ServerRedirectHandler(self):
942    """Sends a server redirect to the given URL. The syntax is
943    '/server-redirect?http://foo.bar/asdf' to redirect to
944    'http://foo.bar/asdf'"""
945
946    test_name = "/server-redirect"
947    if not self._ShouldHandleRequest(test_name):
948      return False
949
950    query_char = self.path.find('?')
951    if query_char < 0 or len(self.path) <= query_char + 1:
952      self.sendRedirectHelp(test_name)
953      return True
954    dest = self.path[query_char + 1:]
955
956    self.send_response(301)  # moved permanently
957    self.send_header('Location', dest)
958    self.send_header('Content-type', 'text/html')
959    self.end_headers()
960    self.wfile.write('<html><head>')
961    self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
962
963    return True
964
965  def ClientRedirectHandler(self):
966    """Sends a client redirect to the given URL. The syntax is
967    '/client-redirect?http://foo.bar/asdf' to redirect to
968    'http://foo.bar/asdf'"""
969
970    test_name = "/client-redirect"
971    if not self._ShouldHandleRequest(test_name):
972      return False
973
974    query_char = self.path.find('?');
975    if query_char < 0 or len(self.path) <= query_char + 1:
976      self.sendRedirectHelp(test_name)
977      return True
978    dest = self.path[query_char + 1:]
979
980    self.send_response(200)
981    self.send_header('Content-type', 'text/html')
982    self.end_headers()
983    self.wfile.write('<html><head>')
984    self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
985    self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
986
987    return True
988
989  def ChromiumSyncTimeHandler(self):
990    """Handle Chromium sync .../time requests.
991
992    The syncer sometimes checks server reachability by examining /time.
993    """
994    test_name = "/chromiumsync/time"
995    if not self._ShouldHandleRequest(test_name):
996      return False
997
998    self.send_response(200)
999    self.send_header('Content-type', 'text/html')
1000    self.end_headers()
1001    return True
1002
1003  def ChromiumSyncCommandHandler(self):
1004    """Handle a chromiumsync command arriving via http.
1005
1006    This covers all sync protocol commands: authentication, getupdates, and
1007    commit.
1008    """
1009    test_name = "/chromiumsync/command"
1010    if not self._ShouldHandleRequest(test_name):
1011      return False
1012
1013    length = int(self.headers.getheader('content-length'))
1014    raw_request = self.rfile.read(length)
1015
1016    if not self.server._sync_handler:
1017      import chromiumsync
1018      self.server._sync_handler = chromiumsync.TestServer()
1019    http_response, raw_reply = self.server._sync_handler.HandleCommand(
1020        self.path, raw_request)
1021    self.send_response(http_response)
1022    self.end_headers()
1023    self.wfile.write(raw_reply)
1024    return True
1025
1026  def MultipartHandler(self):
1027    """Send a multipart response (10 text/html pages)."""
1028    test_name = "/multipart"
1029    if not self._ShouldHandleRequest(test_name):
1030      return False
1031
1032    num_frames = 10
1033    bound = '12345'
1034    self.send_response(200)
1035    self.send_header('Content-type',
1036                     'multipart/x-mixed-replace;boundary=' + bound)
1037    self.end_headers()
1038
1039    for i in xrange(num_frames):
1040      self.wfile.write('--' + bound + '\r\n')
1041      self.wfile.write('Content-type: text/html\r\n\r\n')
1042      self.wfile.write('<title>page ' + str(i) + '</title>')
1043      self.wfile.write('page ' + str(i))
1044
1045    self.wfile.write('--' + bound + '--')
1046    return True
1047
1048  def DefaultResponseHandler(self):
1049    """This is the catch-all response handler for requests that aren't handled
1050    by one of the special handlers above.
1051    Note that we specify the content-length as without it the https connection
1052    is not closed properly (and the browser keeps expecting data)."""
1053
1054    contents = "Default response given for path: " + self.path
1055    self.send_response(200)
1056    self.send_header('Content-type', 'text/html')
1057    self.send_header("Content-Length", len(contents))
1058    self.end_headers()
1059    self.wfile.write(contents)
1060    return True
1061
1062  def RedirectConnectHandler(self):
1063    """Sends a redirect to the CONNECT request for www.redirect.com. This
1064    response is not specified by the RFC, so the browser should not follow
1065    the redirect."""
1066
1067    if (self.path.find("www.redirect.com") < 0):
1068      return False
1069
1070    dest = "http://www.destination.com/foo.js"
1071
1072    self.send_response(302)  # moved temporarily
1073    self.send_header('Location', dest)
1074    self.send_header('Connection', 'close')
1075    self.end_headers()
1076    return True
1077
1078  def ServerAuthConnectHandler(self):
1079    """Sends a 401 to the CONNECT request for www.server-auth.com. This
1080    response doesn't make sense because the proxy server cannot request
1081    server authentication."""
1082
1083    if (self.path.find("www.server-auth.com") < 0):
1084      return False
1085
1086    challenge = 'Basic realm="WallyWorld"'
1087
1088    self.send_response(401)  # unauthorized
1089    self.send_header('WWW-Authenticate', challenge)
1090    self.send_header('Connection', 'close')
1091    self.end_headers()
1092    return True
1093
1094  def DefaultConnectResponseHandler(self):
1095    """This is the catch-all response handler for CONNECT requests that aren't
1096    handled by one of the special handlers above.  Real Web servers respond
1097    with 400 to CONNECT requests."""
1098
1099    contents = "Your client has issued a malformed or illegal request."
1100    self.send_response(400)  # bad request
1101    self.send_header('Content-type', 'text/html')
1102    self.send_header("Content-Length", len(contents))
1103    self.end_headers()
1104    self.wfile.write(contents)
1105    return True
1106
1107  def do_CONNECT(self):
1108    for handler in self._connect_handlers:
1109      if handler():
1110        return
1111
1112  def do_GET(self):
1113    for handler in self._get_handlers:
1114      if handler():
1115        return
1116
1117  def do_POST(self):
1118    for handler in self._post_handlers:
1119      if handler():
1120        return
1121
1122  def do_PUT(self):
1123    for handler in self._put_handlers:
1124      if handler():
1125        return
1126
1127  # called by the redirect handling function when there is no parameter
1128  def sendRedirectHelp(self, redirect_name):
1129    self.send_response(200)
1130    self.send_header('Content-type', 'text/html')
1131    self.end_headers()
1132    self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1133    self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1134    self.wfile.write('</body></html>')
1135
1136def MakeDataDir():
1137  if options.data_dir:
1138    if not os.path.isdir(options.data_dir):
1139      print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1140      return None
1141    my_data_dir = options.data_dir
1142  else:
1143    # Create the default path to our data dir, relative to the exe dir.
1144    my_data_dir = os.path.dirname(sys.argv[0])
1145    my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
1146                               "test", "data")
1147
1148    #TODO(ibrar): Must use Find* funtion defined in google\tools
1149    #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1150
1151  return my_data_dir
1152
1153class FileMultiplexer:
1154  def __init__(self, fd1, fd2) :
1155    self.__fd1 = fd1
1156    self.__fd2 = fd2
1157
1158  def __del__(self) :
1159    if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1160      self.__fd1.close()
1161    if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1162      self.__fd2.close()
1163
1164  def write(self, text) :
1165    self.__fd1.write(text)
1166    self.__fd2.write(text)
1167
1168  def flush(self) :
1169    self.__fd1.flush()
1170    self.__fd2.flush()
1171
1172def main(options, args):
1173  logfile = open('testserver.log', 'w')
1174  sys.stdout = FileMultiplexer(sys.stdout, logfile)
1175  sys.stderr = FileMultiplexer(sys.stderr, logfile)
1176
1177  port = options.port
1178
1179  if options.server_type == SERVER_HTTP:
1180    if options.cert:
1181      # let's make sure the cert file exists.
1182      if not os.path.isfile(options.cert):
1183        print 'specified server cert file not found: ' + options.cert + \
1184              ' exiting...'
1185        return
1186      for ca_cert in options.ssl_client_ca:
1187        if not os.path.isfile(ca_cert):
1188          print 'specified trusted client CA file not found: ' + ca_cert + \
1189                ' exiting...'
1190          return
1191      server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
1192                           options.ssl_client_auth, options.ssl_client_ca,
1193                           options.ssl_bulk_cipher)
1194      print 'HTTPS server started on port %d...' % port
1195    else:
1196      server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
1197      print 'HTTP server started on port %d...' % port
1198
1199    server.data_dir = MakeDataDir()
1200    server.file_root_url = options.file_root_url
1201    server._sync_handler = None
1202
1203  # means FTP Server
1204  else:
1205    my_data_dir = MakeDataDir()
1206
1207    # Instantiate a dummy authorizer for managing 'virtual' users
1208    authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1209
1210    # Define a new user having full r/w permissions and a read-only
1211    # anonymous user
1212    authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1213
1214    authorizer.add_anonymous(my_data_dir)
1215
1216    # Instantiate FTP handler class
1217    ftp_handler = pyftpdlib.ftpserver.FTPHandler
1218    ftp_handler.authorizer = authorizer
1219
1220    # Define a customized banner (string returned when client connects)
1221    ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1222                          pyftpdlib.ftpserver.__ver__)
1223
1224    # Instantiate FTP server class and listen to 127.0.0.1:port
1225    address = ('127.0.0.1', port)
1226    server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
1227    print 'FTP server started on port %d...' % port
1228
1229  # Notify the parent that we've started. (BaseServer subclasses
1230  # bind their sockets on construction.)
1231  if options.startup_pipe is not None:
1232    if sys.platform == 'win32':
1233      fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1234    else:
1235      fd = options.startup_pipe
1236    startup_pipe = os.fdopen(fd, "w")
1237    startup_pipe.write("READY")
1238    startup_pipe.close()
1239
1240  try:
1241    server.serve_forever()
1242  except KeyboardInterrupt:
1243    print 'shutting down server'
1244    server.stop = True
1245
1246if __name__ == '__main__':
1247  option_parser = optparse.OptionParser()
1248  option_parser.add_option("-f", '--ftp', action='store_const',
1249                           const=SERVER_FTP, default=SERVER_HTTP,
1250                           dest='server_type',
1251                           help='FTP or HTTP server: default is HTTP.')
1252  option_parser.add_option('', '--port', default='8888', type='int',
1253                           help='Port used by the server.')
1254  option_parser.add_option('', '--data-dir', dest='data_dir',
1255                           help='Directory from which to read the files.')
1256  option_parser.add_option('', '--https', dest='cert',
1257                           help='Specify that https should be used, specify '
1258                           'the path to the cert containing the private key '
1259                           'the server should use.')
1260  option_parser.add_option('', '--ssl-client-auth', action='store_true',
1261                           help='Require SSL client auth on every connection.')
1262  option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1263                           help='Specify that the client certificate request '
1264                           'should include the CA named in the subject of '
1265                           'the DER-encoded certificate contained in the '
1266                           'specified file. This option may appear multiple '
1267                           'times, indicating multiple CA names should be '
1268                           'sent in the request.')
1269  option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1270                           help='Specify the bulk encryption algorithm(s)'
1271                           'that will be accepted by the SSL server. Valid '
1272                           'values are "aes256", "aes128", "3des", "rc4". If '
1273                           'omitted, all algorithms will be used. This '
1274                           'option may appear multiple times, indicating '
1275                           'multiple algorithms should be enabled.');
1276  option_parser.add_option('', '--file-root-url', default='/files/',
1277                           help='Specify a root URL for files served.')
1278  option_parser.add_option('', '--startup-pipe', type='int',
1279                           dest='startup_pipe',
1280                           help='File handle of pipe to parent process')
1281  options, args = option_parser.parse_args()
1282
1283  sys.exit(main(options, args))
1284