blob: 56d7c0ed89792b98478cbb574f70c5f4da82f648 [file] [log] [blame]
initial.commit94958cf2008-07-26 22:42:52 +00001#!/usr/bin/python2.4
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00002# Copyright (c) 2006-2010 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
6"""This is a simple HTTP server used for testing Chrome.
7
8It supports several test URLs, as specified by the handlers in TestPageHandler.
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00009By default, it listens on an ephemeral port and sends the port number back to
10the originating process over a pipe. The originating process can specify an
11explicit port if necessary.
initial.commit94958cf2008-07-26 22:42:52 +000012It can use https if you specify the flag --https=CERT where CERT is the path
13to a pem file containing the certificate and private key that should be used.
initial.commit94958cf2008-07-26 22:42:52 +000014"""
15
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +000016import asyncore
initial.commit94958cf2008-07-26 22:42:52 +000017import base64
18import BaseHTTPServer
19import cgi
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +000020import errno
initial.commit94958cf2008-07-26 22:42:52 +000021import optparse
22import os
23import re
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +000024import select
akalin@chromium.org18e34882010-11-26 07:10:41 +000025import simplejson
initial.commit94958cf2008-07-26 22:42:52 +000026import SocketServer
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +000027import socket
initial.commit94958cf2008-07-26 22:42:52 +000028import sys
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +000029import struct
initial.commit94958cf2008-07-26 22:42:52 +000030import time
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +000031import urlparse
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +000032import warnings
33
34# Ignore deprecation warnings, they make our output more cluttered.
35warnings.filterwarnings("ignore", category=DeprecationWarning)
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000036
37import pyftpdlib.ftpserver
initial.commit94958cf2008-07-26 22:42:52 +000038import tlslite
39import tlslite.api
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000040
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +000041try:
42 import hashlib
43 _new_md5 = hashlib.md5
44except ImportError:
45 import md5
46 _new_md5 = md5.new
47
davidben@chromium.org06fcf202010-09-22 18:15:23 +000048if sys.platform == 'win32':
49 import msvcrt
50
maruel@chromium.org756cf982009-03-05 12:46:38 +000051SERVER_HTTP = 0
erikkay@google.comd5182ff2009-01-08 20:45:27 +000052SERVER_FTP = 1
akalin@chromium.org154bb132010-11-12 02:20:27 +000053SERVER_SYNC = 2
initial.commit94958cf2008-07-26 22:42:52 +000054
akalin@chromium.orgf8479c62010-11-27 01:52:52 +000055# Using debug() seems to cause hangs on XP: see http://crbug.com/64515 .
initial.commit94958cf2008-07-26 22:42:52 +000056debug_output = sys.stderr
57def debug(str):
58 debug_output.write(str + "\n")
59 debug_output.flush()
60
61class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
62 """This is a specialization of of BaseHTTPServer to allow it
63 to be exited cleanly (by setting its "stop" member to True)."""
64
65 def serve_forever(self):
66 self.stop = False
tonyg@chromium.org75054202010-03-31 22:06:10 +000067 self.nonce_time = None
initial.commit94958cf2008-07-26 22:42:52 +000068 while not self.stop:
69 self.handle_request()
70 self.socket.close()
71
72class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
73 """This is a specialization of StoppableHTTPerver that add https support."""
74
davidben@chromium.org31282a12010-08-07 01:10:02 +000075 def __init__(self, server_address, request_hander_class, cert_path,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000076 ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers):
initial.commit94958cf2008-07-26 22:42:52 +000077 s = open(cert_path).read()
78 x509 = tlslite.api.X509()
79 x509.parse(s)
80 self.cert_chain = tlslite.api.X509CertChain([x509])
81 s = open(cert_path).read()
82 self.private_key = tlslite.api.parsePEMKey(s, private=True)
davidben@chromium.org31282a12010-08-07 01:10:02 +000083 self.ssl_client_auth = ssl_client_auth
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +000084 self.ssl_client_cas = []
85 for ca_file in ssl_client_cas:
86 s = open(ca_file).read()
87 x509 = tlslite.api.X509()
88 x509.parse(s)
89 self.ssl_client_cas.append(x509.subject)
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000090 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
91 if ssl_bulk_ciphers is not None:
92 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
initial.commit94958cf2008-07-26 22:42:52 +000093
94 self.session_cache = tlslite.api.SessionCache()
95 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
96
97 def handshake(self, tlsConnection):
98 """Creates the SSL connection."""
99 try:
100 tlsConnection.handshakeServer(certChain=self.cert_chain,
101 privateKey=self.private_key,
davidben@chromium.org31282a12010-08-07 01:10:02 +0000102 sessionCache=self.session_cache,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000103 reqCert=self.ssl_client_auth,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +0000104 settings=self.ssl_handshake_settings,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000105 reqCAs=self.ssl_client_cas)
initial.commit94958cf2008-07-26 22:42:52 +0000106 tlsConnection.ignoreAbruptClose = True
107 return True
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +0000108 except tlslite.api.TLSAbruptCloseError:
109 # Ignore abrupt close.
110 return True
initial.commit94958cf2008-07-26 22:42:52 +0000111 except tlslite.api.TLSError, error:
112 print "Handshake failure:", str(error)
113 return False
114
akalin@chromium.org154bb132010-11-12 02:20:27 +0000115
116class SyncHTTPServer(StoppableHTTPServer):
117 """An HTTP server that handles sync commands."""
118
119 def __init__(self, server_address, request_handler_class):
120 # We import here to avoid pulling in chromiumsync's dependencies
121 # unless strictly necessary.
122 import chromiumsync
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000123 import xmppserver
akalin@chromium.org154bb132010-11-12 02:20:27 +0000124 StoppableHTTPServer.__init__(self, server_address, request_handler_class)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000125 self._sync_handler = chromiumsync.TestServer()
126 self._xmpp_socket_map = {}
127 self._xmpp_server = xmppserver.XmppServer(
128 self._xmpp_socket_map, ('localhost', 0))
129 self.xmpp_port = self._xmpp_server.getsockname()[1]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000130
131 def HandleCommand(self, query, raw_request):
132 return self._sync_handler.HandleCommand(query, raw_request)
133
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000134 def HandleRequestNoBlock(self):
135 """Handles a single request.
136
137 Copied from SocketServer._handle_request_noblock().
138 """
139 try:
140 request, client_address = self.get_request()
141 except socket.error:
142 return
143 if self.verify_request(request, client_address):
144 try:
145 self.process_request(request, client_address)
146 except:
147 self.handle_error(request, client_address)
148 self.close_request(request)
149
150 def serve_forever(self):
151 """This is a merge of asyncore.loop() and SocketServer.serve_forever().
152 """
153
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000154 def HandleXmppSocket(fd, socket_map, handler):
155 """Runs the handler for the xmpp connection for fd.
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000156
157 Adapted from asyncore.read() et al.
158 """
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000159 xmpp_connection = socket_map.get(fd)
160 # This could happen if a previous handler call caused fd to get
161 # removed from socket_map.
162 if xmpp_connection is None:
163 return
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000164 try:
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000165 handler(xmpp_connection)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000166 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
167 raise
168 except:
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000169 xmpp_connection.handle_error()
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000170
171 while True:
172 read_fds = [ self.fileno() ]
173 write_fds = []
174 exceptional_fds = []
175
176 for fd, xmpp_connection in self._xmpp_socket_map.items():
177 is_r = xmpp_connection.readable()
178 is_w = xmpp_connection.writable()
179 if is_r:
180 read_fds.append(fd)
181 if is_w:
182 write_fds.append(fd)
183 if is_r or is_w:
184 exceptional_fds.append(fd)
185
186 try:
187 read_fds, write_fds, exceptional_fds = (
188 select.select(read_fds, write_fds, exceptional_fds))
189 except select.error, err:
190 if err.args[0] != errno.EINTR:
191 raise
192 else:
193 continue
194
195 for fd in read_fds:
196 if fd == self.fileno():
197 self.HandleRequestNoBlock()
198 continue
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000199 HandleXmppSocket(fd, self._xmpp_socket_map,
200 asyncore.dispatcher.handle_read_event)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000201
202 for fd in write_fds:
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000203 HandleXmppSocket(fd, self._xmpp_socket_map,
204 asyncore.dispatcher.handle_write_event)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000205
206 for fd in exceptional_fds:
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000207 HandleXmppSocket(fd, self._xmpp_socket_map,
208 asyncore.dispatcher.handle_expt_event)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000209
akalin@chromium.org154bb132010-11-12 02:20:27 +0000210
211class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
212
213 def __init__(self, request, client_address, socket_server,
214 connect_handlers, get_handlers, post_handlers, put_handlers):
215 self._connect_handlers = connect_handlers
216 self._get_handlers = get_handlers
217 self._post_handlers = post_handlers
218 self._put_handlers = put_handlers
219 BaseHTTPServer.BaseHTTPRequestHandler.__init__(
220 self, request, client_address, socket_server)
221
222 def log_request(self, *args, **kwargs):
223 # Disable request logging to declutter test log output.
224 pass
225
226 def _ShouldHandleRequest(self, handler_name):
227 """Determines if the path can be handled by the handler.
228
229 We consider a handler valid if the path begins with the
230 handler name. It can optionally be followed by "?*", "/*".
231 """
232
233 pattern = re.compile('%s($|\?|/).*' % handler_name)
234 return pattern.match(self.path)
235
236 def do_CONNECT(self):
237 for handler in self._connect_handlers:
238 if handler():
239 return
240
241 def do_GET(self):
242 for handler in self._get_handlers:
243 if handler():
244 return
245
246 def do_POST(self):
247 for handler in self._post_handlers:
248 if handler():
249 return
250
251 def do_PUT(self):
252 for handler in self._put_handlers:
253 if handler():
254 return
255
256
257class TestPageHandler(BasePageHandler):
initial.commit94958cf2008-07-26 22:42:52 +0000258
259 def __init__(self, request, client_address, socket_server):
akalin@chromium.org154bb132010-11-12 02:20:27 +0000260 connect_handlers = [
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000261 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000262 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000263 self.DefaultConnectResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000264 get_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000265 self.NoCacheMaxAgeTimeHandler,
266 self.NoCacheTimeHandler,
267 self.CacheTimeHandler,
268 self.CacheExpiresHandler,
269 self.CacheProxyRevalidateHandler,
270 self.CachePrivateHandler,
271 self.CachePublicHandler,
272 self.CacheSMaxAgeHandler,
273 self.CacheMustRevalidateHandler,
274 self.CacheMustRevalidateMaxAgeHandler,
275 self.CacheNoStoreHandler,
276 self.CacheNoStoreMaxAgeHandler,
277 self.CacheNoTransformHandler,
278 self.DownloadHandler,
279 self.DownloadFinishHandler,
280 self.EchoHeader,
ananta@chromium.org219b2062009-10-23 16:09:41 +0000281 self.EchoHeaderOverride,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000282 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000283 self.FileHandler,
284 self.RealFileWithCommonHeaderHandler,
285 self.RealBZ2FileWithCommonHeaderHandler,
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000286 self.SetCookieHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000287 self.AuthBasicHandler,
288 self.AuthDigestHandler,
289 self.SlowServerHandler,
290 self.ContentTypeHandler,
291 self.ServerRedirectHandler,
292 self.ClientRedirectHandler,
tony@chromium.org03266982010-03-05 03:18:42 +0000293 self.MultipartHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000294 self.DefaultResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000295 post_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000296 self.EchoTitleHandler,
297 self.EchoAllHandler,
mnissler@chromium.org7c939802010-11-11 08:47:14 +0000298 self.EchoHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000299 self.DeviceManagementHandler] + get_handlers
300 put_handlers = [
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000301 self.EchoTitleHandler,
302 self.EchoAllHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000303 self.EchoHandler] + get_handlers
initial.commit94958cf2008-07-26 22:42:52 +0000304
maruel@google.come250a9b2009-03-10 17:39:46 +0000305 self._mime_types = {
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000306 'crx' : 'application/x-chrome-extension',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000307 'exe' : 'application/octet-stream',
maruel@google.come250a9b2009-03-10 17:39:46 +0000308 'gif': 'image/gif',
309 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000310 'jpg' : 'image/jpeg',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000311 'pdf' : 'application/pdf',
312 'xml' : 'text/xml'
maruel@google.come250a9b2009-03-10 17:39:46 +0000313 }
initial.commit94958cf2008-07-26 22:42:52 +0000314 self._default_mime_type = 'text/html'
315
akalin@chromium.org154bb132010-11-12 02:20:27 +0000316 BasePageHandler.__init__(self, request, client_address, socket_server,
317 connect_handlers, get_handlers, post_handlers,
318 put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000319
initial.commit94958cf2008-07-26 22:42:52 +0000320 def GetMIMETypeFromName(self, file_name):
321 """Returns the mime type for the specified file_name. So far it only looks
322 at the file extension."""
323
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000324 (shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000325 if len(extension) == 0:
326 # no extension.
327 return self._default_mime_type
328
ericroman@google.comc17ca532009-05-07 03:51:05 +0000329 # extension starts with a dot, so we need to remove it
330 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000331
initial.commit94958cf2008-07-26 22:42:52 +0000332 def NoCacheMaxAgeTimeHandler(self):
333 """This request handler yields a page with the title set to the current
334 system time, and no caching requested."""
335
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000336 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000337 return False
338
339 self.send_response(200)
340 self.send_header('Cache-Control', 'max-age=0')
341 self.send_header('Content-type', 'text/html')
342 self.end_headers()
343
maruel@google.come250a9b2009-03-10 17:39:46 +0000344 self.wfile.write('<html><head><title>%s</title></head></html>' %
345 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000346
347 return True
348
349 def NoCacheTimeHandler(self):
350 """This request handler yields a page with the title set to the current
351 system time, and no caching requested."""
352
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000353 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000354 return False
355
356 self.send_response(200)
357 self.send_header('Cache-Control', 'no-cache')
358 self.send_header('Content-type', 'text/html')
359 self.end_headers()
360
maruel@google.come250a9b2009-03-10 17:39:46 +0000361 self.wfile.write('<html><head><title>%s</title></head></html>' %
362 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000363
364 return True
365
366 def CacheTimeHandler(self):
367 """This request handler yields a page with the title set to the current
368 system time, and allows caching for one minute."""
369
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000370 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000371 return False
372
373 self.send_response(200)
374 self.send_header('Cache-Control', 'max-age=60')
375 self.send_header('Content-type', 'text/html')
376 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 CacheExpiresHandler(self):
384 """This request handler yields a page with the title set to the current
385 system time, and set the page to expire on 1 Jan 2099."""
386
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000387 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000388 return False
389
390 self.send_response(200)
391 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
392 self.send_header('Content-type', 'text/html')
393 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 CacheProxyRevalidateHandler(self):
401 """This request handler yields a page with the title set to the current
402 system time, and allows caching for 60 seconds"""
403
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000404 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000405 return False
406
407 self.send_response(200)
408 self.send_header('Content-type', 'text/html')
409 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
410 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 CachePrivateHandler(self):
418 """This request handler yields a page with the title set to the current
419 system time, and allows caching for 5 seconds."""
420
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000421 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000422 return False
423
424 self.send_response(200)
425 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000426 self.send_header('Cache-Control', 'max-age=3, private')
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 CachePublicHandler(self):
435 """This request handler yields a page with the title set to the current
436 system time, and allows caching for 5 seconds."""
437
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000438 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000439 return False
440
441 self.send_response(200)
442 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000443 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000444 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 CacheSMaxAgeHandler(self):
452 """This request handler yields a page with the title set to the current
453 system time, and does not allow for caching."""
454
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000455 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000456 return False
457
458 self.send_response(200)
459 self.send_header('Content-type', 'text/html')
460 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
461 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 CacheMustRevalidateHandler(self):
469 """This request handler yields a page with the title set to the current
470 system time, and does not allow caching."""
471
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000472 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000473 return False
474
475 self.send_response(200)
476 self.send_header('Content-type', 'text/html')
477 self.send_header('Cache-Control', 'must-revalidate')
478 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 CacheMustRevalidateMaxAgeHandler(self):
486 """This request handler yields a page with the title set to the current
487 system time, and does not allow caching event though max-age of 60
488 seconds is specified."""
489
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000490 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000491 return False
492
493 self.send_response(200)
494 self.send_header('Content-type', 'text/html')
495 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
496 self.end_headers()
497
maruel@google.come250a9b2009-03-10 17:39:46 +0000498 self.wfile.write('<html><head><title>%s</title></head></html>' %
499 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000500
501 return True
502
initial.commit94958cf2008-07-26 22:42:52 +0000503 def CacheNoStoreHandler(self):
504 """This request handler yields a page with the title set to the current
505 system time, and does not allow the page to be stored."""
506
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000507 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000508 return False
509
510 self.send_response(200)
511 self.send_header('Content-type', 'text/html')
512 self.send_header('Cache-Control', 'no-store')
513 self.end_headers()
514
maruel@google.come250a9b2009-03-10 17:39:46 +0000515 self.wfile.write('<html><head><title>%s</title></head></html>' %
516 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000517
518 return True
519
520 def CacheNoStoreMaxAgeHandler(self):
521 """This request handler yields a page with the title set to the current
522 system time, and does not allow the page to be stored even though max-age
523 of 60 seconds is specified."""
524
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000525 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000526 return False
527
528 self.send_response(200)
529 self.send_header('Content-type', 'text/html')
530 self.send_header('Cache-Control', 'max-age=60, no-store')
531 self.end_headers()
532
maruel@google.come250a9b2009-03-10 17:39:46 +0000533 self.wfile.write('<html><head><title>%s</title></head></html>' %
534 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000535
536 return True
537
538
539 def CacheNoTransformHandler(self):
540 """This request handler yields a page with the title set to the current
541 system time, and does not allow the content to transformed during
542 user-agent caching"""
543
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000544 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000545 return False
546
547 self.send_response(200)
548 self.send_header('Content-type', 'text/html')
549 self.send_header('Cache-Control', 'no-transform')
550 self.end_headers()
551
maruel@google.come250a9b2009-03-10 17:39:46 +0000552 self.wfile.write('<html><head><title>%s</title></head></html>' %
553 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000554
555 return True
556
557 def EchoHeader(self):
558 """This handler echoes back the value of a specific request header."""
ananta@chromium.org219b2062009-10-23 16:09:41 +0000559 """The only difference between this function and the EchoHeaderOverride"""
560 """function is in the parameter being passed to the helper function"""
561 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000562
ananta@chromium.org219b2062009-10-23 16:09:41 +0000563 def EchoHeaderOverride(self):
564 """This handler echoes back the value of a specific request header."""
565 """The UrlRequest unit tests also execute for ChromeFrame which uses"""
566 """IE to issue HTTP requests using the host network stack."""
567 """The Accept and Charset tests which expect the server to echo back"""
568 """the corresponding headers fail here as IE returns cached responses"""
569 """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
570 """treats this request as a new request and does not cache it."""
571 return self.EchoHeaderHelper("/echoheaderoverride")
572
573 def EchoHeaderHelper(self, echo_header):
574 """This function echoes back the value of the request header passed in."""
575 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000576 return False
577
578 query_char = self.path.find('?')
579 if query_char != -1:
580 header_name = self.path[query_char+1:]
581
582 self.send_response(200)
583 self.send_header('Content-type', 'text/plain')
584 self.send_header('Cache-control', 'max-age=60000')
585 # insert a vary header to properly indicate that the cachability of this
586 # request is subject to value of the request header being echoed.
587 if len(header_name) > 0:
588 self.send_header('Vary', header_name)
589 self.end_headers()
590
591 if len(header_name) > 0:
592 self.wfile.write(self.headers.getheader(header_name))
593
594 return True
595
596 def EchoHandler(self):
597 """This handler just echoes back the payload of the request, for testing
598 form submission."""
599
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000600 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000601 return False
602
603 self.send_response(200)
604 self.send_header('Content-type', 'text/html')
605 self.end_headers()
606 length = int(self.headers.getheader('content-length'))
607 request = self.rfile.read(length)
608 self.wfile.write(request)
609 return True
610
611 def EchoTitleHandler(self):
612 """This handler is like Echo, but sets the page title to the request."""
613
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000614 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000615 return False
616
617 self.send_response(200)
618 self.send_header('Content-type', 'text/html')
619 self.end_headers()
620 length = int(self.headers.getheader('content-length'))
621 request = self.rfile.read(length)
622 self.wfile.write('<html><head><title>')
623 self.wfile.write(request)
624 self.wfile.write('</title></head></html>')
625 return True
626
627 def EchoAllHandler(self):
628 """This handler yields a (more) human-readable page listing information
629 about the request header & contents."""
630
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000631 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000632 return False
633
634 self.send_response(200)
635 self.send_header('Content-type', 'text/html')
636 self.end_headers()
637 self.wfile.write('<html><head><style>'
638 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
639 '</style></head><body>'
640 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000641 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000642 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000643
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000644 if self.command == 'POST' or self.command == 'PUT':
ericroman@google.coma47622b2008-11-15 04:36:51 +0000645 length = int(self.headers.getheader('content-length'))
646 qs = self.rfile.read(length)
647 params = cgi.parse_qs(qs, keep_blank_values=1)
648
649 for param in params:
650 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000651
652 self.wfile.write('</pre>')
653
654 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
655
656 self.wfile.write('</body></html>')
657 return True
658
659 def DownloadHandler(self):
660 """This handler sends a downloadable file with or without reporting
661 the size (6K)."""
662
663 if self.path.startswith("/download-unknown-size"):
664 send_length = False
665 elif self.path.startswith("/download-known-size"):
666 send_length = True
667 else:
668 return False
669
670 #
671 # The test which uses this functionality is attempting to send
672 # small chunks of data to the client. Use a fairly large buffer
673 # so that we'll fill chrome's IO buffer enough to force it to
674 # actually write the data.
675 # See also the comments in the client-side of this test in
676 # download_uitest.cc
677 #
678 size_chunk1 = 35*1024
679 size_chunk2 = 10*1024
680
681 self.send_response(200)
682 self.send_header('Content-type', 'application/octet-stream')
683 self.send_header('Cache-Control', 'max-age=0')
684 if send_length:
685 self.send_header('Content-Length', size_chunk1 + size_chunk2)
686 self.end_headers()
687
688 # First chunk of data:
689 self.wfile.write("*" * size_chunk1)
690 self.wfile.flush()
691
692 # handle requests until one of them clears this flag.
693 self.server.waitForDownload = True
694 while self.server.waitForDownload:
695 self.server.handle_request()
696
697 # Second chunk of data:
698 self.wfile.write("*" * size_chunk2)
699 return True
700
701 def DownloadFinishHandler(self):
702 """This handler just tells the server to finish the current download."""
703
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000704 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000705 return False
706
707 self.server.waitForDownload = False
708 self.send_response(200)
709 self.send_header('Content-type', 'text/html')
710 self.send_header('Cache-Control', 'max-age=0')
711 self.end_headers()
712 return True
713
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000714 def _ReplaceFileData(self, data, query_parameters):
715 """Replaces matching substrings in a file.
716
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000717 If the 'replace_text' URL query parameter is present, it is expected to be
718 of the form old_text:new_text, which indicates that any old_text strings in
719 the file are replaced with new_text. Multiple 'replace_text' parameters may
720 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000721
722 If the parameters are not present, |data| is returned.
723 """
724 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000725 replace_text_values = query_dict.get('replace_text', [])
726 for replace_text_value in replace_text_values:
727 replace_text_args = replace_text_value.split(':')
728 if len(replace_text_args) != 2:
729 raise ValueError(
730 'replace_text must be of form old_text:new_text. Actual value: %s' %
731 replace_text_value)
732 old_text_b64, new_text_b64 = replace_text_args
733 old_text = base64.urlsafe_b64decode(old_text_b64)
734 new_text = base64.urlsafe_b64decode(new_text_b64)
735 data = data.replace(old_text, new_text)
736 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000737
initial.commit94958cf2008-07-26 22:42:52 +0000738 def FileHandler(self):
739 """This handler sends the contents of the requested file. Wow, it's like
740 a real webserver!"""
741
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000742 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000743 if not self.path.startswith(prefix):
744 return False
745
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000746 # Consume a request body if present.
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000747 if self.command == 'POST' or self.command == 'PUT' :
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000748 self.rfile.read(int(self.headers.getheader('content-length')))
749
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000750 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
751 sub_path = url_path[len(prefix):]
752 entries = sub_path.split('/')
753 file_path = os.path.join(self.server.data_dir, *entries)
754 if os.path.isdir(file_path):
755 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000756
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000757 if not os.path.isfile(file_path):
758 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000759 self.send_error(404)
760 return True
761
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000762 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000763 data = f.read()
764 f.close()
765
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000766 data = self._ReplaceFileData(data, query)
767
initial.commit94958cf2008-07-26 22:42:52 +0000768 # If file.mock-http-headers exists, it contains the headers we
769 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000770 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000771 if os.path.isfile(headers_path):
772 f = open(headers_path, "r")
773
774 # "HTTP/1.1 200 OK"
775 response = f.readline()
776 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
777 self.send_response(int(status_code))
778
779 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000780 header_values = re.findall('(\S+):\s*(.*)', line)
781 if len(header_values) > 0:
782 # "name: value"
783 name, value = header_values[0]
784 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000785 f.close()
786 else:
787 # Could be more generic once we support mime-type sniffing, but for
788 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000789
790 range = self.headers.get('Range')
791 if range and range.startswith('bytes='):
792 # Note this doesn't handle all valid byte range values (i.e. open ended
793 # ones), just enough for what we needed so far.
794 range = range[6:].split('-')
795 start = int(range[0])
796 end = int(range[1])
797
798 self.send_response(206)
799 content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
800 str(len(data))
801 self.send_header('Content-Range', content_range)
802 data = data[start: end + 1]
803 else:
804 self.send_response(200)
805
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000806 self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000807 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000808 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000809 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000810 self.end_headers()
811
812 self.wfile.write(data)
813
814 return True
815
816 def RealFileWithCommonHeaderHandler(self):
817 """This handler sends the contents of the requested file without the pseudo
818 http head!"""
819
820 prefix='/realfiles/'
821 if not self.path.startswith(prefix):
822 return False
823
824 file = self.path[len(prefix):]
825 path = os.path.join(self.server.data_dir, file)
826
827 try:
828 f = open(path, "rb")
829 data = f.read()
830 f.close()
831
832 # just simply set the MIME as octal stream
833 self.send_response(200)
834 self.send_header('Content-type', 'application/octet-stream')
835 self.end_headers()
836
837 self.wfile.write(data)
838 except:
839 self.send_error(404)
840
841 return True
842
843 def RealBZ2FileWithCommonHeaderHandler(self):
844 """This handler sends the bzip2 contents of the requested file with
845 corresponding Content-Encoding field in http head!"""
846
847 prefix='/realbz2files/'
848 if not self.path.startswith(prefix):
849 return False
850
851 parts = self.path.split('?')
852 file = parts[0][len(prefix):]
853 path = os.path.join(self.server.data_dir, file) + '.bz2'
854
855 if len(parts) > 1:
856 options = parts[1]
857 else:
858 options = ''
859
860 try:
861 self.send_response(200)
862 accept_encoding = self.headers.get("Accept-Encoding")
863 if accept_encoding.find("bzip2") != -1:
864 f = open(path, "rb")
865 data = f.read()
866 f.close()
867 self.send_header('Content-Encoding', 'bzip2')
868 self.send_header('Content-type', 'application/x-bzip2')
869 self.end_headers()
870 if options == 'incremental-header':
871 self.wfile.write(data[:1])
872 self.wfile.flush()
873 time.sleep(1.0)
874 self.wfile.write(data[1:])
875 else:
876 self.wfile.write(data)
877 else:
878 """client do not support bzip2 format, send pseudo content
879 """
880 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
881 self.end_headers()
882 self.wfile.write("you do not support bzip2 encoding")
883 except:
884 self.send_error(404)
885
886 return True
887
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000888 def SetCookieHandler(self):
889 """This handler just sets a cookie, for testing cookie handling."""
890
891 if not self._ShouldHandleRequest("/set-cookie"):
892 return False
893
894 query_char = self.path.find('?')
895 if query_char != -1:
896 cookie_values = self.path[query_char + 1:].split('&')
897 else:
898 cookie_values = ("",)
899 self.send_response(200)
900 self.send_header('Content-type', 'text/html')
901 for cookie_value in cookie_values:
902 self.send_header('Set-Cookie', '%s' % cookie_value)
903 self.end_headers()
904 for cookie_value in cookie_values:
905 self.wfile.write('%s' % cookie_value)
906 return True
907
initial.commit94958cf2008-07-26 22:42:52 +0000908 def AuthBasicHandler(self):
909 """This handler tests 'Basic' authentication. It just sends a page with
910 title 'user/pass' if you succeed."""
911
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000912 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000913 return False
914
915 username = userpass = password = b64str = ""
916
ericroman@google.com239b4d82009-03-27 04:00:22 +0000917 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
918
initial.commit94958cf2008-07-26 22:42:52 +0000919 auth = self.headers.getheader('authorization')
920 try:
921 if not auth:
922 raise Exception('no auth')
923 b64str = re.findall(r'Basic (\S+)', auth)[0]
924 userpass = base64.b64decode(b64str)
925 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
926 if password != 'secret':
927 raise Exception('wrong password')
928 except Exception, e:
929 # Authentication failed.
930 self.send_response(401)
931 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
932 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000933 if set_cookie_if_challenged:
934 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000935 self.end_headers()
936 self.wfile.write('<html><head>')
937 self.wfile.write('<title>Denied: %s</title>' % e)
938 self.wfile.write('</head><body>')
939 self.wfile.write('auth=%s<p>' % auth)
940 self.wfile.write('b64str=%s<p>' % b64str)
941 self.wfile.write('username: %s<p>' % username)
942 self.wfile.write('userpass: %s<p>' % userpass)
943 self.wfile.write('password: %s<p>' % password)
944 self.wfile.write('You sent:<br>%s<p>' % self.headers)
945 self.wfile.write('</body></html>')
946 return True
947
948 # Authentication successful. (Return a cachable response to allow for
949 # testing cached pages that require authentication.)
950 if_none_match = self.headers.getheader('if-none-match')
951 if if_none_match == "abc":
952 self.send_response(304)
953 self.end_headers()
954 else:
955 self.send_response(200)
956 self.send_header('Content-type', 'text/html')
957 self.send_header('Cache-control', 'max-age=60000')
958 self.send_header('Etag', 'abc')
959 self.end_headers()
960 self.wfile.write('<html><head>')
961 self.wfile.write('<title>%s/%s</title>' % (username, password))
962 self.wfile.write('</head><body>')
963 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000964 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000965 self.wfile.write('</body></html>')
966
967 return True
968
tonyg@chromium.org75054202010-03-31 22:06:10 +0000969 def GetNonce(self, force_reset=False):
970 """Returns a nonce that's stable per request path for the server's lifetime.
initial.commit94958cf2008-07-26 22:42:52 +0000971
tonyg@chromium.org75054202010-03-31 22:06:10 +0000972 This is a fake implementation. A real implementation would only use a given
973 nonce a single time (hence the name n-once). However, for the purposes of
974 unittesting, we don't care about the security of the nonce.
975
976 Args:
977 force_reset: Iff set, the nonce will be changed. Useful for testing the
978 "stale" response.
979 """
980 if force_reset or not self.server.nonce_time:
981 self.server.nonce_time = time.time()
982 return _new_md5('privatekey%s%d' %
983 (self.path, self.server.nonce_time)).hexdigest()
984
985 def AuthDigestHandler(self):
986 """This handler tests 'Digest' authentication.
987
988 It just sends a page with title 'user/pass' if you succeed.
989
990 A stale response is sent iff "stale" is present in the request path.
991 """
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000992 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000993 return False
994
tonyg@chromium.org75054202010-03-31 22:06:10 +0000995 stale = 'stale' in self.path
996 nonce = self.GetNonce(force_reset=stale)
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000997 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000998 password = 'secret'
999 realm = 'testrealm'
1000
1001 auth = self.headers.getheader('authorization')
1002 pairs = {}
1003 try:
1004 if not auth:
1005 raise Exception('no auth')
1006 if not auth.startswith('Digest'):
1007 raise Exception('not digest')
1008 # Pull out all the name="value" pairs as a dictionary.
1009 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1010
1011 # Make sure it's all valid.
1012 if pairs['nonce'] != nonce:
1013 raise Exception('wrong nonce')
1014 if pairs['opaque'] != opaque:
1015 raise Exception('wrong opaque')
1016
1017 # Check the 'response' value and make sure it matches our magic hash.
1018 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +00001019 hash_a1 = _new_md5(
1020 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001021 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001022 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001023 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +00001024 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1025 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001026 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001027
1028 if pairs['response'] != response:
1029 raise Exception('wrong password')
1030 except Exception, e:
1031 # Authentication failed.
1032 self.send_response(401)
1033 hdr = ('Digest '
1034 'realm="%s", '
1035 'domain="/", '
1036 'qop="auth", '
1037 'algorithm=MD5, '
1038 'nonce="%s", '
1039 'opaque="%s"') % (realm, nonce, opaque)
1040 if stale:
1041 hdr += ', stale="TRUE"'
1042 self.send_header('WWW-Authenticate', hdr)
1043 self.send_header('Content-type', 'text/html')
1044 self.end_headers()
1045 self.wfile.write('<html><head>')
1046 self.wfile.write('<title>Denied: %s</title>' % e)
1047 self.wfile.write('</head><body>')
1048 self.wfile.write('auth=%s<p>' % auth)
1049 self.wfile.write('pairs=%s<p>' % pairs)
1050 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1051 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1052 self.wfile.write('</body></html>')
1053 return True
1054
1055 # Authentication successful.
1056 self.send_response(200)
1057 self.send_header('Content-type', 'text/html')
1058 self.end_headers()
1059 self.wfile.write('<html><head>')
1060 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1061 self.wfile.write('</head><body>')
1062 self.wfile.write('auth=%s<p>' % auth)
1063 self.wfile.write('pairs=%s<p>' % pairs)
1064 self.wfile.write('</body></html>')
1065
1066 return True
1067
1068 def SlowServerHandler(self):
1069 """Wait for the user suggested time before responding. The syntax is
1070 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001071 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +00001072 return False
1073 query_char = self.path.find('?')
1074 wait_sec = 1.0
1075 if query_char >= 0:
1076 try:
1077 wait_sec = int(self.path[query_char + 1:])
1078 except ValueError:
1079 pass
1080 time.sleep(wait_sec)
1081 self.send_response(200)
1082 self.send_header('Content-type', 'text/plain')
1083 self.end_headers()
1084 self.wfile.write("waited %d seconds" % wait_sec)
1085 return True
1086
1087 def ContentTypeHandler(self):
1088 """Returns a string of html with the given content type. E.g.,
1089 /contenttype?text/css returns an html file with the Content-Type
1090 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001091 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001092 return False
1093 query_char = self.path.find('?')
1094 content_type = self.path[query_char + 1:].strip()
1095 if not content_type:
1096 content_type = 'text/html'
1097 self.send_response(200)
1098 self.send_header('Content-Type', content_type)
1099 self.end_headers()
1100 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1101 return True
1102
1103 def ServerRedirectHandler(self):
1104 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001105 '/server-redirect?http://foo.bar/asdf' to redirect to
1106 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001107
1108 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001109 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001110 return False
1111
1112 query_char = self.path.find('?')
1113 if query_char < 0 or len(self.path) <= query_char + 1:
1114 self.sendRedirectHelp(test_name)
1115 return True
1116 dest = self.path[query_char + 1:]
1117
1118 self.send_response(301) # moved permanently
1119 self.send_header('Location', dest)
1120 self.send_header('Content-type', 'text/html')
1121 self.end_headers()
1122 self.wfile.write('<html><head>')
1123 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1124
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001125 return True
initial.commit94958cf2008-07-26 22:42:52 +00001126
1127 def ClientRedirectHandler(self):
1128 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001129 '/client-redirect?http://foo.bar/asdf' to redirect to
1130 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001131
1132 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001133 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001134 return False
1135
1136 query_char = self.path.find('?');
1137 if query_char < 0 or len(self.path) <= query_char + 1:
1138 self.sendRedirectHelp(test_name)
1139 return True
1140 dest = self.path[query_char + 1:]
1141
1142 self.send_response(200)
1143 self.send_header('Content-type', 'text/html')
1144 self.end_headers()
1145 self.wfile.write('<html><head>')
1146 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1147 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1148
1149 return True
1150
tony@chromium.org03266982010-03-05 03:18:42 +00001151 def MultipartHandler(self):
1152 """Send a multipart response (10 text/html pages)."""
1153 test_name = "/multipart"
1154 if not self._ShouldHandleRequest(test_name):
1155 return False
1156
1157 num_frames = 10
1158 bound = '12345'
1159 self.send_response(200)
1160 self.send_header('Content-type',
1161 'multipart/x-mixed-replace;boundary=' + bound)
1162 self.end_headers()
1163
1164 for i in xrange(num_frames):
1165 self.wfile.write('--' + bound + '\r\n')
1166 self.wfile.write('Content-type: text/html\r\n\r\n')
1167 self.wfile.write('<title>page ' + str(i) + '</title>')
1168 self.wfile.write('page ' + str(i))
1169
1170 self.wfile.write('--' + bound + '--')
1171 return True
1172
initial.commit94958cf2008-07-26 22:42:52 +00001173 def DefaultResponseHandler(self):
1174 """This is the catch-all response handler for requests that aren't handled
1175 by one of the special handlers above.
1176 Note that we specify the content-length as without it the https connection
1177 is not closed properly (and the browser keeps expecting data)."""
1178
1179 contents = "Default response given for path: " + self.path
1180 self.send_response(200)
1181 self.send_header('Content-type', 'text/html')
1182 self.send_header("Content-Length", len(contents))
1183 self.end_headers()
1184 self.wfile.write(contents)
1185 return True
1186
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001187 def RedirectConnectHandler(self):
1188 """Sends a redirect to the CONNECT request for www.redirect.com. This
1189 response is not specified by the RFC, so the browser should not follow
1190 the redirect."""
1191
1192 if (self.path.find("www.redirect.com") < 0):
1193 return False
1194
1195 dest = "http://www.destination.com/foo.js"
1196
1197 self.send_response(302) # moved temporarily
1198 self.send_header('Location', dest)
1199 self.send_header('Connection', 'close')
1200 self.end_headers()
1201 return True
1202
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001203 def ServerAuthConnectHandler(self):
1204 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1205 response doesn't make sense because the proxy server cannot request
1206 server authentication."""
1207
1208 if (self.path.find("www.server-auth.com") < 0):
1209 return False
1210
1211 challenge = 'Basic realm="WallyWorld"'
1212
1213 self.send_response(401) # unauthorized
1214 self.send_header('WWW-Authenticate', challenge)
1215 self.send_header('Connection', 'close')
1216 self.end_headers()
1217 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001218
1219 def DefaultConnectResponseHandler(self):
1220 """This is the catch-all response handler for CONNECT requests that aren't
1221 handled by one of the special handlers above. Real Web servers respond
1222 with 400 to CONNECT requests."""
1223
1224 contents = "Your client has issued a malformed or illegal request."
1225 self.send_response(400) # bad request
1226 self.send_header('Content-type', 'text/html')
1227 self.send_header("Content-Length", len(contents))
1228 self.end_headers()
1229 self.wfile.write(contents)
1230 return True
1231
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001232 def DeviceManagementHandler(self):
1233 """Delegates to the device management service used for cloud policy."""
1234 if not self._ShouldHandleRequest("/device_management"):
1235 return False
1236
1237 length = int(self.headers.getheader('content-length'))
1238 raw_request = self.rfile.read(length)
1239
1240 if not self.server._device_management_handler:
1241 import device_management
1242 policy_path = os.path.join(self.server.data_dir, 'device_management')
1243 self.server._device_management_handler = (
1244 device_management.TestServer(policy_path))
1245
1246 http_response, raw_reply = (
1247 self.server._device_management_handler.HandleRequest(self.path,
1248 self.headers,
1249 raw_request))
1250 self.send_response(http_response)
1251 self.end_headers()
1252 self.wfile.write(raw_reply)
1253 return True
1254
initial.commit94958cf2008-07-26 22:42:52 +00001255 # called by the redirect handling function when there is no parameter
1256 def sendRedirectHelp(self, redirect_name):
1257 self.send_response(200)
1258 self.send_header('Content-type', 'text/html')
1259 self.end_headers()
1260 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1261 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1262 self.wfile.write('</body></html>')
1263
akalin@chromium.org154bb132010-11-12 02:20:27 +00001264
1265class SyncPageHandler(BasePageHandler):
1266 """Handler for the main HTTP sync server."""
1267
1268 def __init__(self, request, client_address, sync_http_server):
1269 get_handlers = [self.ChromiumSyncTimeHandler]
1270 post_handlers = [self.ChromiumSyncCommandHandler]
1271 BasePageHandler.__init__(self, request, client_address,
1272 sync_http_server, [], get_handlers,
1273 post_handlers, [])
1274
1275 def ChromiumSyncTimeHandler(self):
1276 """Handle Chromium sync .../time requests.
1277
1278 The syncer sometimes checks server reachability by examining /time.
1279 """
1280 test_name = "/chromiumsync/time"
1281 if not self._ShouldHandleRequest(test_name):
1282 return False
1283
1284 self.send_response(200)
1285 self.send_header('Content-type', 'text/html')
1286 self.end_headers()
1287 return True
1288
1289 def ChromiumSyncCommandHandler(self):
1290 """Handle a chromiumsync command arriving via http.
1291
1292 This covers all sync protocol commands: authentication, getupdates, and
1293 commit.
1294 """
1295 test_name = "/chromiumsync/command"
1296 if not self._ShouldHandleRequest(test_name):
1297 return False
1298
1299 length = int(self.headers.getheader('content-length'))
1300 raw_request = self.rfile.read(length)
1301
1302 http_response, raw_reply = self.server.HandleCommand(
1303 self.path, raw_request)
1304 self.send_response(http_response)
1305 self.end_headers()
1306 self.wfile.write(raw_reply)
1307 return True
1308
1309
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001310def MakeDataDir():
1311 if options.data_dir:
1312 if not os.path.isdir(options.data_dir):
1313 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1314 return None
1315 my_data_dir = options.data_dir
1316 else:
1317 # Create the default path to our data dir, relative to the exe dir.
1318 my_data_dir = os.path.dirname(sys.argv[0])
1319 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00001320 "test", "data")
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001321
1322 #TODO(ibrar): Must use Find* funtion defined in google\tools
1323 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1324
1325 return my_data_dir
1326
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001327class FileMultiplexer:
1328 def __init__(self, fd1, fd2) :
1329 self.__fd1 = fd1
1330 self.__fd2 = fd2
1331
1332 def __del__(self) :
1333 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1334 self.__fd1.close()
1335 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1336 self.__fd2.close()
1337
1338 def write(self, text) :
1339 self.__fd1.write(text)
1340 self.__fd2.write(text)
1341
1342 def flush(self) :
1343 self.__fd1.flush()
1344 self.__fd2.flush()
1345
initial.commit94958cf2008-07-26 22:42:52 +00001346def main(options, args):
initial.commit94958cf2008-07-26 22:42:52 +00001347 logfile = open('testserver.log', 'w')
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001348 sys.stdout = FileMultiplexer(sys.stdout, logfile)
1349 sys.stderr = FileMultiplexer(sys.stderr, logfile)
initial.commit94958cf2008-07-26 22:42:52 +00001350
1351 port = options.port
1352
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001353 server_data = {}
1354
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001355 if options.server_type == SERVER_HTTP:
1356 if options.cert:
1357 # let's make sure the cert file exists.
1358 if not os.path.isfile(options.cert):
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001359 print 'specified server cert file not found: ' + options.cert + \
1360 ' exiting...'
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001361 return
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001362 for ca_cert in options.ssl_client_ca:
1363 if not os.path.isfile(ca_cert):
1364 print 'specified trusted client CA file not found: ' + ca_cert + \
1365 ' exiting...'
1366 return
davidben@chromium.org31282a12010-08-07 01:10:02 +00001367 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001368 options.ssl_client_auth, options.ssl_client_ca,
1369 options.ssl_bulk_cipher)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001370 print 'HTTPS server started on port %d...' % server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001371 else:
phajdan.jr@chromium.orgfa49b5f2010-07-23 20:38:56 +00001372 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001373 print 'HTTP server started on port %d...' % server.server_port
erikkay@google.com70397b62008-12-30 21:49:21 +00001374
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001375 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001376 server.file_root_url = options.file_root_url
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001377 server_data['port'] = server.server_port
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001378 server._device_management_handler = None
akalin@chromium.org154bb132010-11-12 02:20:27 +00001379 elif options.server_type == SERVER_SYNC:
1380 server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1381 print 'Sync HTTP server started on port %d...' % server.server_port
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001382 print 'Sync XMPP server started on port %d...' % server.xmpp_port
1383 server_data['port'] = server.server_port
1384 server_data['xmpp_port'] = server.xmpp_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001385 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001386 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001387 my_data_dir = MakeDataDir()
1388
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001389 # Instantiate a dummy authorizer for managing 'virtual' users
1390 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1391
1392 # Define a new user having full r/w permissions and a read-only
1393 # anonymous user
1394 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1395
1396 authorizer.add_anonymous(my_data_dir)
1397
1398 # Instantiate FTP handler class
1399 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1400 ftp_handler.authorizer = authorizer
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001401
1402 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001403 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1404 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001405
1406 # Instantiate FTP server class and listen to 127.0.0.1:port
1407 address = ('127.0.0.1', port)
1408 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001409 server_data['port'] = server.socket.getsockname()[1]
1410 print 'FTP server started on port %d...' % server_data['port']
initial.commit94958cf2008-07-26 22:42:52 +00001411
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001412 # Notify the parent that we've started. (BaseServer subclasses
1413 # bind their sockets on construction.)
1414 if options.startup_pipe is not None:
akalin@chromium.org73b7b5c2010-11-26 19:42:26 +00001415 server_data_json = simplejson.dumps(server_data)
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001416 server_data_len = len(server_data_json)
1417 print 'sending server_data: %s (%d bytes)' % (
1418 server_data_json, server_data_len)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001419 if sys.platform == 'win32':
1420 fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1421 else:
1422 fd = options.startup_pipe
1423 startup_pipe = os.fdopen(fd, "w")
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001424 # First write the data length as an unsigned 4-byte value. This
1425 # is _not_ using network byte ordering since the other end of the
1426 # pipe is on the same machine.
1427 startup_pipe.write(struct.pack('=L', server_data_len))
1428 startup_pipe.write(server_data_json)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001429 startup_pipe.close()
1430
initial.commit94958cf2008-07-26 22:42:52 +00001431 try:
1432 server.serve_forever()
1433 except KeyboardInterrupt:
1434 print 'shutting down server'
1435 server.stop = True
1436
1437if __name__ == '__main__':
1438 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001439 option_parser.add_option("-f", '--ftp', action='store_const',
1440 const=SERVER_FTP, default=SERVER_HTTP,
1441 dest='server_type',
akalin@chromium.org154bb132010-11-12 02:20:27 +00001442 help='start up an FTP server.')
1443 option_parser.add_option('', '--sync', action='store_const',
1444 const=SERVER_SYNC, default=SERVER_HTTP,
1445 dest='server_type',
1446 help='start up a sync server.')
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001447 option_parser.add_option('', '--port', default='0', type='int',
1448 help='Port used by the server. If unspecified, the '
1449 'server will listen on an ephemeral port.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001450 option_parser.add_option('', '--data-dir', dest='data_dir',
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001451 help='Directory from which to read the files.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001452 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001453 help='Specify that https should be used, specify '
1454 'the path to the cert containing the private key '
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001455 'the server should use.')
davidben@chromium.org31282a12010-08-07 01:10:02 +00001456 option_parser.add_option('', '--ssl-client-auth', action='store_true',
1457 help='Require SSL client auth on every connection.')
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001458 option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1459 help='Specify that the client certificate request '
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001460 'should include the CA named in the subject of '
1461 'the DER-encoded certificate contained in the '
1462 'specified file. This option may appear multiple '
1463 'times, indicating multiple CA names should be '
1464 'sent in the request.')
1465 option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1466 help='Specify the bulk encryption algorithm(s)'
1467 'that will be accepted by the SSL server. Valid '
1468 'values are "aes256", "aes128", "3des", "rc4". If '
1469 'omitted, all algorithms will be used. This '
1470 'option may appear multiple times, indicating '
1471 'multiple algorithms should be enabled.');
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001472 option_parser.add_option('', '--file-root-url', default='/files/',
1473 help='Specify a root URL for files served.')
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001474 option_parser.add_option('', '--startup-pipe', type='int',
1475 dest='startup_pipe',
1476 help='File handle of pipe to parent process')
initial.commit94958cf2008-07-26 22:42:52 +00001477 options, args = option_parser.parse_args()
1478
1479 sys.exit(main(options, args))