blob: 2b0c36c9586b2c694c7fbd2e277c9678d37bb626 [file] [log] [blame]
dpranke@chromium.org70049b72011-10-14 00:38:18 +00001#!/usr/bin/env python
rsimha@chromium.org99a6f172013-01-20 01:10:24 +00002# Copyright 2013 The Chromium Authors. All rights reserved.
license.botf3378c22008-08-24 00:55:55 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
initial.commit94958cf2008-07-26 22:42:52 +00005
rsimha@chromium.org99a6f172013-01-20 01:10:24 +00006"""This is a simple HTTP/FTP/TCP/UDP/BASIC_AUTH_PROXY/WEBSOCKET server used for
7testing Chrome.
initial.commit94958cf2008-07-26 22:42:52 +00008
9It supports several test URLs, as specified by the handlers in TestPageHandler.
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +000010By default, it listens on an ephemeral port and sends the port number back to
11the originating process over a pipe. The originating process can specify an
12explicit port if necessary.
initial.commit94958cf2008-07-26 22:42:52 +000013It can use https if you specify the flag --https=CERT where CERT is the path
14to a pem file containing the certificate and private key that should be used.
initial.commit94958cf2008-07-26 22:42:52 +000015"""
16
17import base64
18import BaseHTTPServer
19import cgi
mattm@chromium.org11f17fb2012-09-23 00:06:27 +000020import hashlib
toyoshim@chromium.orgaa1b6e72012-10-09 03:43:19 +000021import logging
agl@chromium.org77a9ad92012-03-20 15:14:27 +000022import minica
initial.commit94958cf2008-07-26 22:42:52 +000023import os
kkimlabs@chromium.org622395e2013-08-22 22:50:55 +000024import json
rtenneti@chromium.org922a8222011-08-16 03:30:45 +000025import random
initial.commit94958cf2008-07-26 22:42:52 +000026import re
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +000027import select
agl@chromium.orgb3ec3462012-03-19 20:32:47 +000028import socket
agl@chromium.org77a9ad92012-03-20 15:14:27 +000029import SocketServer
yhirano@chromium.org51f90d92014-03-24 04:49:23 +000030import ssl
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +000031import struct
agl@chromium.org77a9ad92012-03-20 15:14:27 +000032import sys
33import threading
initial.commit94958cf2008-07-26 22:42:52 +000034import time
battre@chromium.orgd4479e12011-11-07 17:09:19 +000035import urllib
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +000036import urlparse
ahendrickson@chromium.orgab17b6a2011-05-24 20:14:39 +000037import zlib
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +000038
maruel@chromium.org5ddc64e2013-12-05 17:50:12 +000039BASE_DIR = os.path.dirname(os.path.abspath(__file__))
40ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(BASE_DIR)))
41
davidben@chromium.org7d53b542014-04-10 17:56:44 +000042# Temporary hack to deal with tlslite 0.3.8 -> 0.4.6 upgrade.
43#
44# TODO(davidben): Remove this when it has cycled through all the bots and
45# developer checkouts or when http://crbug.com/356276 is resolved.
46try:
47 os.remove(os.path.join(ROOT_DIR, 'third_party', 'tlslite',
48 'tlslite', 'utils', 'hmac.pyc'))
49except Exception:
50 pass
maruel@chromium.org5ddc64e2013-12-05 17:50:12 +000051
52# Append at the end of sys.path, it's fine to use the system library.
53sys.path.append(os.path.join(ROOT_DIR, 'third_party', 'pyftpdlib', 'src'))
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000054
davidben@chromium.org7d53b542014-04-10 17:56:44 +000055# Insert at the beginning of the path, we want to use our copies of the library
maruel@chromium.org5ddc64e2013-12-05 17:50:12 +000056# unconditionally.
57sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party', 'pywebsocket', 'src'))
davidben@chromium.org7d53b542014-04-10 17:56:44 +000058sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party', 'tlslite'))
59
yhirano@chromium.org51f90d92014-03-24 04:49:23 +000060import mod_pywebsocket.standalone
pliard@chromium.org3f8873f2012-11-14 11:38:55 +000061from mod_pywebsocket.standalone import WebSocketServer
yhirano@chromium.org51f90d92014-03-24 04:49:23 +000062# import manually
63mod_pywebsocket.standalone.ssl = ssl
davidben@chromium.org06fcf202010-09-22 18:15:23 +000064
davidben@chromium.org7d53b542014-04-10 17:56:44 +000065import pyftpdlib.ftpserver
66
67import tlslite
68import tlslite.api
69
70import echo_message
71import testserver_base
72
maruel@chromium.org756cf982009-03-05 12:46:38 +000073SERVER_HTTP = 0
erikkay@google.comd5182ff2009-01-08 20:45:27 +000074SERVER_FTP = 1
rsimha@chromium.org99a6f172013-01-20 01:10:24 +000075SERVER_TCP_ECHO = 2
76SERVER_UDP_ECHO = 3
77SERVER_BASIC_AUTH_PROXY = 4
78SERVER_WEBSOCKET = 5
toyoshim@chromium.orgaa1b6e72012-10-09 03:43:19 +000079
80# Default request queue size for WebSocketServer.
81_DEFAULT_REQUEST_QUEUE_SIZE = 128
initial.commit94958cf2008-07-26 22:42:52 +000082
toyoshim@chromium.orgaa1b6e72012-10-09 03:43:19 +000083class WebSocketOptions:
84 """Holds options for WebSocketServer."""
85
86 def __init__(self, host, port, data_dir):
87 self.request_queue_size = _DEFAULT_REQUEST_QUEUE_SIZE
88 self.server_host = host
89 self.port = port
90 self.websock_handlers = data_dir
91 self.scan_dir = None
92 self.allow_handlers_outside_root_dir = False
93 self.websock_handlers_map_file = None
94 self.cgi_directories = []
95 self.is_executable_method = None
96 self.allow_draft75 = False
97 self.strict = True
98
toyoshim@chromium.orgaa1b6e72012-10-09 03:43:19 +000099 self.use_tls = False
100 self.private_key = None
101 self.certificate = None
toyoshim@chromium.orgd532cf32012-10-18 05:05:51 +0000102 self.tls_client_auth = False
toyoshim@chromium.orgaa1b6e72012-10-09 03:43:19 +0000103 self.tls_client_ca = None
yhirano@chromium.org51f90d92014-03-24 04:49:23 +0000104 self.tls_module = 'ssl'
toyoshim@chromium.orgaa1b6e72012-10-09 03:43:19 +0000105 self.use_basic_auth = False
106
mattm@chromium.org830a3712012-11-07 23:00:07 +0000107
agl@chromium.orgf9e66792011-12-12 22:22:19 +0000108class RecordingSSLSessionCache(object):
109 """RecordingSSLSessionCache acts as a TLS session cache and maintains a log of
110 lookups and inserts in order to test session cache behaviours."""
111
112 def __init__(self):
113 self.log = []
114
115 def __getitem__(self, sessionID):
116 self.log.append(('lookup', sessionID))
117 raise KeyError()
118
119 def __setitem__(self, sessionID, session):
120 self.log.append(('insert', sessionID))
121
erikwright@chromium.org847ef282012-02-22 16:41:10 +0000122
rsimha@chromium.org99a6f172013-01-20 01:10:24 +0000123class HTTPServer(testserver_base.ClientRestrictingServerMixIn,
124 testserver_base.BrokenPipeHandlerMixIn,
125 testserver_base.StoppableHTTPServer):
agl@chromium.org77a9ad92012-03-20 15:14:27 +0000126 """This is a specialization of StoppableHTTPServer that adds client
erikwright@chromium.org847ef282012-02-22 16:41:10 +0000127 verification."""
128
129 pass
130
rsimha@chromium.org99a6f172013-01-20 01:10:24 +0000131class OCSPServer(testserver_base.ClientRestrictingServerMixIn,
132 testserver_base.BrokenPipeHandlerMixIn,
bauerb@chromium.orgcc71a892012-12-04 21:21:21 +0000133 BaseHTTPServer.HTTPServer):
agl@chromium.org77a9ad92012-03-20 15:14:27 +0000134 """This is a specialization of HTTPServer that serves an
135 OCSP response"""
136
137 def serve_forever_on_thread(self):
138 self.thread = threading.Thread(target = self.serve_forever,
139 name = "OCSPServerThread")
140 self.thread.start()
141
142 def stop_serving(self):
143 self.shutdown()
144 self.thread.join()
erikwright@chromium.org847ef282012-02-22 16:41:10 +0000145
mattm@chromium.org830a3712012-11-07 23:00:07 +0000146
erikwright@chromium.org847ef282012-02-22 16:41:10 +0000147class HTTPSServer(tlslite.api.TLSSocketServerMixIn,
rsimha@chromium.org99a6f172013-01-20 01:10:24 +0000148 testserver_base.ClientRestrictingServerMixIn,
149 testserver_base.BrokenPipeHandlerMixIn,
150 testserver_base.StoppableHTTPServer):
agl@chromium.org77a9ad92012-03-20 15:14:27 +0000151 """This is a specialization of StoppableHTTPServer that add https support and
erikwright@chromium.org847ef282012-02-22 16:41:10 +0000152 client verification."""
initial.commit94958cf2008-07-26 22:42:52 +0000153
agl@chromium.org77a9ad92012-03-20 15:14:27 +0000154 def __init__(self, server_address, request_hander_class, pem_cert_and_key,
davidben@chromium.org74aa8dd2014-04-11 07:20:26 +0000155 ssl_client_auth, ssl_client_cas,
156 ssl_bulk_ciphers, ssl_key_exchanges,
agl@chromium.orgd0e11ca2013-12-11 20:16:13 +0000157 record_resume_info, tls_intolerant, signed_cert_timestamps,
ekasper@google.com3bce2cf2013-12-17 00:25:51 +0000158 fallback_scsv_enabled, ocsp_response):
davidben@chromium.org7d53b542014-04-10 17:56:44 +0000159 self.cert_chain = tlslite.api.X509CertChain()
160 self.cert_chain.parsePemList(pem_cert_and_key)
phajdan.jr@chromium.org9e6098d2013-06-24 19:00:38 +0000161 # Force using only python implementation - otherwise behavior is different
162 # depending on whether m2crypto Python module is present (error is thrown
163 # when it is). m2crypto uses a C (based on OpenSSL) implementation under
164 # the hood.
165 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key,
166 private=True,
167 implementations=['python'])
davidben@chromium.org31282a12010-08-07 01:10:02 +0000168 self.ssl_client_auth = ssl_client_auth
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000169 self.ssl_client_cas = []
davidben@chromium.org7d53b542014-04-10 17:56:44 +0000170 if tls_intolerant == 0:
171 self.tls_intolerant = None
172 else:
173 self.tls_intolerant = (3, tls_intolerant)
ekasper@google.com24aa8222013-11-28 13:43:26 +0000174 self.signed_cert_timestamps = signed_cert_timestamps
agl@chromium.orgd0e11ca2013-12-11 20:16:13 +0000175 self.fallback_scsv_enabled = fallback_scsv_enabled
ekasper@google.com3bce2cf2013-12-17 00:25:51 +0000176 self.ocsp_response = ocsp_response
agl@chromium.org143daa42012-04-26 18:45:34 +0000177
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000178 for ca_file in ssl_client_cas:
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000179 s = open(ca_file).read()
180 x509 = tlslite.api.X509()
181 x509.parse(s)
182 self.ssl_client_cas.append(x509.subject)
rsleevi@chromium.org2124c812010-10-28 11:57:36 +0000183 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
184 if ssl_bulk_ciphers is not None:
185 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
davidben@chromium.org74aa8dd2014-04-11 07:20:26 +0000186 if ssl_key_exchanges is not None:
187 self.ssl_handshake_settings.keyExchangeNames = ssl_key_exchanges
initial.commit94958cf2008-07-26 22:42:52 +0000188
agl@chromium.orgf9e66792011-12-12 22:22:19 +0000189 if record_resume_info:
190 # If record_resume_info is true then we'll replace the session cache with
191 # an object that records the lookups and inserts that it sees.
192 self.session_cache = RecordingSSLSessionCache()
193 else:
194 self.session_cache = tlslite.api.SessionCache()
rsimha@chromium.org99a6f172013-01-20 01:10:24 +0000195 testserver_base.StoppableHTTPServer.__init__(self,
196 server_address,
197 request_hander_class)
initial.commit94958cf2008-07-26 22:42:52 +0000198
199 def handshake(self, tlsConnection):
200 """Creates the SSL connection."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000201
initial.commit94958cf2008-07-26 22:42:52 +0000202 try:
agl@chromium.org04700be2013-03-02 18:40:41 +0000203 self.tlsConnection = tlsConnection
initial.commit94958cf2008-07-26 22:42:52 +0000204 tlsConnection.handshakeServer(certChain=self.cert_chain,
205 privateKey=self.private_key,
davidben@chromium.org31282a12010-08-07 01:10:02 +0000206 sessionCache=self.session_cache,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000207 reqCert=self.ssl_client_auth,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +0000208 settings=self.ssl_handshake_settings,
agl@chromium.org143daa42012-04-26 18:45:34 +0000209 reqCAs=self.ssl_client_cas,
ekasper@google.com24aa8222013-11-28 13:43:26 +0000210 tlsIntolerant=self.tls_intolerant,
211 signedCertTimestamps=
agl@chromium.orgd0e11ca2013-12-11 20:16:13 +0000212 self.signed_cert_timestamps,
ekasper@google.com3bce2cf2013-12-17 00:25:51 +0000213 fallbackSCSV=self.fallback_scsv_enabled,
214 ocspResponse = self.ocsp_response)
initial.commit94958cf2008-07-26 22:42:52 +0000215 tlsConnection.ignoreAbruptClose = True
216 return True
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +0000217 except tlslite.api.TLSAbruptCloseError:
218 # Ignore abrupt close.
219 return True
initial.commit94958cf2008-07-26 22:42:52 +0000220 except tlslite.api.TLSError, error:
wjia@chromium.orgff532f32013-03-18 19:23:44 +0000221 print "Handshake failure:", str(error)
initial.commit94958cf2008-07-26 22:42:52 +0000222 return False
223
akalin@chromium.org154bb132010-11-12 02:20:27 +0000224
rsimha@chromium.org99a6f172013-01-20 01:10:24 +0000225class FTPServer(testserver_base.ClientRestrictingServerMixIn,
226 pyftpdlib.ftpserver.FTPServer):
erikwright@chromium.org847ef282012-02-22 16:41:10 +0000227 """This is a specialization of FTPServer that adds client verification."""
228
229 pass
230
231
rsimha@chromium.org99a6f172013-01-20 01:10:24 +0000232class TCPEchoServer(testserver_base.ClientRestrictingServerMixIn,
233 SocketServer.TCPServer):
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +0000234 """A TCP echo server that echoes back what it has received."""
235
236 def server_bind(self):
237 """Override server_bind to store the server name."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000238
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +0000239 SocketServer.TCPServer.server_bind(self)
240 host, port = self.socket.getsockname()[:2]
241 self.server_name = socket.getfqdn(host)
242 self.server_port = port
243
244 def serve_forever(self):
245 self.stop = False
246 self.nonce_time = None
247 while not self.stop:
248 self.handle_request()
249 self.socket.close()
250
251
rsimha@chromium.org99a6f172013-01-20 01:10:24 +0000252class UDPEchoServer(testserver_base.ClientRestrictingServerMixIn,
253 SocketServer.UDPServer):
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +0000254 """A UDP echo server that echoes back what it has received."""
255
256 def server_bind(self):
257 """Override server_bind to store the server name."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000258
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +0000259 SocketServer.UDPServer.server_bind(self)
260 host, port = self.socket.getsockname()[:2]
261 self.server_name = socket.getfqdn(host)
262 self.server_port = port
263
264 def serve_forever(self):
265 self.stop = False
266 self.nonce_time = None
267 while not self.stop:
268 self.handle_request()
269 self.socket.close()
270
271
rsimha@chromium.org99a6f172013-01-20 01:10:24 +0000272class TestPageHandler(testserver_base.BasePageHandler):
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +0000273 # Class variables to allow for persistence state between page handler
274 # invocations
275 rst_limits = {}
276 fail_precondition = {}
initial.commit94958cf2008-07-26 22:42:52 +0000277
278 def __init__(self, request, client_address, socket_server):
akalin@chromium.org154bb132010-11-12 02:20:27 +0000279 connect_handlers = [
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000280 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000281 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000282 self.DefaultConnectResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000283 get_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000284 self.NoCacheMaxAgeTimeHandler,
285 self.NoCacheTimeHandler,
286 self.CacheTimeHandler,
287 self.CacheExpiresHandler,
288 self.CacheProxyRevalidateHandler,
289 self.CachePrivateHandler,
290 self.CachePublicHandler,
291 self.CacheSMaxAgeHandler,
292 self.CacheMustRevalidateHandler,
293 self.CacheMustRevalidateMaxAgeHandler,
294 self.CacheNoStoreHandler,
295 self.CacheNoStoreMaxAgeHandler,
296 self.CacheNoTransformHandler,
297 self.DownloadHandler,
298 self.DownloadFinishHandler,
299 self.EchoHeader,
ananta@chromium.org56812d02011-04-07 17:52:05 +0000300 self.EchoHeaderCache,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000301 self.EchoAllHandler,
ahendrickson@chromium.orgab17b6a2011-05-24 20:14:39 +0000302 self.ZipFileHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000303 self.FileHandler,
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000304 self.SetCookieHandler,
shalev@chromium.org9ede92f2012-06-14 22:40:34 +0000305 self.SetManyCookiesHandler,
mattm@chromium.org983fc462012-06-30 00:52:08 +0000306 self.ExpectAndSetCookieHandler,
battre@chromium.orgd4479e12011-11-07 17:09:19 +0000307 self.SetHeaderHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000308 self.AuthBasicHandler,
309 self.AuthDigestHandler,
310 self.SlowServerHandler,
vsevik@chromium.orgf0e997e2011-05-20 09:36:14 +0000311 self.ChunkedServerHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000312 self.ContentTypeHandler,
creis@google.com2f4f6a42011-03-25 19:44:19 +0000313 self.NoContentHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000314 self.ServerRedirectHandler,
315 self.ClientRedirectHandler,
tony@chromium.org03266982010-03-05 03:18:42 +0000316 self.MultipartHandler,
agl@chromium.orgf9e66792011-12-12 22:22:19 +0000317 self.GetSSLSessionCacheHandler,
rsleevi@chromium.org6bb9f042013-02-16 04:10:07 +0000318 self.SSLManySmallRecords,
agl@chromium.org04700be2013-03-02 18:40:41 +0000319 self.GetChannelID,
simonjam@chromium.orgf9cf32f2012-02-13 23:56:14 +0000320 self.CloseSocketHandler,
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +0000321 self.RangeResetHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000322 self.DefaultResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000323 post_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000324 self.EchoTitleHandler,
mnissler@chromium.org7c939802010-11-11 08:47:14 +0000325 self.EchoHandler,
kkimlabs@chromium.org622395e2013-08-22 22:50:55 +0000326 self.PostOnlyFileHandler,
327 self.EchoMultipartPostHandler] + get_handlers
akalin@chromium.org154bb132010-11-12 02:20:27 +0000328 put_handlers = [
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000329 self.EchoTitleHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000330 self.EchoHandler] + get_handlers
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000331 head_handlers = [
332 self.FileHandler,
333 self.DefaultResponseHandler]
initial.commit94958cf2008-07-26 22:42:52 +0000334
maruel@google.come250a9b2009-03-10 17:39:46 +0000335 self._mime_types = {
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000336 'crx' : 'application/x-chrome-extension',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000337 'exe' : 'application/octet-stream',
maruel@google.come250a9b2009-03-10 17:39:46 +0000338 'gif': 'image/gif',
339 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000340 'jpg' : 'image/jpeg',
satorux@chromium.orgfdc70122012-03-07 18:08:41 +0000341 'json': 'application/json',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000342 'pdf' : 'application/pdf',
dsjang@chromium.org3f4d97b2013-08-23 23:55:37 +0000343 'txt' : 'text/plain',
wolenetz@chromium.org6c74fb82013-01-09 00:38:34 +0000344 'wav' : 'audio/wav',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000345 'xml' : 'text/xml'
maruel@google.come250a9b2009-03-10 17:39:46 +0000346 }
initial.commit94958cf2008-07-26 22:42:52 +0000347 self._default_mime_type = 'text/html'
348
rsimha@chromium.org99a6f172013-01-20 01:10:24 +0000349 testserver_base.BasePageHandler.__init__(self, request, client_address,
350 socket_server, connect_handlers,
351 get_handlers, head_handlers,
352 post_handlers, put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000353
initial.commit94958cf2008-07-26 22:42:52 +0000354 def GetMIMETypeFromName(self, file_name):
355 """Returns the mime type for the specified file_name. So far it only looks
356 at the file extension."""
357
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000358 (_shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000359 if len(extension) == 0:
360 # no extension.
361 return self._default_mime_type
362
ericroman@google.comc17ca532009-05-07 03:51:05 +0000363 # extension starts with a dot, so we need to remove it
364 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000365
initial.commit94958cf2008-07-26 22:42:52 +0000366 def NoCacheMaxAgeTimeHandler(self):
367 """This request handler yields a page with the title set to the current
368 system time, and no caching requested."""
369
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000370 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000371 return False
372
373 self.send_response(200)
374 self.send_header('Cache-Control', 'max-age=0')
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000375 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000376 self.end_headers()
377
maruel@google.come250a9b2009-03-10 17:39:46 +0000378 self.wfile.write('<html><head><title>%s</title></head></html>' %
379 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000380
381 return True
382
383 def NoCacheTimeHandler(self):
384 """This request handler yields a page with the title set to the current
385 system time, and no caching requested."""
386
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000387 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000388 return False
389
390 self.send_response(200)
391 self.send_header('Cache-Control', 'no-cache')
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000392 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000393 self.end_headers()
394
maruel@google.come250a9b2009-03-10 17:39:46 +0000395 self.wfile.write('<html><head><title>%s</title></head></html>' %
396 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000397
398 return True
399
400 def CacheTimeHandler(self):
401 """This request handler yields a page with the title set to the current
402 system time, and allows caching for one minute."""
403
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000404 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000405 return False
406
407 self.send_response(200)
408 self.send_header('Cache-Control', 'max-age=60')
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000409 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000410 self.end_headers()
411
maruel@google.come250a9b2009-03-10 17:39:46 +0000412 self.wfile.write('<html><head><title>%s</title></head></html>' %
413 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000414
415 return True
416
417 def CacheExpiresHandler(self):
418 """This request handler yields a page with the title set to the current
419 system time, and set the page to expire on 1 Jan 2099."""
420
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000421 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000422 return False
423
424 self.send_response(200)
425 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000426 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000427 self.end_headers()
428
maruel@google.come250a9b2009-03-10 17:39:46 +0000429 self.wfile.write('<html><head><title>%s</title></head></html>' %
430 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000431
432 return True
433
434 def CacheProxyRevalidateHandler(self):
435 """This request handler yields a page with the title set to the current
436 system time, and allows caching for 60 seconds"""
437
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000438 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000439 return False
440
441 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000442 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000443 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
444 self.end_headers()
445
maruel@google.come250a9b2009-03-10 17:39:46 +0000446 self.wfile.write('<html><head><title>%s</title></head></html>' %
447 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000448
449 return True
450
451 def CachePrivateHandler(self):
452 """This request handler yields a page with the title set to the current
453 system time, and allows caching for 5 seconds."""
454
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000455 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000456 return False
457
458 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000459 self.send_header('Content-Type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000460 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000461 self.end_headers()
462
maruel@google.come250a9b2009-03-10 17:39:46 +0000463 self.wfile.write('<html><head><title>%s</title></head></html>' %
464 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000465
466 return True
467
468 def CachePublicHandler(self):
469 """This request handler yields a page with the title set to the current
470 system time, and allows caching for 5 seconds."""
471
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000472 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000473 return False
474
475 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000476 self.send_header('Content-Type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000477 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000478 self.end_headers()
479
maruel@google.come250a9b2009-03-10 17:39:46 +0000480 self.wfile.write('<html><head><title>%s</title></head></html>' %
481 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000482
483 return True
484
485 def CacheSMaxAgeHandler(self):
486 """This request handler yields a page with the title set to the current
487 system time, and does not allow for caching."""
488
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000489 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000490 return False
491
492 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000493 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000494 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
495 self.end_headers()
496
maruel@google.come250a9b2009-03-10 17:39:46 +0000497 self.wfile.write('<html><head><title>%s</title></head></html>' %
498 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000499
500 return True
501
502 def CacheMustRevalidateHandler(self):
503 """This request handler yields a page with the title set to the current
504 system time, and does not allow caching."""
505
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000506 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000507 return False
508
509 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000510 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000511 self.send_header('Cache-Control', 'must-revalidate')
512 self.end_headers()
513
maruel@google.come250a9b2009-03-10 17:39:46 +0000514 self.wfile.write('<html><head><title>%s</title></head></html>' %
515 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000516
517 return True
518
519 def CacheMustRevalidateMaxAgeHandler(self):
520 """This request handler yields a page with the title set to the current
521 system time, and does not allow caching event though max-age of 60
522 seconds is specified."""
523
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000524 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000525 return False
526
527 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000528 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000529 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
530 self.end_headers()
531
maruel@google.come250a9b2009-03-10 17:39:46 +0000532 self.wfile.write('<html><head><title>%s</title></head></html>' %
533 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000534
535 return True
536
initial.commit94958cf2008-07-26 22:42:52 +0000537 def CacheNoStoreHandler(self):
538 """This request handler yields a page with the title set to the current
539 system time, and does not allow the page to be stored."""
540
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000541 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000542 return False
543
544 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000545 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000546 self.send_header('Cache-Control', 'no-store')
547 self.end_headers()
548
maruel@google.come250a9b2009-03-10 17:39:46 +0000549 self.wfile.write('<html><head><title>%s</title></head></html>' %
550 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000551
552 return True
553
554 def CacheNoStoreMaxAgeHandler(self):
555 """This request handler yields a page with the title set to the current
556 system time, and does not allow the page to be stored even though max-age
557 of 60 seconds is specified."""
558
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000559 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000560 return False
561
562 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000563 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000564 self.send_header('Cache-Control', 'max-age=60, no-store')
565 self.end_headers()
566
maruel@google.come250a9b2009-03-10 17:39:46 +0000567 self.wfile.write('<html><head><title>%s</title></head></html>' %
568 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000569
570 return True
571
572
573 def CacheNoTransformHandler(self):
574 """This request handler yields a page with the title set to the current
575 system time, and does not allow the content to transformed during
576 user-agent caching"""
577
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000578 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000579 return False
580
581 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000582 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000583 self.send_header('Cache-Control', 'no-transform')
584 self.end_headers()
585
maruel@google.come250a9b2009-03-10 17:39:46 +0000586 self.wfile.write('<html><head><title>%s</title></head></html>' %
587 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000588
589 return True
590
591 def EchoHeader(self):
592 """This handler echoes back the value of a specific request header."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000593
ananta@chromium.org219b2062009-10-23 16:09:41 +0000594 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000595
ananta@chromium.org56812d02011-04-07 17:52:05 +0000596 def EchoHeaderCache(self):
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000597 """This function echoes back the value of a specific request header while
598 allowing caching for 16 hours."""
599
ananta@chromium.org56812d02011-04-07 17:52:05 +0000600 return self.EchoHeaderHelper("/echoheadercache")
ananta@chromium.org219b2062009-10-23 16:09:41 +0000601
602 def EchoHeaderHelper(self, echo_header):
603 """This function echoes back the value of the request header passed in."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000604
ananta@chromium.org219b2062009-10-23 16:09:41 +0000605 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000606 return False
607
608 query_char = self.path.find('?')
609 if query_char != -1:
610 header_name = self.path[query_char+1:]
611
612 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000613 self.send_header('Content-Type', 'text/plain')
ananta@chromium.org56812d02011-04-07 17:52:05 +0000614 if echo_header == '/echoheadercache':
615 self.send_header('Cache-control', 'max-age=60000')
616 else:
617 self.send_header('Cache-control', 'no-cache')
initial.commit94958cf2008-07-26 22:42:52 +0000618 # insert a vary header to properly indicate that the cachability of this
619 # request is subject to value of the request header being echoed.
620 if len(header_name) > 0:
621 self.send_header('Vary', header_name)
622 self.end_headers()
623
624 if len(header_name) > 0:
625 self.wfile.write(self.headers.getheader(header_name))
626
627 return True
628
satish@chromium.orgce0b1d02011-01-25 07:17:11 +0000629 def ReadRequestBody(self):
630 """This function reads the body of the current HTTP request, handling
631 both plain and chunked transfer encoded requests."""
632
633 if self.headers.getheader('transfer-encoding') != 'chunked':
634 length = int(self.headers.getheader('content-length'))
635 return self.rfile.read(length)
636
637 # Read the request body as chunks.
638 body = ""
639 while True:
640 line = self.rfile.readline()
641 length = int(line, 16)
642 if length == 0:
643 self.rfile.readline()
644 break
645 body += self.rfile.read(length)
646 self.rfile.read(2)
647 return body
648
initial.commit94958cf2008-07-26 22:42:52 +0000649 def EchoHandler(self):
650 """This handler just echoes back the payload of the request, for testing
651 form submission."""
652
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000653 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000654 return False
655
656 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000657 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000658 self.end_headers()
satish@chromium.orgce0b1d02011-01-25 07:17:11 +0000659 self.wfile.write(self.ReadRequestBody())
initial.commit94958cf2008-07-26 22:42:52 +0000660 return True
661
662 def EchoTitleHandler(self):
663 """This handler is like Echo, but sets the page title to the request."""
664
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000665 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000666 return False
667
668 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000669 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000670 self.end_headers()
satish@chromium.orgce0b1d02011-01-25 07:17:11 +0000671 request = self.ReadRequestBody()
initial.commit94958cf2008-07-26 22:42:52 +0000672 self.wfile.write('<html><head><title>')
673 self.wfile.write(request)
674 self.wfile.write('</title></head></html>')
675 return True
676
677 def EchoAllHandler(self):
678 """This handler yields a (more) human-readable page listing information
679 about the request header & contents."""
680
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000681 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000682 return False
683
684 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000685 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000686 self.end_headers()
687 self.wfile.write('<html><head><style>'
688 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
689 '</style></head><body>'
690 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000691 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000692 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000693
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000694 if self.command == 'POST' or self.command == 'PUT':
satish@chromium.orgce0b1d02011-01-25 07:17:11 +0000695 qs = self.ReadRequestBody()
ericroman@google.coma47622b2008-11-15 04:36:51 +0000696 params = cgi.parse_qs(qs, keep_blank_values=1)
697
698 for param in params:
699 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000700
701 self.wfile.write('</pre>')
702
703 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
704
705 self.wfile.write('</body></html>')
706 return True
707
kkimlabs@chromium.org622395e2013-08-22 22:50:55 +0000708 def EchoMultipartPostHandler(self):
709 """This handler echoes received multipart post data as json format."""
710
711 if not (self._ShouldHandleRequest("/echomultipartpost") or
712 self._ShouldHandleRequest("/searchbyimage")):
713 return False
714
715 content_type, parameters = cgi.parse_header(
716 self.headers.getheader('content-type'))
717 if content_type == 'multipart/form-data':
718 post_multipart = cgi.parse_multipart(self.rfile, parameters)
719 elif content_type == 'application/x-www-form-urlencoded':
720 raise Exception('POST by application/x-www-form-urlencoded is '
721 'not implemented.')
722 else:
723 post_multipart = {}
724
725 # Since the data can be binary, we encode them by base64.
726 post_multipart_base64_encoded = {}
727 for field, values in post_multipart.items():
728 post_multipart_base64_encoded[field] = [base64.b64encode(value)
729 for value in values]
730
731 result = {'POST_multipart' : post_multipart_base64_encoded}
732
733 self.send_response(200)
734 self.send_header("Content-type", "text/plain")
735 self.end_headers()
736 self.wfile.write(json.dumps(result, indent=2, sort_keys=False))
737 return True
738
initial.commit94958cf2008-07-26 22:42:52 +0000739 def DownloadHandler(self):
740 """This handler sends a downloadable file with or without reporting
741 the size (6K)."""
742
743 if self.path.startswith("/download-unknown-size"):
744 send_length = False
745 elif self.path.startswith("/download-known-size"):
746 send_length = True
747 else:
748 return False
749
750 #
751 # The test which uses this functionality is attempting to send
752 # small chunks of data to the client. Use a fairly large buffer
753 # so that we'll fill chrome's IO buffer enough to force it to
754 # actually write the data.
755 # See also the comments in the client-side of this test in
756 # download_uitest.cc
757 #
758 size_chunk1 = 35*1024
759 size_chunk2 = 10*1024
760
761 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000762 self.send_header('Content-Type', 'application/octet-stream')
initial.commit94958cf2008-07-26 22:42:52 +0000763 self.send_header('Cache-Control', 'max-age=0')
764 if send_length:
765 self.send_header('Content-Length', size_chunk1 + size_chunk2)
766 self.end_headers()
767
768 # First chunk of data:
769 self.wfile.write("*" * size_chunk1)
770 self.wfile.flush()
771
772 # handle requests until one of them clears this flag.
rdsmith@chromium.orgd1c45532013-01-22 19:20:29 +0000773 self.server.wait_for_download = True
774 while self.server.wait_for_download:
initial.commit94958cf2008-07-26 22:42:52 +0000775 self.server.handle_request()
776
777 # Second chunk of data:
778 self.wfile.write("*" * size_chunk2)
779 return True
780
781 def DownloadFinishHandler(self):
782 """This handler just tells the server to finish the current download."""
783
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000784 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000785 return False
786
rdsmith@chromium.orgd1c45532013-01-22 19:20:29 +0000787 self.server.wait_for_download = False
initial.commit94958cf2008-07-26 22:42:52 +0000788 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000789 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +0000790 self.send_header('Cache-Control', 'max-age=0')
791 self.end_headers()
792 return True
793
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000794 def _ReplaceFileData(self, data, query_parameters):
795 """Replaces matching substrings in a file.
796
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000797 If the 'replace_text' URL query parameter is present, it is expected to be
798 of the form old_text:new_text, which indicates that any old_text strings in
799 the file are replaced with new_text. Multiple 'replace_text' parameters may
800 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000801
802 If the parameters are not present, |data| is returned.
803 """
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000804
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000805 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000806 replace_text_values = query_dict.get('replace_text', [])
807 for replace_text_value in replace_text_values:
808 replace_text_args = replace_text_value.split(':')
809 if len(replace_text_args) != 2:
810 raise ValueError(
811 'replace_text must be of form old_text:new_text. Actual value: %s' %
812 replace_text_value)
813 old_text_b64, new_text_b64 = replace_text_args
814 old_text = base64.urlsafe_b64decode(old_text_b64)
815 new_text = base64.urlsafe_b64decode(new_text_b64)
816 data = data.replace(old_text, new_text)
817 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000818
ahendrickson@chromium.orgab17b6a2011-05-24 20:14:39 +0000819 def ZipFileHandler(self):
820 """This handler sends the contents of the requested file in compressed form.
821 Can pass in a parameter that specifies that the content length be
822 C - the compressed size (OK),
823 U - the uncompressed size (Non-standard, but handled),
824 S - less than compressed (OK because we keep going),
825 M - larger than compressed but less than uncompressed (an error),
826 L - larger than uncompressed (an error)
827 Example: compressedfiles/Picture_1.doc?C
828 """
829
830 prefix = "/compressedfiles/"
831 if not self.path.startswith(prefix):
832 return False
833
834 # Consume a request body if present.
835 if self.command == 'POST' or self.command == 'PUT' :
836 self.ReadRequestBody()
837
838 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
839
840 if not query in ('C', 'U', 'S', 'M', 'L'):
841 return False
842
843 sub_path = url_path[len(prefix):]
844 entries = sub_path.split('/')
845 file_path = os.path.join(self.server.data_dir, *entries)
846 if os.path.isdir(file_path):
847 file_path = os.path.join(file_path, 'index.html')
848
849 if not os.path.isfile(file_path):
wjia@chromium.orgff532f32013-03-18 19:23:44 +0000850 print "File not found " + sub_path + " full path:" + file_path
ahendrickson@chromium.orgab17b6a2011-05-24 20:14:39 +0000851 self.send_error(404)
852 return True
853
854 f = open(file_path, "rb")
855 data = f.read()
856 uncompressed_len = len(data)
857 f.close()
858
859 # Compress the data.
860 data = zlib.compress(data)
861 compressed_len = len(data)
862
863 content_length = compressed_len
864 if query == 'U':
865 content_length = uncompressed_len
866 elif query == 'S':
867 content_length = compressed_len / 2
868 elif query == 'M':
869 content_length = (compressed_len + uncompressed_len) / 2
870 elif query == 'L':
871 content_length = compressed_len + uncompressed_len
872
873 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000874 self.send_header('Content-Type', 'application/msword')
ahendrickson@chromium.orgab17b6a2011-05-24 20:14:39 +0000875 self.send_header('Content-encoding', 'deflate')
876 self.send_header('Connection', 'close')
877 self.send_header('Content-Length', content_length)
878 self.send_header('ETag', '\'' + file_path + '\'')
879 self.end_headers()
880
881 self.wfile.write(data)
882
883 return True
884
initial.commit94958cf2008-07-26 22:42:52 +0000885 def FileHandler(self):
886 """This handler sends the contents of the requested file. Wow, it's like
887 a real webserver!"""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000888
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000889 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000890 if not self.path.startswith(prefix):
891 return False
cbentzel@chromium.org18fc5562012-01-13 13:27:44 +0000892 return self._FileHandlerHelper(prefix)
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000893
cbentzel@chromium.org18fc5562012-01-13 13:27:44 +0000894 def PostOnlyFileHandler(self):
895 """This handler sends the contents of the requested file on a POST."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000896
cbentzel@chromium.org4f3a9412012-01-31 23:47:20 +0000897 prefix = urlparse.urljoin(self.server.file_root_url, 'post/')
cbentzel@chromium.org18fc5562012-01-13 13:27:44 +0000898 if not self.path.startswith(prefix):
899 return False
cbentzel@chromium.org18fc5562012-01-13 13:27:44 +0000900 return self._FileHandlerHelper(prefix)
901
902 def _FileHandlerHelper(self, prefix):
benjhayden@chromium.org77ea5442012-02-14 23:29:37 +0000903 request_body = ''
904 if self.command == 'POST' or self.command == 'PUT':
905 # Consume a request body if present.
906 request_body = self.ReadRequestBody()
907
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000908 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
benjhayden@chromium.org77ea5442012-02-14 23:29:37 +0000909 query_dict = cgi.parse_qs(query)
910
911 expected_body = query_dict.get('expected_body', [])
912 if expected_body and request_body not in expected_body:
913 self.send_response(404)
914 self.end_headers()
915 self.wfile.write('')
916 return True
917
918 expected_headers = query_dict.get('expected_headers', [])
919 for expected_header in expected_headers:
920 header_name, expected_value = expected_header.split(':')
921 if self.headers.getheader(header_name) != expected_value:
922 self.send_response(404)
923 self.end_headers()
924 self.wfile.write('')
925 return True
926
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000927 sub_path = url_path[len(prefix):]
928 entries = sub_path.split('/')
929 file_path = os.path.join(self.server.data_dir, *entries)
930 if os.path.isdir(file_path):
931 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000932
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000933 if not os.path.isfile(file_path):
wjia@chromium.orgff532f32013-03-18 19:23:44 +0000934 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000935 self.send_error(404)
936 return True
937
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000938 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000939 data = f.read()
940 f.close()
941
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000942 data = self._ReplaceFileData(data, query)
943
benjhayden@chromium.org77ea5442012-02-14 23:29:37 +0000944 old_protocol_version = self.protocol_version
945
initial.commit94958cf2008-07-26 22:42:52 +0000946 # If file.mock-http-headers exists, it contains the headers we
947 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000948 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000949 if os.path.isfile(headers_path):
950 f = open(headers_path, "r")
951
952 # "HTTP/1.1 200 OK"
953 response = f.readline()
simonjam@chromium.orgf9cf32f2012-02-13 23:56:14 +0000954 http_major, http_minor, status_code = re.findall(
955 'HTTP/(\d+).(\d+) (\d+)', response)[0]
956 self.protocol_version = "HTTP/%s.%s" % (http_major, http_minor)
initial.commit94958cf2008-07-26 22:42:52 +0000957 self.send_response(int(status_code))
958
959 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000960 header_values = re.findall('(\S+):\s*(.*)', line)
961 if len(header_values) > 0:
962 # "name: value"
963 name, value = header_values[0]
964 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000965 f.close()
966 else:
967 # Could be more generic once we support mime-type sniffing, but for
968 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000969
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000970 range_header = self.headers.get('Range')
971 if range_header and range_header.startswith('bytes='):
972 # Note this doesn't handle all valid byte range_header values (i.e.
973 # left open ended ones), just enough for what we needed so far.
974 range_header = range_header[6:].split('-')
975 start = int(range_header[0])
976 if range_header[1]:
977 end = int(range_header[1])
shishir@chromium.orge6444822011-12-09 02:45:44 +0000978 else:
fischman@chromium.orgd4f2e722012-03-16 20:57:26 +0000979 end = len(data) - 1
jam@chromium.org41550782010-11-17 23:47:50 +0000980
981 self.send_response(206)
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +0000982 content_range = ('bytes ' + str(start) + '-' + str(end) + '/' +
983 str(len(data)))
jam@chromium.org41550782010-11-17 23:47:50 +0000984 self.send_header('Content-Range', content_range)
985 data = data[start: end + 1]
986 else:
987 self.send_response(200)
988
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000989 self.send_header('Content-Type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000990 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000991 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000992 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000993 self.end_headers()
994
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +0000995 if (self.command != 'HEAD'):
996 self.wfile.write(data)
initial.commit94958cf2008-07-26 22:42:52 +0000997
simonjam@chromium.orgf9cf32f2012-02-13 23:56:14 +0000998 self.protocol_version = old_protocol_version
initial.commit94958cf2008-07-26 22:42:52 +0000999 return True
1000
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +00001001 def SetCookieHandler(self):
1002 """This handler just sets a cookie, for testing cookie handling."""
1003
1004 if not self._ShouldHandleRequest("/set-cookie"):
1005 return False
1006
1007 query_char = self.path.find('?')
1008 if query_char != -1:
1009 cookie_values = self.path[query_char + 1:].split('&')
1010 else:
1011 cookie_values = ("",)
1012 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001013 self.send_header('Content-Type', 'text/html')
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +00001014 for cookie_value in cookie_values:
1015 self.send_header('Set-Cookie', '%s' % cookie_value)
1016 self.end_headers()
1017 for cookie_value in cookie_values:
1018 self.wfile.write('%s' % cookie_value)
1019 return True
1020
shalev@chromium.org9ede92f2012-06-14 22:40:34 +00001021 def SetManyCookiesHandler(self):
1022 """This handler just sets a given number of cookies, for testing handling
1023 of large numbers of cookies."""
1024
1025 if not self._ShouldHandleRequest("/set-many-cookies"):
1026 return False
1027
1028 query_char = self.path.find('?')
1029 if query_char != -1:
1030 num_cookies = int(self.path[query_char + 1:])
1031 else:
1032 num_cookies = 0
1033 self.send_response(200)
1034 self.send_header('', 'text/html')
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001035 for _i in range(0, num_cookies):
shalev@chromium.org9ede92f2012-06-14 22:40:34 +00001036 self.send_header('Set-Cookie', 'a=')
1037 self.end_headers()
1038 self.wfile.write('%d cookies were sent' % num_cookies)
1039 return True
1040
mattm@chromium.org983fc462012-06-30 00:52:08 +00001041 def ExpectAndSetCookieHandler(self):
1042 """Expects some cookies to be sent, and if they are, sets more cookies.
1043
1044 The expect parameter specifies a required cookie. May be specified multiple
1045 times.
1046 The set parameter specifies a cookie to set if all required cookies are
1047 preset. May be specified multiple times.
1048 The data parameter specifies the response body data to be returned."""
1049
1050 if not self._ShouldHandleRequest("/expect-and-set-cookie"):
1051 return False
1052
1053 _, _, _, _, query, _ = urlparse.urlparse(self.path)
1054 query_dict = cgi.parse_qs(query)
1055 cookies = set()
1056 if 'Cookie' in self.headers:
1057 cookie_header = self.headers.getheader('Cookie')
1058 cookies.update([s.strip() for s in cookie_header.split(';')])
1059 got_all_expected_cookies = True
1060 for expected_cookie in query_dict.get('expect', []):
1061 if expected_cookie not in cookies:
1062 got_all_expected_cookies = False
1063 self.send_response(200)
1064 self.send_header('Content-Type', 'text/html')
1065 if got_all_expected_cookies:
1066 for cookie_value in query_dict.get('set', []):
1067 self.send_header('Set-Cookie', '%s' % cookie_value)
1068 self.end_headers()
1069 for data_value in query_dict.get('data', []):
1070 self.wfile.write(data_value)
1071 return True
1072
battre@chromium.orgd4479e12011-11-07 17:09:19 +00001073 def SetHeaderHandler(self):
1074 """This handler sets a response header. Parameters are in the
1075 key%3A%20value&key2%3A%20value2 format."""
1076
1077 if not self._ShouldHandleRequest("/set-header"):
1078 return False
1079
1080 query_char = self.path.find('?')
1081 if query_char != -1:
1082 headers_values = self.path[query_char + 1:].split('&')
1083 else:
1084 headers_values = ("",)
1085 self.send_response(200)
1086 self.send_header('Content-Type', 'text/html')
1087 for header_value in headers_values:
1088 header_value = urllib.unquote(header_value)
1089 (key, value) = header_value.split(': ', 1)
1090 self.send_header(key, value)
1091 self.end_headers()
1092 for header_value in headers_values:
1093 self.wfile.write('%s' % header_value)
1094 return True
1095
initial.commit94958cf2008-07-26 22:42:52 +00001096 def AuthBasicHandler(self):
1097 """This handler tests 'Basic' authentication. It just sends a page with
1098 title 'user/pass' if you succeed."""
1099
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001100 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +00001101 return False
1102
1103 username = userpass = password = b64str = ""
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +00001104 expected_password = 'secret'
1105 realm = 'testrealm'
1106 set_cookie_if_challenged = False
initial.commit94958cf2008-07-26 22:42:52 +00001107
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +00001108 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1109 query_params = cgi.parse_qs(query, True)
1110 if 'set-cookie-if-challenged' in query_params:
1111 set_cookie_if_challenged = True
1112 if 'password' in query_params:
1113 expected_password = query_params['password'][0]
1114 if 'realm' in query_params:
1115 realm = query_params['realm'][0]
ericroman@google.com239b4d82009-03-27 04:00:22 +00001116
initial.commit94958cf2008-07-26 22:42:52 +00001117 auth = self.headers.getheader('authorization')
1118 try:
1119 if not auth:
1120 raise Exception('no auth')
1121 b64str = re.findall(r'Basic (\S+)', auth)[0]
1122 userpass = base64.b64decode(b64str)
1123 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +00001124 if password != expected_password:
initial.commit94958cf2008-07-26 22:42:52 +00001125 raise Exception('wrong password')
1126 except Exception, e:
1127 # Authentication failed.
1128 self.send_response(401)
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +00001129 self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001130 self.send_header('Content-Type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +00001131 if set_cookie_if_challenged:
1132 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +00001133 self.end_headers()
1134 self.wfile.write('<html><head>')
1135 self.wfile.write('<title>Denied: %s</title>' % e)
1136 self.wfile.write('</head><body>')
1137 self.wfile.write('auth=%s<p>' % auth)
1138 self.wfile.write('b64str=%s<p>' % b64str)
1139 self.wfile.write('username: %s<p>' % username)
1140 self.wfile.write('userpass: %s<p>' % userpass)
1141 self.wfile.write('password: %s<p>' % password)
1142 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1143 self.wfile.write('</body></html>')
1144 return True
1145
1146 # Authentication successful. (Return a cachable response to allow for
1147 # testing cached pages that require authentication.)
rvargas@google.com54453b72011-05-19 01:11:11 +00001148 old_protocol_version = self.protocol_version
1149 self.protocol_version = "HTTP/1.1"
1150
initial.commit94958cf2008-07-26 22:42:52 +00001151 if_none_match = self.headers.getheader('if-none-match')
1152 if if_none_match == "abc":
1153 self.send_response(304)
1154 self.end_headers()
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +00001155 elif url_path.endswith(".gif"):
1156 # Using chrome/test/data/google/logo.gif as the test image
1157 test_image_path = ['google', 'logo.gif']
1158 gif_path = os.path.join(self.server.data_dir, *test_image_path)
1159 if not os.path.isfile(gif_path):
1160 self.send_error(404)
rvargas@google.com54453b72011-05-19 01:11:11 +00001161 self.protocol_version = old_protocol_version
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +00001162 return True
1163
1164 f = open(gif_path, "rb")
1165 data = f.read()
1166 f.close()
1167
1168 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001169 self.send_header('Content-Type', 'image/gif')
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +00001170 self.send_header('Cache-control', 'max-age=60000')
1171 self.send_header('Etag', 'abc')
1172 self.end_headers()
1173 self.wfile.write(data)
initial.commit94958cf2008-07-26 22:42:52 +00001174 else:
1175 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001176 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +00001177 self.send_header('Cache-control', 'max-age=60000')
1178 self.send_header('Etag', 'abc')
1179 self.end_headers()
1180 self.wfile.write('<html><head>')
1181 self.wfile.write('<title>%s/%s</title>' % (username, password))
1182 self.wfile.write('</head><body>')
1183 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +00001184 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +00001185 self.wfile.write('</body></html>')
1186
rvargas@google.com54453b72011-05-19 01:11:11 +00001187 self.protocol_version = old_protocol_version
initial.commit94958cf2008-07-26 22:42:52 +00001188 return True
1189
tonyg@chromium.org75054202010-03-31 22:06:10 +00001190 def GetNonce(self, force_reset=False):
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001191 """Returns a nonce that's stable per request path for the server's lifetime.
1192 This is a fake implementation. A real implementation would only use a given
1193 nonce a single time (hence the name n-once). However, for the purposes of
1194 unittesting, we don't care about the security of the nonce.
initial.commit94958cf2008-07-26 22:42:52 +00001195
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001196 Args:
1197 force_reset: Iff set, the nonce will be changed. Useful for testing the
1198 "stale" response.
1199 """
tonyg@chromium.org75054202010-03-31 22:06:10 +00001200
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001201 if force_reset or not self.server.nonce_time:
1202 self.server.nonce_time = time.time()
1203 return hashlib.md5('privatekey%s%d' %
1204 (self.path, self.server.nonce_time)).hexdigest()
tonyg@chromium.org75054202010-03-31 22:06:10 +00001205
1206 def AuthDigestHandler(self):
1207 """This handler tests 'Digest' authentication.
1208
1209 It just sends a page with title 'user/pass' if you succeed.
1210
1211 A stale response is sent iff "stale" is present in the request path.
1212 """
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001213
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001214 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +00001215 return False
1216
tonyg@chromium.org75054202010-03-31 22:06:10 +00001217 stale = 'stale' in self.path
1218 nonce = self.GetNonce(force_reset=stale)
mattm@chromium.org11f17fb2012-09-23 00:06:27 +00001219 opaque = hashlib.md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001220 password = 'secret'
1221 realm = 'testrealm'
1222
1223 auth = self.headers.getheader('authorization')
1224 pairs = {}
1225 try:
1226 if not auth:
1227 raise Exception('no auth')
1228 if not auth.startswith('Digest'):
1229 raise Exception('not digest')
1230 # Pull out all the name="value" pairs as a dictionary.
1231 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1232
1233 # Make sure it's all valid.
1234 if pairs['nonce'] != nonce:
1235 raise Exception('wrong nonce')
1236 if pairs['opaque'] != opaque:
1237 raise Exception('wrong opaque')
1238
1239 # Check the 'response' value and make sure it matches our magic hash.
1240 # See http://www.ietf.org/rfc/rfc2617.txt
mattm@chromium.org11f17fb2012-09-23 00:06:27 +00001241 hash_a1 = hashlib.md5(
maruel@google.come250a9b2009-03-10 17:39:46 +00001242 ':'.join([pairs['username'], realm, password])).hexdigest()
mattm@chromium.org11f17fb2012-09-23 00:06:27 +00001243 hash_a2 = hashlib.md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001244 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
mattm@chromium.org11f17fb2012-09-23 00:06:27 +00001245 response = hashlib.md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +00001246 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1247 else:
mattm@chromium.org11f17fb2012-09-23 00:06:27 +00001248 response = hashlib.md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001249
1250 if pairs['response'] != response:
1251 raise Exception('wrong password')
1252 except Exception, e:
1253 # Authentication failed.
1254 self.send_response(401)
1255 hdr = ('Digest '
1256 'realm="%s", '
1257 'domain="/", '
1258 'qop="auth", '
1259 'algorithm=MD5, '
1260 'nonce="%s", '
1261 'opaque="%s"') % (realm, nonce, opaque)
1262 if stale:
1263 hdr += ', stale="TRUE"'
1264 self.send_header('WWW-Authenticate', hdr)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001265 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +00001266 self.end_headers()
1267 self.wfile.write('<html><head>')
1268 self.wfile.write('<title>Denied: %s</title>' % e)
1269 self.wfile.write('</head><body>')
1270 self.wfile.write('auth=%s<p>' % auth)
1271 self.wfile.write('pairs=%s<p>' % pairs)
1272 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1273 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1274 self.wfile.write('</body></html>')
1275 return True
1276
1277 # Authentication successful.
1278 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001279 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +00001280 self.end_headers()
1281 self.wfile.write('<html><head>')
1282 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1283 self.wfile.write('</head><body>')
1284 self.wfile.write('auth=%s<p>' % auth)
1285 self.wfile.write('pairs=%s<p>' % pairs)
1286 self.wfile.write('</body></html>')
1287
1288 return True
1289
1290 def SlowServerHandler(self):
1291 """Wait for the user suggested time before responding. The syntax is
1292 /slow?0.5 to wait for half a second."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001293
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001294 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +00001295 return False
1296 query_char = self.path.find('?')
1297 wait_sec = 1.0
1298 if query_char >= 0:
1299 try:
1300 wait_sec = int(self.path[query_char + 1:])
1301 except ValueError:
1302 pass
1303 time.sleep(wait_sec)
1304 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001305 self.send_header('Content-Type', 'text/plain')
initial.commit94958cf2008-07-26 22:42:52 +00001306 self.end_headers()
1307 self.wfile.write("waited %d seconds" % wait_sec)
1308 return True
1309
vsevik@chromium.orgf0e997e2011-05-20 09:36:14 +00001310 def ChunkedServerHandler(self):
1311 """Send chunked response. Allows to specify chunks parameters:
1312 - waitBeforeHeaders - ms to wait before sending headers
1313 - waitBetweenChunks - ms to wait between chunks
1314 - chunkSize - size of each chunk in bytes
1315 - chunksNumber - number of chunks
1316 Example: /chunked?waitBeforeHeaders=1000&chunkSize=5&chunksNumber=5
1317 waits one second, then sends headers and five chunks five bytes each."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001318
vsevik@chromium.orgf0e997e2011-05-20 09:36:14 +00001319 if not self._ShouldHandleRequest("/chunked"):
1320 return False
1321 query_char = self.path.find('?')
1322 chunkedSettings = {'waitBeforeHeaders' : 0,
1323 'waitBetweenChunks' : 0,
1324 'chunkSize' : 5,
1325 'chunksNumber' : 5}
1326 if query_char >= 0:
1327 params = self.path[query_char + 1:].split('&')
1328 for param in params:
1329 keyValue = param.split('=')
1330 if len(keyValue) == 2:
1331 try:
1332 chunkedSettings[keyValue[0]] = int(keyValue[1])
1333 except ValueError:
1334 pass
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001335 time.sleep(0.001 * chunkedSettings['waitBeforeHeaders'])
vsevik@chromium.orgf0e997e2011-05-20 09:36:14 +00001336 self.protocol_version = 'HTTP/1.1' # Needed for chunked encoding
1337 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001338 self.send_header('Content-Type', 'text/plain')
vsevik@chromium.orgf0e997e2011-05-20 09:36:14 +00001339 self.send_header('Connection', 'close')
1340 self.send_header('Transfer-Encoding', 'chunked')
1341 self.end_headers()
1342 # Chunked encoding: sending all chunks, then final zero-length chunk and
1343 # then final CRLF.
1344 for i in range(0, chunkedSettings['chunksNumber']):
1345 if i > 0:
1346 time.sleep(0.001 * chunkedSettings['waitBetweenChunks'])
1347 self.sendChunkHelp('*' * chunkedSettings['chunkSize'])
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001348 self.wfile.flush() # Keep in mind that we start flushing only after 1kb.
vsevik@chromium.orgf0e997e2011-05-20 09:36:14 +00001349 self.sendChunkHelp('')
1350 return True
1351
initial.commit94958cf2008-07-26 22:42:52 +00001352 def ContentTypeHandler(self):
1353 """Returns a string of html with the given content type. E.g.,
1354 /contenttype?text/css returns an html file with the Content-Type
1355 header set to text/css."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001356
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001357 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001358 return False
1359 query_char = self.path.find('?')
1360 content_type = self.path[query_char + 1:].strip()
1361 if not content_type:
1362 content_type = 'text/html'
1363 self.send_response(200)
1364 self.send_header('Content-Type', content_type)
1365 self.end_headers()
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001366 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n")
initial.commit94958cf2008-07-26 22:42:52 +00001367 return True
1368
creis@google.com2f4f6a42011-03-25 19:44:19 +00001369 def NoContentHandler(self):
1370 """Returns a 204 No Content response."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001371
creis@google.com2f4f6a42011-03-25 19:44:19 +00001372 if not self._ShouldHandleRequest("/nocontent"):
1373 return False
1374 self.send_response(204)
1375 self.end_headers()
1376 return True
1377
initial.commit94958cf2008-07-26 22:42:52 +00001378 def ServerRedirectHandler(self):
1379 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001380 '/server-redirect?http://foo.bar/asdf' to redirect to
1381 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001382
1383 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001384 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001385 return False
1386
1387 query_char = self.path.find('?')
1388 if query_char < 0 or len(self.path) <= query_char + 1:
1389 self.sendRedirectHelp(test_name)
1390 return True
davidben@chromium.orgc3e1fc72013-09-18 01:17:38 +00001391 dest = urllib.unquote(self.path[query_char + 1:])
initial.commit94958cf2008-07-26 22:42:52 +00001392
1393 self.send_response(301) # moved permanently
1394 self.send_header('Location', dest)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001395 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +00001396 self.end_headers()
1397 self.wfile.write('<html><head>')
1398 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1399
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001400 return True
initial.commit94958cf2008-07-26 22:42:52 +00001401
1402 def ClientRedirectHandler(self):
1403 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001404 '/client-redirect?http://foo.bar/asdf' to redirect to
1405 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001406
1407 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001408 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001409 return False
1410
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001411 query_char = self.path.find('?')
initial.commit94958cf2008-07-26 22:42:52 +00001412 if query_char < 0 or len(self.path) <= query_char + 1:
1413 self.sendRedirectHelp(test_name)
1414 return True
davidben@chromium.orgc3e1fc72013-09-18 01:17:38 +00001415 dest = urllib.unquote(self.path[query_char + 1:])
initial.commit94958cf2008-07-26 22:42:52 +00001416
1417 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001418 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +00001419 self.end_headers()
1420 self.wfile.write('<html><head>')
1421 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1422 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1423
1424 return True
1425
tony@chromium.org03266982010-03-05 03:18:42 +00001426 def MultipartHandler(self):
1427 """Send a multipart response (10 text/html pages)."""
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001428
tony@chromium.org4cb88302011-09-27 22:13:49 +00001429 test_name = '/multipart'
tony@chromium.org03266982010-03-05 03:18:42 +00001430 if not self._ShouldHandleRequest(test_name):
1431 return False
1432
1433 num_frames = 10
1434 bound = '12345'
1435 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001436 self.send_header('Content-Type',
tony@chromium.org03266982010-03-05 03:18:42 +00001437 'multipart/x-mixed-replace;boundary=' + bound)
1438 self.end_headers()
1439
1440 for i in xrange(num_frames):
1441 self.wfile.write('--' + bound + '\r\n')
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001442 self.wfile.write('Content-Type: text/html\r\n\r\n')
tony@chromium.org03266982010-03-05 03:18:42 +00001443 self.wfile.write('<title>page ' + str(i) + '</title>')
1444 self.wfile.write('page ' + str(i))
1445
1446 self.wfile.write('--' + bound + '--')
1447 return True
1448
agl@chromium.orgf9e66792011-12-12 22:22:19 +00001449 def GetSSLSessionCacheHandler(self):
1450 """Send a reply containing a log of the session cache operations."""
1451
1452 if not self._ShouldHandleRequest('/ssl-session-cache'):
1453 return False
1454
1455 self.send_response(200)
1456 self.send_header('Content-Type', 'text/plain')
1457 self.end_headers()
1458 try:
davidben@chromium.org7d53b542014-04-10 17:56:44 +00001459 log = self.server.session_cache.log
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001460 except AttributeError:
agl@chromium.orgf9e66792011-12-12 22:22:19 +00001461 self.wfile.write('Pass --https-record-resume in order to use' +
1462 ' this request')
davidben@chromium.org7d53b542014-04-10 17:56:44 +00001463 return True
1464
1465 for (action, sessionID) in log:
1466 self.wfile.write('%s\t%s\n' % (action, bytes(sessionID).encode('hex')))
agl@chromium.orgf9e66792011-12-12 22:22:19 +00001467 return True
1468
rsleevi@chromium.org6bb9f042013-02-16 04:10:07 +00001469 def SSLManySmallRecords(self):
1470 """Sends a reply consisting of a variety of small writes. These will be
1471 translated into a series of small SSL records when used over an HTTPS
1472 server."""
1473
1474 if not self._ShouldHandleRequest('/ssl-many-small-records'):
1475 return False
1476
1477 self.send_response(200)
1478 self.send_header('Content-Type', 'text/plain')
1479 self.end_headers()
1480
1481 # Write ~26K of data, in 1350 byte chunks
1482 for i in xrange(20):
1483 self.wfile.write('*' * 1350)
1484 self.wfile.flush()
1485 return True
1486
agl@chromium.org04700be2013-03-02 18:40:41 +00001487 def GetChannelID(self):
1488 """Send a reply containing the hashed ChannelID that the client provided."""
1489
1490 if not self._ShouldHandleRequest('/channel-id'):
1491 return False
1492
1493 self.send_response(200)
1494 self.send_header('Content-Type', 'text/plain')
1495 self.end_headers()
davidben@chromium.org7d53b542014-04-10 17:56:44 +00001496 channel_id = bytes(self.server.tlsConnection.channel_id)
agl@chromium.org04700be2013-03-02 18:40:41 +00001497 self.wfile.write(hashlib.sha256(channel_id).digest().encode('base64'))
1498 return True
1499
simonjam@chromium.orgf9cf32f2012-02-13 23:56:14 +00001500 def CloseSocketHandler(self):
1501 """Closes the socket without sending anything."""
1502
1503 if not self._ShouldHandleRequest('/close-socket'):
1504 return False
1505
1506 self.wfile.close()
1507 return True
1508
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001509 def RangeResetHandler(self):
1510 """Send data broken up by connection resets every N (default 4K) bytes.
1511 Support range requests. If the data requested doesn't straddle a reset
1512 boundary, it will all be sent. Used for testing resuming downloads."""
1513
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001514 def DataForRange(start, end):
1515 """Data to be provided for a particular range of bytes."""
1516 # Offset and scale to avoid too obvious (and hence potentially
1517 # collidable) data.
1518 return ''.join([chr(y % 256)
1519 for y in range(start * 2 + 15, end * 2 + 15, 2)])
1520
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001521 if not self._ShouldHandleRequest('/rangereset'):
1522 return False
1523
asanka@chromium.org94670cb2013-11-26 16:57:52 +00001524 # HTTP/1.1 is required for ETag and range support.
1525 self.protocol_version = 'HTTP/1.1'
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001526 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1527
1528 # Defaults
1529 size = 8000
1530 # Note that the rst is sent just before sending the rst_boundary byte.
1531 rst_boundary = 4000
1532 respond_to_range = True
1533 hold_for_signal = False
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001534 rst_limit = -1
1535 token = 'DEFAULT'
1536 fail_precondition = 0
1537 send_verifiers = True
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001538
1539 # Parse the query
1540 qdict = urlparse.parse_qs(query, True)
1541 if 'size' in qdict:
rdsmith@chromium.orgd1c45532013-01-22 19:20:29 +00001542 size = int(qdict['size'][0])
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001543 if 'rst_boundary' in qdict:
rdsmith@chromium.orgd1c45532013-01-22 19:20:29 +00001544 rst_boundary = int(qdict['rst_boundary'][0])
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001545 if 'token' in qdict:
1546 # Identifying token for stateful tests.
1547 token = qdict['token'][0]
1548 if 'rst_limit' in qdict:
1549 # Max number of rsts for a given token.
1550 rst_limit = int(qdict['rst_limit'][0])
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001551 if 'bounce_range' in qdict:
1552 respond_to_range = False
1553 if 'hold' in qdict:
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001554 # Note that hold_for_signal will not work with null range requests;
1555 # see TODO below.
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001556 hold_for_signal = True
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001557 if 'no_verifiers' in qdict:
1558 send_verifiers = False
1559 if 'fail_precondition' in qdict:
1560 fail_precondition = int(qdict['fail_precondition'][0])
1561
1562 # Record already set information, or set it.
1563 rst_limit = TestPageHandler.rst_limits.setdefault(token, rst_limit)
1564 if rst_limit != 0:
1565 TestPageHandler.rst_limits[token] -= 1
1566 fail_precondition = TestPageHandler.fail_precondition.setdefault(
1567 token, fail_precondition)
1568 if fail_precondition != 0:
1569 TestPageHandler.fail_precondition[token] -= 1
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001570
1571 first_byte = 0
1572 last_byte = size - 1
1573
1574 # Does that define what we want to return, or do we need to apply
1575 # a range?
1576 range_response = False
1577 range_header = self.headers.getheader('range')
1578 if range_header and respond_to_range:
1579 mo = re.match("bytes=(\d*)-(\d*)", range_header)
1580 if mo.group(1):
1581 first_byte = int(mo.group(1))
1582 if mo.group(2):
1583 last_byte = int(mo.group(2))
1584 if last_byte > size - 1:
1585 last_byte = size - 1
1586 range_response = True
1587 if last_byte < first_byte:
1588 return False
1589
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001590 if (fail_precondition and
1591 (self.headers.getheader('If-Modified-Since') or
1592 self.headers.getheader('If-Match'))):
1593 self.send_response(412)
1594 self.end_headers()
1595 return True
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001596
1597 if range_response:
1598 self.send_response(206)
1599 self.send_header('Content-Range',
1600 'bytes %d-%d/%d' % (first_byte, last_byte, size))
1601 else:
1602 self.send_response(200)
1603 self.send_header('Content-Type', 'application/octet-stream')
1604 self.send_header('Content-Length', last_byte - first_byte + 1)
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001605 if send_verifiers:
asanka@chromium.org3ffced92013-12-13 22:20:27 +00001606 # If fail_precondition is non-zero, then the ETag for each request will be
1607 # different.
1608 etag = "%s%d" % (token, fail_precondition)
1609 self.send_header('ETag', etag)
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001610 self.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST')
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001611 self.end_headers()
1612
1613 if hold_for_signal:
1614 # TODO(rdsmith/phajdan.jr): http://crbug.com/169519: Without writing
1615 # a single byte, the self.server.handle_request() below hangs
1616 # without processing new incoming requests.
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001617 self.wfile.write(DataForRange(first_byte, first_byte + 1))
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001618 first_byte = first_byte + 1
1619 # handle requests until one of them clears this flag.
rdsmith@chromium.orgd1c45532013-01-22 19:20:29 +00001620 self.server.wait_for_download = True
1621 while self.server.wait_for_download:
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001622 self.server.handle_request()
1623
1624 possible_rst = ((first_byte / rst_boundary) + 1) * rst_boundary
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001625 if possible_rst >= last_byte or rst_limit == 0:
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001626 # No RST has been requested in this range, so we don't need to
1627 # do anything fancy; just write the data and let the python
1628 # infrastructure close the connection.
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001629 self.wfile.write(DataForRange(first_byte, last_byte + 1))
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001630 self.wfile.flush()
1631 return True
1632
1633 # We're resetting the connection part way in; go to the RST
1634 # boundary and then send an RST.
rdsmith@chromium.orgd1c45532013-01-22 19:20:29 +00001635 # Because socket semantics do not guarantee that all the data will be
1636 # sent when using the linger semantics to hard close a socket,
1637 # we send the data and then wait for our peer to release us
1638 # before sending the reset.
rdsmith@chromium.org801f9a62013-03-16 09:35:19 +00001639 data = DataForRange(first_byte, possible_rst)
1640 self.wfile.write(data)
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001641 self.wfile.flush()
rdsmith@chromium.orgd1c45532013-01-22 19:20:29 +00001642 self.server.wait_for_download = True
1643 while self.server.wait_for_download:
1644 self.server.handle_request()
rdsmith@chromium.org4abbbcd2013-01-12 15:57:10 +00001645 l_onoff = 1 # Linger is active.
1646 l_linger = 0 # Seconds to linger for.
1647 self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
1648 struct.pack('ii', l_onoff, l_linger))
1649
1650 # Close all duplicates of the underlying socket to force the RST.
1651 self.wfile.close()
1652 self.rfile.close()
1653 self.connection.close()
1654
1655 return True
1656
initial.commit94958cf2008-07-26 22:42:52 +00001657 def DefaultResponseHandler(self):
1658 """This is the catch-all response handler for requests that aren't handled
1659 by one of the special handlers above.
1660 Note that we specify the content-length as without it the https connection
1661 is not closed properly (and the browser keeps expecting data)."""
1662
1663 contents = "Default response given for path: " + self.path
1664 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001665 self.send_header('Content-Type', 'text/html')
1666 self.send_header('Content-Length', len(contents))
initial.commit94958cf2008-07-26 22:42:52 +00001667 self.end_headers()
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001668 if (self.command != 'HEAD'):
1669 self.wfile.write(contents)
initial.commit94958cf2008-07-26 22:42:52 +00001670 return True
1671
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001672 def RedirectConnectHandler(self):
1673 """Sends a redirect to the CONNECT request for www.redirect.com. This
1674 response is not specified by the RFC, so the browser should not follow
1675 the redirect."""
1676
1677 if (self.path.find("www.redirect.com") < 0):
1678 return False
1679
1680 dest = "http://www.destination.com/foo.js"
1681
1682 self.send_response(302) # moved temporarily
1683 self.send_header('Location', dest)
1684 self.send_header('Connection', 'close')
1685 self.end_headers()
1686 return True
1687
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001688 def ServerAuthConnectHandler(self):
1689 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1690 response doesn't make sense because the proxy server cannot request
1691 server authentication."""
1692
1693 if (self.path.find("www.server-auth.com") < 0):
1694 return False
1695
1696 challenge = 'Basic realm="WallyWorld"'
1697
1698 self.send_response(401) # unauthorized
1699 self.send_header('WWW-Authenticate', challenge)
1700 self.send_header('Connection', 'close')
1701 self.end_headers()
1702 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001703
1704 def DefaultConnectResponseHandler(self):
1705 """This is the catch-all response handler for CONNECT requests that aren't
1706 handled by one of the special handlers above. Real Web servers respond
1707 with 400 to CONNECT requests."""
1708
1709 contents = "Your client has issued a malformed or illegal request."
1710 self.send_response(400) # bad request
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001711 self.send_header('Content-Type', 'text/html')
1712 self.send_header('Content-Length', len(contents))
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001713 self.end_headers()
1714 self.wfile.write(contents)
1715 return True
1716
initial.commit94958cf2008-07-26 22:42:52 +00001717 # called by the redirect handling function when there is no parameter
1718 def sendRedirectHelp(self, redirect_name):
1719 self.send_response(200)
mmenke@chromium.orgbfff75b2011-11-01 02:32:05 +00001720 self.send_header('Content-Type', 'text/html')
initial.commit94958cf2008-07-26 22:42:52 +00001721 self.end_headers()
1722 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1723 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1724 self.wfile.write('</body></html>')
1725
vsevik@chromium.orgf0e997e2011-05-20 09:36:14 +00001726 # called by chunked handling function
1727 def sendChunkHelp(self, chunk):
1728 # Each chunk consists of: chunk size (hex), CRLF, chunk body, CRLF
1729 self.wfile.write('%X\r\n' % len(chunk))
1730 self.wfile.write(chunk)
1731 self.wfile.write('\r\n')
1732
akalin@chromium.org154bb132010-11-12 02:20:27 +00001733
rsimha@chromium.org99a6f172013-01-20 01:10:24 +00001734class OCSPHandler(testserver_base.BasePageHandler):
agl@chromium.org77a9ad92012-03-20 15:14:27 +00001735 def __init__(self, request, client_address, socket_server):
1736 handlers = [self.OCSPResponse]
1737 self.ocsp_response = socket_server.ocsp_response
rsimha@chromium.org99a6f172013-01-20 01:10:24 +00001738 testserver_base.BasePageHandler.__init__(self, request, client_address,
1739 socket_server, [], handlers, [],
1740 handlers, [])
agl@chromium.org77a9ad92012-03-20 15:14:27 +00001741
1742 def OCSPResponse(self):
1743 self.send_response(200)
1744 self.send_header('Content-Type', 'application/ocsp-response')
1745 self.send_header('Content-Length', str(len(self.ocsp_response)))
1746 self.end_headers()
1747
1748 self.wfile.write(self.ocsp_response)
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +00001749
mattm@chromium.org830a3712012-11-07 23:00:07 +00001750
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +00001751class TCPEchoHandler(SocketServer.BaseRequestHandler):
1752 """The RequestHandler class for TCP echo server.
1753
1754 It is instantiated once per connection to the server, and overrides the
1755 handle() method to implement communication to the client.
1756 """
1757
1758 def handle(self):
rtenneti@chromium.org922a8222011-08-16 03:30:45 +00001759 """Handles the request from the client and constructs a response."""
1760
1761 data = self.request.recv(65536).strip()
1762 # Verify the "echo request" message received from the client. Send back
1763 # "echo response" message if "echo request" message is valid.
1764 try:
1765 return_data = echo_message.GetEchoResponseData(data)
1766 if not return_data:
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +00001767 return
rtenneti@chromium.org922a8222011-08-16 03:30:45 +00001768 except ValueError:
1769 return
1770
1771 self.request.send(return_data)
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +00001772
1773
1774class UDPEchoHandler(SocketServer.BaseRequestHandler):
1775 """The RequestHandler class for UDP echo server.
1776
1777 It is instantiated once per connection to the server, and overrides the
1778 handle() method to implement communication to the client.
1779 """
1780
1781 def handle(self):
rtenneti@chromium.org922a8222011-08-16 03:30:45 +00001782 """Handles the request from the client and constructs a response."""
1783
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +00001784 data = self.request[0].strip()
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001785 request_socket = self.request[1]
rtenneti@chromium.org922a8222011-08-16 03:30:45 +00001786 # Verify the "echo request" message received from the client. Send back
1787 # "echo response" message if "echo request" message is valid.
1788 try:
1789 return_data = echo_message.GetEchoResponseData(data)
1790 if not return_data:
1791 return
1792 except ValueError:
1793 return
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001794 request_socket.sendto(return_data, self.client_address)
rtenneti@chromium.orgfc70e5e2011-06-09 05:11:41 +00001795
1796
bashi@chromium.org33233532012-09-08 17:37:24 +00001797class BasicAuthProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
1798 """A request handler that behaves as a proxy server which requires
1799 basic authentication. Only CONNECT, GET and HEAD is supported for now.
1800 """
1801
1802 _AUTH_CREDENTIAL = 'Basic Zm9vOmJhcg==' # foo:bar
1803
1804 def parse_request(self):
1805 """Overrides parse_request to check credential."""
1806
1807 if not BaseHTTPServer.BaseHTTPRequestHandler.parse_request(self):
1808 return False
1809
1810 auth = self.headers.getheader('Proxy-Authorization')
1811 if auth != self._AUTH_CREDENTIAL:
1812 self.send_response(407)
1813 self.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"')
1814 self.end_headers()
1815 return False
1816
1817 return True
1818
1819 def _start_read_write(self, sock):
1820 sock.setblocking(0)
1821 self.request.setblocking(0)
1822 rlist = [self.request, sock]
1823 while True:
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001824 ready_sockets, _unused, errors = select.select(rlist, [], [])
bashi@chromium.org33233532012-09-08 17:37:24 +00001825 if errors:
1826 self.send_response(500)
1827 self.end_headers()
1828 return
1829 for s in ready_sockets:
1830 received = s.recv(1024)
1831 if len(received) == 0:
1832 return
1833 if s == self.request:
1834 other = sock
1835 else:
1836 other = self.request
1837 other.send(received)
1838
1839 def _do_common_method(self):
1840 url = urlparse.urlparse(self.path)
1841 port = url.port
1842 if not port:
1843 if url.scheme == 'http':
1844 port = 80
1845 elif url.scheme == 'https':
1846 port = 443
1847 if not url.hostname or not port:
1848 self.send_response(400)
1849 self.end_headers()
1850 return
1851
1852 if len(url.path) == 0:
1853 path = '/'
1854 else:
1855 path = url.path
1856 if len(url.query) > 0:
1857 path = '%s?%s' % (url.path, url.query)
1858
1859 sock = None
1860 try:
1861 sock = socket.create_connection((url.hostname, port))
1862 sock.send('%s %s %s\r\n' % (
1863 self.command, path, self.protocol_version))
1864 for header in self.headers.headers:
1865 header = header.strip()
1866 if (header.lower().startswith('connection') or
1867 header.lower().startswith('proxy')):
1868 continue
1869 sock.send('%s\r\n' % header)
1870 sock.send('\r\n')
1871 self._start_read_write(sock)
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001872 except Exception:
bashi@chromium.org33233532012-09-08 17:37:24 +00001873 self.send_response(500)
1874 self.end_headers()
1875 finally:
1876 if sock is not None:
1877 sock.close()
1878
1879 def do_CONNECT(self):
1880 try:
1881 pos = self.path.rfind(':')
1882 host = self.path[:pos]
1883 port = int(self.path[pos+1:])
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001884 except Exception:
bashi@chromium.org33233532012-09-08 17:37:24 +00001885 self.send_response(400)
1886 self.end_headers()
1887
1888 try:
1889 sock = socket.create_connection((host, port))
1890 self.send_response(200, 'Connection established')
1891 self.end_headers()
1892 self._start_read_write(sock)
toyoshim@chromium.org9d7219e2012-10-25 03:30:10 +00001893 except Exception:
bashi@chromium.org33233532012-09-08 17:37:24 +00001894 self.send_response(500)
1895 self.end_headers()
1896 finally:
1897 sock.close()
1898
1899 def do_GET(self):
1900 self._do_common_method()
1901
1902 def do_HEAD(self):
1903 self._do_common_method()
1904
1905
mattm@chromium.org830a3712012-11-07 23:00:07 +00001906class ServerRunner(testserver_base.TestServerRunner):
1907 """TestServerRunner for the net test servers."""
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001908
mattm@chromium.org830a3712012-11-07 23:00:07 +00001909 def __init__(self):
1910 super(ServerRunner, self).__init__()
1911 self.__ocsp_server = None
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001912
mattm@chromium.org830a3712012-11-07 23:00:07 +00001913 def __make_data_dir(self):
1914 if self.options.data_dir:
1915 if not os.path.isdir(self.options.data_dir):
1916 raise testserver_base.OptionError('specified data dir not found: ' +
1917 self.options.data_dir + ' exiting...')
1918 my_data_dir = self.options.data_dir
1919 else:
1920 # Create the default path to our data dir, relative to the exe dir.
1921 my_data_dir = os.path.join(BASE_DIR, "..", "..", "..", "..",
1922 "test", "data")
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001923
mattm@chromium.org830a3712012-11-07 23:00:07 +00001924 #TODO(ibrar): Must use Find* funtion defined in google\tools
1925 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001926
mattm@chromium.org830a3712012-11-07 23:00:07 +00001927 return my_data_dir
newt@chromium.org1fc32742012-10-20 00:28:35 +00001928
mattm@chromium.org830a3712012-11-07 23:00:07 +00001929 def create_server(self, server_data):
1930 port = self.options.port
1931 host = self.options.host
newt@chromium.org1fc32742012-10-20 00:28:35 +00001932
mattm@chromium.org830a3712012-11-07 23:00:07 +00001933 if self.options.server_type == SERVER_HTTP:
1934 if self.options.https:
1935 pem_cert_and_key = None
1936 if self.options.cert_and_key_file:
1937 if not os.path.isfile(self.options.cert_and_key_file):
1938 raise testserver_base.OptionError(
1939 'specified server cert file not found: ' +
1940 self.options.cert_and_key_file + ' exiting...')
1941 pem_cert_and_key = file(self.options.cert_and_key_file, 'r').read()
thakis@chromium.org408ea8f2012-11-07 06:57:04 +00001942 else:
mattm@chromium.org830a3712012-11-07 23:00:07 +00001943 # generate a new certificate and run an OCSP server for it.
1944 self.__ocsp_server = OCSPServer((host, 0), OCSPHandler)
wjia@chromium.orgff532f32013-03-18 19:23:44 +00001945 print ('OCSP server started on %s:%d...' %
mattm@chromium.org830a3712012-11-07 23:00:07 +00001946 (host, self.__ocsp_server.server_port))
mattm@chromium.orgdeed82b2012-11-07 04:36:07 +00001947
mattm@chromium.org830a3712012-11-07 23:00:07 +00001948 ocsp_der = None
1949 ocsp_state = None
mattm@chromium.orgdeed82b2012-11-07 04:36:07 +00001950
mattm@chromium.org830a3712012-11-07 23:00:07 +00001951 if self.options.ocsp == 'ok':
1952 ocsp_state = minica.OCSP_STATE_GOOD
1953 elif self.options.ocsp == 'revoked':
1954 ocsp_state = minica.OCSP_STATE_REVOKED
1955 elif self.options.ocsp == 'invalid':
1956 ocsp_state = minica.OCSP_STATE_INVALID
1957 elif self.options.ocsp == 'unauthorized':
1958 ocsp_state = minica.OCSP_STATE_UNAUTHORIZED
1959 elif self.options.ocsp == 'unknown':
1960 ocsp_state = minica.OCSP_STATE_UNKNOWN
1961 else:
1962 raise testserver_base.OptionError('unknown OCSP status: ' +
1963 self.options.ocsp_status)
mattm@chromium.orgdeed82b2012-11-07 04:36:07 +00001964
mattm@chromium.org830a3712012-11-07 23:00:07 +00001965 (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP(
1966 subject = "127.0.0.1",
1967 ocsp_url = ("http://%s:%d/ocsp" %
1968 (host, self.__ocsp_server.server_port)),
agl@chromium.orgdf778142013-07-31 21:57:28 +00001969 ocsp_state = ocsp_state,
1970 serial = self.options.cert_serial)
mattm@chromium.org830a3712012-11-07 23:00:07 +00001971
1972 self.__ocsp_server.ocsp_response = ocsp_der
1973
1974 for ca_cert in self.options.ssl_client_ca:
1975 if not os.path.isfile(ca_cert):
1976 raise testserver_base.OptionError(
1977 'specified trusted client CA file not found: ' + ca_cert +
1978 ' exiting...')
ekasper@google.com3bce2cf2013-12-17 00:25:51 +00001979
1980 stapled_ocsp_response = None
1981 if self.__ocsp_server and self.options.staple_ocsp_response:
1982 stapled_ocsp_response = self.__ocsp_server.ocsp_response
1983
mattm@chromium.org830a3712012-11-07 23:00:07 +00001984 server = HTTPSServer((host, port), TestPageHandler, pem_cert_and_key,
1985 self.options.ssl_client_auth,
1986 self.options.ssl_client_ca,
1987 self.options.ssl_bulk_cipher,
davidben@chromium.org74aa8dd2014-04-11 07:20:26 +00001988 self.options.ssl_key_exchange,
mattm@chromium.org830a3712012-11-07 23:00:07 +00001989 self.options.record_resume,
ekasper@google.com24aa8222013-11-28 13:43:26 +00001990 self.options.tls_intolerant,
ekasper@google.com3bce2cf2013-12-17 00:25:51 +00001991 self.options.signed_cert_timestamps_tls_ext.decode(
agl@chromium.orgd0e11ca2013-12-11 20:16:13 +00001992 "base64"),
ekasper@google.com3bce2cf2013-12-17 00:25:51 +00001993 self.options.fallback_scsv,
1994 stapled_ocsp_response)
wjia@chromium.orgff532f32013-03-18 19:23:44 +00001995 print 'HTTPS server started on %s:%d...' % (host, server.server_port)
mattm@chromium.org830a3712012-11-07 23:00:07 +00001996 else:
1997 server = HTTPServer((host, port), TestPageHandler)
wjia@chromium.orgff532f32013-03-18 19:23:44 +00001998 print 'HTTP server started on %s:%d...' % (host, server.server_port)
mattm@chromium.org830a3712012-11-07 23:00:07 +00001999
2000 server.data_dir = self.__make_data_dir()
2001 server.file_root_url = self.options.file_root_url
2002 server_data['port'] = server.server_port
mattm@chromium.org830a3712012-11-07 23:00:07 +00002003 elif self.options.server_type == SERVER_WEBSOCKET:
2004 # Launch pywebsocket via WebSocketServer.
2005 logger = logging.getLogger()
2006 logger.addHandler(logging.StreamHandler())
2007 # TODO(toyoshim): Remove following os.chdir. Currently this operation
2008 # is required to work correctly. It should be fixed from pywebsocket side.
2009 os.chdir(self.__make_data_dir())
2010 websocket_options = WebSocketOptions(host, port, '.')
2011 if self.options.cert_and_key_file:
2012 websocket_options.use_tls = True
2013 websocket_options.private_key = self.options.cert_and_key_file
2014 websocket_options.certificate = self.options.cert_and_key_file
2015 if self.options.ssl_client_auth:
2016 websocket_options.tls_client_auth = True
2017 if len(self.options.ssl_client_ca) != 1:
2018 raise testserver_base.OptionError(
2019 'one trusted client CA file should be specified')
2020 if not os.path.isfile(self.options.ssl_client_ca[0]):
2021 raise testserver_base.OptionError(
2022 'specified trusted client CA file not found: ' +
2023 self.options.ssl_client_ca[0] + ' exiting...')
2024 websocket_options.tls_client_ca = self.options.ssl_client_ca[0]
2025 server = WebSocketServer(websocket_options)
wjia@chromium.orgff532f32013-03-18 19:23:44 +00002026 print 'WebSocket server started on %s:%d...' % (host, server.server_port)
mattm@chromium.org830a3712012-11-07 23:00:07 +00002027 server_data['port'] = server.server_port
mattm@chromium.org830a3712012-11-07 23:00:07 +00002028 elif self.options.server_type == SERVER_TCP_ECHO:
2029 # Used for generating the key (randomly) that encodes the "echo request"
2030 # message.
2031 random.seed()
2032 server = TCPEchoServer((host, port), TCPEchoHandler)
wjia@chromium.orgff532f32013-03-18 19:23:44 +00002033 print 'Echo TCP server started on port %d...' % server.server_port
mattm@chromium.org830a3712012-11-07 23:00:07 +00002034 server_data['port'] = server.server_port
2035 elif self.options.server_type == SERVER_UDP_ECHO:
2036 # Used for generating the key (randomly) that encodes the "echo request"
2037 # message.
2038 random.seed()
2039 server = UDPEchoServer((host, port), UDPEchoHandler)
wjia@chromium.orgff532f32013-03-18 19:23:44 +00002040 print 'Echo UDP server started on port %d...' % server.server_port
mattm@chromium.org830a3712012-11-07 23:00:07 +00002041 server_data['port'] = server.server_port
2042 elif self.options.server_type == SERVER_BASIC_AUTH_PROXY:
2043 server = HTTPServer((host, port), BasicAuthProxyRequestHandler)
wjia@chromium.orgff532f32013-03-18 19:23:44 +00002044 print 'BasicAuthProxy server started on port %d...' % server.server_port
mattm@chromium.org830a3712012-11-07 23:00:07 +00002045 server_data['port'] = server.server_port
2046 elif self.options.server_type == SERVER_FTP:
2047 my_data_dir = self.__make_data_dir()
2048
2049 # Instantiate a dummy authorizer for managing 'virtual' users
2050 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
2051
2052 # Define a new user having full r/w permissions and a read-only
2053 # anonymous user
2054 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
2055
2056 authorizer.add_anonymous(my_data_dir)
2057
2058 # Instantiate FTP handler class
2059 ftp_handler = pyftpdlib.ftpserver.FTPHandler
2060 ftp_handler.authorizer = authorizer
2061
2062 # Define a customized banner (string returned when client connects)
2063 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
2064 pyftpdlib.ftpserver.__ver__)
2065
2066 # Instantiate FTP server class and listen to address:port
2067 server = pyftpdlib.ftpserver.FTPServer((host, port), ftp_handler)
2068 server_data['port'] = server.socket.getsockname()[1]
wjia@chromium.orgff532f32013-03-18 19:23:44 +00002069 print 'FTP server started on port %d...' % server_data['port']
erikkay@google.comd5182ff2009-01-08 20:45:27 +00002070 else:
mattm@chromium.org830a3712012-11-07 23:00:07 +00002071 raise testserver_base.OptionError('unknown server type' +
2072 self.options.server_type)
erikkay@google.com70397b62008-12-30 21:49:21 +00002073
mattm@chromium.org830a3712012-11-07 23:00:07 +00002074 return server
erikkay@google.comd5182ff2009-01-08 20:45:27 +00002075
mattm@chromium.org830a3712012-11-07 23:00:07 +00002076 def run_server(self):
2077 if self.__ocsp_server:
2078 self.__ocsp_server.serve_forever_on_thread()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00002079
mattm@chromium.org830a3712012-11-07 23:00:07 +00002080 testserver_base.TestServerRunner.run_server(self)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00002081
mattm@chromium.org830a3712012-11-07 23:00:07 +00002082 if self.__ocsp_server:
2083 self.__ocsp_server.stop_serving()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00002084
mattm@chromium.org830a3712012-11-07 23:00:07 +00002085 def add_options(self):
2086 testserver_base.TestServerRunner.add_options(self)
2087 self.option_parser.add_option('-f', '--ftp', action='store_const',
2088 const=SERVER_FTP, default=SERVER_HTTP,
2089 dest='server_type',
2090 help='start up an FTP server.')
mattm@chromium.org830a3712012-11-07 23:00:07 +00002091 self.option_parser.add_option('--tcp-echo', action='store_const',
2092 const=SERVER_TCP_ECHO, default=SERVER_HTTP,
2093 dest='server_type',
2094 help='start up a tcp echo server.')
2095 self.option_parser.add_option('--udp-echo', action='store_const',
2096 const=SERVER_UDP_ECHO, default=SERVER_HTTP,
2097 dest='server_type',
2098 help='start up a udp echo server.')
2099 self.option_parser.add_option('--basic-auth-proxy', action='store_const',
2100 const=SERVER_BASIC_AUTH_PROXY,
2101 default=SERVER_HTTP, dest='server_type',
2102 help='start up a proxy server which requires '
2103 'basic authentication.')
2104 self.option_parser.add_option('--websocket', action='store_const',
2105 const=SERVER_WEBSOCKET, default=SERVER_HTTP,
2106 dest='server_type',
2107 help='start up a WebSocket server.')
mattm@chromium.org830a3712012-11-07 23:00:07 +00002108 self.option_parser.add_option('--https', action='store_true',
2109 dest='https', help='Specify that https '
2110 'should be used.')
2111 self.option_parser.add_option('--cert-and-key-file',
2112 dest='cert_and_key_file', help='specify the '
2113 'path to the file containing the certificate '
2114 'and private key for the server in PEM '
2115 'format')
2116 self.option_parser.add_option('--ocsp', dest='ocsp', default='ok',
2117 help='The type of OCSP response generated '
2118 'for the automatically generated '
2119 'certificate. One of [ok,revoked,invalid]')
agl@chromium.orgdf778142013-07-31 21:57:28 +00002120 self.option_parser.add_option('--cert-serial', dest='cert_serial',
2121 default=0, type=int,
2122 help='If non-zero then the generated '
2123 'certificate will have this serial number')
mattm@chromium.org830a3712012-11-07 23:00:07 +00002124 self.option_parser.add_option('--tls-intolerant', dest='tls_intolerant',
2125 default='0', type='int',
2126 help='If nonzero, certain TLS connections '
2127 'will be aborted in order to test version '
2128 'fallback. 1 means all TLS versions will be '
2129 'aborted. 2 means TLS 1.1 or higher will be '
2130 'aborted. 3 means TLS 1.2 or higher will be '
2131 'aborted.')
ekasper@google.com3bce2cf2013-12-17 00:25:51 +00002132 self.option_parser.add_option('--signed-cert-timestamps-tls-ext',
2133 dest='signed_cert_timestamps_tls_ext',
ekasper@google.com24aa8222013-11-28 13:43:26 +00002134 default='',
2135 help='Base64 encoded SCT list. If set, '
2136 'server will respond with a '
2137 'signed_certificate_timestamp TLS extension '
2138 'whenever the client supports it.')
agl@chromium.orgd0e11ca2013-12-11 20:16:13 +00002139 self.option_parser.add_option('--fallback-scsv', dest='fallback_scsv',
2140 default=False, const=True,
2141 action='store_const',
2142 help='If given, TLS_FALLBACK_SCSV support '
2143 'will be enabled. This causes the server to '
2144 'reject fallback connections from compatible '
2145 'clients (e.g. Chrome).')
ekasper@google.com3bce2cf2013-12-17 00:25:51 +00002146 self.option_parser.add_option('--staple-ocsp-response',
2147 dest='staple_ocsp_response',
2148 default=False, action='store_true',
2149 help='If set, server will staple the OCSP '
2150 'response whenever OCSP is on and the client '
2151 'supports OCSP stapling.')
mattm@chromium.org830a3712012-11-07 23:00:07 +00002152 self.option_parser.add_option('--https-record-resume',
2153 dest='record_resume', const=True,
2154 default=False, action='store_const',
2155 help='Record resumption cache events rather '
2156 'than resuming as normal. Allows the use of '
2157 'the /ssl-session-cache request')
2158 self.option_parser.add_option('--ssl-client-auth', action='store_true',
2159 help='Require SSL client auth on every '
2160 'connection.')
2161 self.option_parser.add_option('--ssl-client-ca', action='append',
2162 default=[], help='Specify that the client '
2163 'certificate request should include the CA '
2164 'named in the subject of the DER-encoded '
2165 'certificate contained in the specified '
2166 'file. This option may appear multiple '
2167 'times, indicating multiple CA names should '
2168 'be sent in the request.')
2169 self.option_parser.add_option('--ssl-bulk-cipher', action='append',
2170 help='Specify the bulk encryption '
2171 'algorithm(s) that will be accepted by the '
2172 'SSL server. Valid values are "aes256", '
2173 '"aes128", "3des", "rc4". If omitted, all '
2174 'algorithms will be used. This option may '
2175 'appear multiple times, indicating '
2176 'multiple algorithms should be enabled.');
davidben@chromium.org74aa8dd2014-04-11 07:20:26 +00002177 self.option_parser.add_option('--ssl-key-exchange', action='append',
2178 help='Specify the key exchange algorithm(s)'
2179 'that will be accepted by the SSL server. '
2180 'Valid values are "rsa", "dhe_rsa". If '
2181 'omitted, all algorithms will be used. This '
2182 'option may appear multiple times, '
2183 'indicating multiple algorithms should be '
2184 'enabled.');
mattm@chromium.org830a3712012-11-07 23:00:07 +00002185 self.option_parser.add_option('--file-root-url', default='/files/',
2186 help='Specify a root URL for files served.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00002187
toyoshim@chromium.org16f57522012-10-24 06:14:38 +00002188
initial.commit94958cf2008-07-26 22:42:52 +00002189if __name__ == '__main__':
mattm@chromium.org830a3712012-11-07 23:00:07 +00002190 sys.exit(ServerRunner().main())