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