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