blob: c0dc930be00ee6d95ab268fd3ffa07ac41e326bb [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
154 def RunDispatcherHandler(dispatcher, handler):
155 """Handles a single event for an asyncore.dispatcher.
156
157 Adapted from asyncore.read() et al.
158 """
159 try:
160 handler(dispatcher)
161 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
162 raise
163 except:
164 dispatcher.handle_error()
165
166 while True:
167 read_fds = [ self.fileno() ]
168 write_fds = []
169 exceptional_fds = []
170
171 for fd, xmpp_connection in self._xmpp_socket_map.items():
172 is_r = xmpp_connection.readable()
173 is_w = xmpp_connection.writable()
174 if is_r:
175 read_fds.append(fd)
176 if is_w:
177 write_fds.append(fd)
178 if is_r or is_w:
179 exceptional_fds.append(fd)
180
181 try:
182 read_fds, write_fds, exceptional_fds = (
183 select.select(read_fds, write_fds, exceptional_fds))
184 except select.error, err:
185 if err.args[0] != errno.EINTR:
186 raise
187 else:
188 continue
189
190 for fd in read_fds:
191 if fd == self.fileno():
192 self.HandleRequestNoBlock()
193 continue
194 xmpp_connection = self._xmpp_socket_map.get(fd)
195 RunDispatcherHandler(xmpp_connection,
196 asyncore.dispatcher.handle_read_event)
197
198 for fd in write_fds:
199 xmpp_connection = self._xmpp_socket_map.get(fd)
200 RunDispatcherHandler(xmpp_connection,
201 asyncore.dispatcher.handle_write_event)
202
203 for fd in exceptional_fds:
204 xmpp_connection = self._xmpp_socket_map.get(fd)
205 RunDispatcherHandler(xmpp_connection,
206 asyncore.dispatcher.handle_expt_event)
207
akalin@chromium.org154bb132010-11-12 02:20:27 +0000208
209class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
210
211 def __init__(self, request, client_address, socket_server,
212 connect_handlers, get_handlers, post_handlers, put_handlers):
213 self._connect_handlers = connect_handlers
214 self._get_handlers = get_handlers
215 self._post_handlers = post_handlers
216 self._put_handlers = put_handlers
217 BaseHTTPServer.BaseHTTPRequestHandler.__init__(
218 self, request, client_address, socket_server)
219
220 def log_request(self, *args, **kwargs):
221 # Disable request logging to declutter test log output.
222 pass
223
224 def _ShouldHandleRequest(self, handler_name):
225 """Determines if the path can be handled by the handler.
226
227 We consider a handler valid if the path begins with the
228 handler name. It can optionally be followed by "?*", "/*".
229 """
230
231 pattern = re.compile('%s($|\?|/).*' % handler_name)
232 return pattern.match(self.path)
233
234 def do_CONNECT(self):
235 for handler in self._connect_handlers:
236 if handler():
237 return
238
239 def do_GET(self):
240 for handler in self._get_handlers:
241 if handler():
242 return
243
244 def do_POST(self):
245 for handler in self._post_handlers:
246 if handler():
247 return
248
249 def do_PUT(self):
250 for handler in self._put_handlers:
251 if handler():
252 return
253
254
255class TestPageHandler(BasePageHandler):
initial.commit94958cf2008-07-26 22:42:52 +0000256
257 def __init__(self, request, client_address, socket_server):
akalin@chromium.org154bb132010-11-12 02:20:27 +0000258 connect_handlers = [
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000259 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000260 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000261 self.DefaultConnectResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000262 get_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000263 self.NoCacheMaxAgeTimeHandler,
264 self.NoCacheTimeHandler,
265 self.CacheTimeHandler,
266 self.CacheExpiresHandler,
267 self.CacheProxyRevalidateHandler,
268 self.CachePrivateHandler,
269 self.CachePublicHandler,
270 self.CacheSMaxAgeHandler,
271 self.CacheMustRevalidateHandler,
272 self.CacheMustRevalidateMaxAgeHandler,
273 self.CacheNoStoreHandler,
274 self.CacheNoStoreMaxAgeHandler,
275 self.CacheNoTransformHandler,
276 self.DownloadHandler,
277 self.DownloadFinishHandler,
278 self.EchoHeader,
ananta@chromium.org219b2062009-10-23 16:09:41 +0000279 self.EchoHeaderOverride,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000280 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000281 self.FileHandler,
282 self.RealFileWithCommonHeaderHandler,
283 self.RealBZ2FileWithCommonHeaderHandler,
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000284 self.SetCookieHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000285 self.AuthBasicHandler,
286 self.AuthDigestHandler,
287 self.SlowServerHandler,
288 self.ContentTypeHandler,
289 self.ServerRedirectHandler,
290 self.ClientRedirectHandler,
tony@chromium.org03266982010-03-05 03:18:42 +0000291 self.MultipartHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000292 self.DefaultResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000293 post_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000294 self.EchoTitleHandler,
295 self.EchoAllHandler,
mnissler@chromium.org7c939802010-11-11 08:47:14 +0000296 self.EchoHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000297 self.DeviceManagementHandler] + get_handlers
298 put_handlers = [
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000299 self.EchoTitleHandler,
300 self.EchoAllHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000301 self.EchoHandler] + get_handlers
initial.commit94958cf2008-07-26 22:42:52 +0000302
maruel@google.come250a9b2009-03-10 17:39:46 +0000303 self._mime_types = {
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000304 'crx' : 'application/x-chrome-extension',
maruel@google.come250a9b2009-03-10 17:39:46 +0000305 'gif': 'image/gif',
306 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000307 'jpg' : 'image/jpeg',
jam@chromium.org41550782010-11-17 23:47:50 +0000308 'xml' : 'text/xml',
309 'pdf' : 'application/pdf'
maruel@google.come250a9b2009-03-10 17:39:46 +0000310 }
initial.commit94958cf2008-07-26 22:42:52 +0000311 self._default_mime_type = 'text/html'
312
akalin@chromium.org154bb132010-11-12 02:20:27 +0000313 BasePageHandler.__init__(self, request, client_address, socket_server,
314 connect_handlers, get_handlers, post_handlers,
315 put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000316
initial.commit94958cf2008-07-26 22:42:52 +0000317 def GetMIMETypeFromName(self, file_name):
318 """Returns the mime type for the specified file_name. So far it only looks
319 at the file extension."""
320
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000321 (shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000322 if len(extension) == 0:
323 # no extension.
324 return self._default_mime_type
325
ericroman@google.comc17ca532009-05-07 03:51:05 +0000326 # extension starts with a dot, so we need to remove it
327 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000328
initial.commit94958cf2008-07-26 22:42:52 +0000329 def NoCacheMaxAgeTimeHandler(self):
330 """This request handler yields a page with the title set to the current
331 system time, and no caching requested."""
332
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000333 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000334 return False
335
336 self.send_response(200)
337 self.send_header('Cache-Control', 'max-age=0')
338 self.send_header('Content-type', 'text/html')
339 self.end_headers()
340
maruel@google.come250a9b2009-03-10 17:39:46 +0000341 self.wfile.write('<html><head><title>%s</title></head></html>' %
342 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000343
344 return True
345
346 def NoCacheTimeHandler(self):
347 """This request handler yields a page with the title set to the current
348 system time, and no caching requested."""
349
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000350 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000351 return False
352
353 self.send_response(200)
354 self.send_header('Cache-Control', 'no-cache')
355 self.send_header('Content-type', 'text/html')
356 self.end_headers()
357
maruel@google.come250a9b2009-03-10 17:39:46 +0000358 self.wfile.write('<html><head><title>%s</title></head></html>' %
359 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000360
361 return True
362
363 def CacheTimeHandler(self):
364 """This request handler yields a page with the title set to the current
365 system time, and allows caching for one minute."""
366
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000367 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000368 return False
369
370 self.send_response(200)
371 self.send_header('Cache-Control', 'max-age=60')
372 self.send_header('Content-type', 'text/html')
373 self.end_headers()
374
maruel@google.come250a9b2009-03-10 17:39:46 +0000375 self.wfile.write('<html><head><title>%s</title></head></html>' %
376 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000377
378 return True
379
380 def CacheExpiresHandler(self):
381 """This request handler yields a page with the title set to the current
382 system time, and set the page to expire on 1 Jan 2099."""
383
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000384 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000385 return False
386
387 self.send_response(200)
388 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
389 self.send_header('Content-type', 'text/html')
390 self.end_headers()
391
maruel@google.come250a9b2009-03-10 17:39:46 +0000392 self.wfile.write('<html><head><title>%s</title></head></html>' %
393 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000394
395 return True
396
397 def CacheProxyRevalidateHandler(self):
398 """This request handler yields a page with the title set to the current
399 system time, and allows caching for 60 seconds"""
400
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000401 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000402 return False
403
404 self.send_response(200)
405 self.send_header('Content-type', 'text/html')
406 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
407 self.end_headers()
408
maruel@google.come250a9b2009-03-10 17:39:46 +0000409 self.wfile.write('<html><head><title>%s</title></head></html>' %
410 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000411
412 return True
413
414 def CachePrivateHandler(self):
415 """This request handler yields a page with the title set to the current
416 system time, and allows caching for 5 seconds."""
417
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000418 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000419 return False
420
421 self.send_response(200)
422 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000423 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000424 self.end_headers()
425
maruel@google.come250a9b2009-03-10 17:39:46 +0000426 self.wfile.write('<html><head><title>%s</title></head></html>' %
427 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000428
429 return True
430
431 def CachePublicHandler(self):
432 """This request handler yields a page with the title set to the current
433 system time, and allows caching for 5 seconds."""
434
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000435 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000436 return False
437
438 self.send_response(200)
439 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000440 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000441 self.end_headers()
442
maruel@google.come250a9b2009-03-10 17:39:46 +0000443 self.wfile.write('<html><head><title>%s</title></head></html>' %
444 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000445
446 return True
447
448 def CacheSMaxAgeHandler(self):
449 """This request handler yields a page with the title set to the current
450 system time, and does not allow for caching."""
451
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000452 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000453 return False
454
455 self.send_response(200)
456 self.send_header('Content-type', 'text/html')
457 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
458 self.end_headers()
459
maruel@google.come250a9b2009-03-10 17:39:46 +0000460 self.wfile.write('<html><head><title>%s</title></head></html>' %
461 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000462
463 return True
464
465 def CacheMustRevalidateHandler(self):
466 """This request handler yields a page with the title set to the current
467 system time, and does not allow caching."""
468
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000469 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000470 return False
471
472 self.send_response(200)
473 self.send_header('Content-type', 'text/html')
474 self.send_header('Cache-Control', 'must-revalidate')
475 self.end_headers()
476
maruel@google.come250a9b2009-03-10 17:39:46 +0000477 self.wfile.write('<html><head><title>%s</title></head></html>' %
478 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000479
480 return True
481
482 def CacheMustRevalidateMaxAgeHandler(self):
483 """This request handler yields a page with the title set to the current
484 system time, and does not allow caching event though max-age of 60
485 seconds is specified."""
486
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000487 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000488 return False
489
490 self.send_response(200)
491 self.send_header('Content-type', 'text/html')
492 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
493 self.end_headers()
494
maruel@google.come250a9b2009-03-10 17:39:46 +0000495 self.wfile.write('<html><head><title>%s</title></head></html>' %
496 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000497
498 return True
499
initial.commit94958cf2008-07-26 22:42:52 +0000500 def CacheNoStoreHandler(self):
501 """This request handler yields a page with the title set to the current
502 system time, and does not allow the page to be stored."""
503
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000504 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000505 return False
506
507 self.send_response(200)
508 self.send_header('Content-type', 'text/html')
509 self.send_header('Cache-Control', 'no-store')
510 self.end_headers()
511
maruel@google.come250a9b2009-03-10 17:39:46 +0000512 self.wfile.write('<html><head><title>%s</title></head></html>' %
513 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000514
515 return True
516
517 def CacheNoStoreMaxAgeHandler(self):
518 """This request handler yields a page with the title set to the current
519 system time, and does not allow the page to be stored even though max-age
520 of 60 seconds is specified."""
521
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000522 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000523 return False
524
525 self.send_response(200)
526 self.send_header('Content-type', 'text/html')
527 self.send_header('Cache-Control', 'max-age=60, no-store')
528 self.end_headers()
529
maruel@google.come250a9b2009-03-10 17:39:46 +0000530 self.wfile.write('<html><head><title>%s</title></head></html>' %
531 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000532
533 return True
534
535
536 def CacheNoTransformHandler(self):
537 """This request handler yields a page with the title set to the current
538 system time, and does not allow the content to transformed during
539 user-agent caching"""
540
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000541 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000542 return False
543
544 self.send_response(200)
545 self.send_header('Content-type', 'text/html')
546 self.send_header('Cache-Control', 'no-transform')
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 EchoHeader(self):
555 """This handler echoes back the value of a specific request header."""
ananta@chromium.org219b2062009-10-23 16:09:41 +0000556 """The only difference between this function and the EchoHeaderOverride"""
557 """function is in the parameter being passed to the helper function"""
558 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000559
ananta@chromium.org219b2062009-10-23 16:09:41 +0000560 def EchoHeaderOverride(self):
561 """This handler echoes back the value of a specific request header."""
562 """The UrlRequest unit tests also execute for ChromeFrame which uses"""
563 """IE to issue HTTP requests using the host network stack."""
564 """The Accept and Charset tests which expect the server to echo back"""
565 """the corresponding headers fail here as IE returns cached responses"""
566 """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
567 """treats this request as a new request and does not cache it."""
568 return self.EchoHeaderHelper("/echoheaderoverride")
569
570 def EchoHeaderHelper(self, echo_header):
571 """This function echoes back the value of the request header passed in."""
572 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000573 return False
574
575 query_char = self.path.find('?')
576 if query_char != -1:
577 header_name = self.path[query_char+1:]
578
579 self.send_response(200)
580 self.send_header('Content-type', 'text/plain')
581 self.send_header('Cache-control', 'max-age=60000')
582 # insert a vary header to properly indicate that the cachability of this
583 # request is subject to value of the request header being echoed.
584 if len(header_name) > 0:
585 self.send_header('Vary', header_name)
586 self.end_headers()
587
588 if len(header_name) > 0:
589 self.wfile.write(self.headers.getheader(header_name))
590
591 return True
592
593 def EchoHandler(self):
594 """This handler just echoes back the payload of the request, for testing
595 form submission."""
596
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000597 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000598 return False
599
600 self.send_response(200)
601 self.send_header('Content-type', 'text/html')
602 self.end_headers()
603 length = int(self.headers.getheader('content-length'))
604 request = self.rfile.read(length)
605 self.wfile.write(request)
606 return True
607
608 def EchoTitleHandler(self):
609 """This handler is like Echo, but sets the page title to the request."""
610
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000611 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000612 return False
613
614 self.send_response(200)
615 self.send_header('Content-type', 'text/html')
616 self.end_headers()
617 length = int(self.headers.getheader('content-length'))
618 request = self.rfile.read(length)
619 self.wfile.write('<html><head><title>')
620 self.wfile.write(request)
621 self.wfile.write('</title></head></html>')
622 return True
623
624 def EchoAllHandler(self):
625 """This handler yields a (more) human-readable page listing information
626 about the request header & contents."""
627
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000628 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000629 return False
630
631 self.send_response(200)
632 self.send_header('Content-type', 'text/html')
633 self.end_headers()
634 self.wfile.write('<html><head><style>'
635 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
636 '</style></head><body>'
637 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000638 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000639 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000640
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000641 if self.command == 'POST' or self.command == 'PUT':
ericroman@google.coma47622b2008-11-15 04:36:51 +0000642 length = int(self.headers.getheader('content-length'))
643 qs = self.rfile.read(length)
644 params = cgi.parse_qs(qs, keep_blank_values=1)
645
646 for param in params:
647 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000648
649 self.wfile.write('</pre>')
650
651 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
652
653 self.wfile.write('</body></html>')
654 return True
655
656 def DownloadHandler(self):
657 """This handler sends a downloadable file with or without reporting
658 the size (6K)."""
659
660 if self.path.startswith("/download-unknown-size"):
661 send_length = False
662 elif self.path.startswith("/download-known-size"):
663 send_length = True
664 else:
665 return False
666
667 #
668 # The test which uses this functionality is attempting to send
669 # small chunks of data to the client. Use a fairly large buffer
670 # so that we'll fill chrome's IO buffer enough to force it to
671 # actually write the data.
672 # See also the comments in the client-side of this test in
673 # download_uitest.cc
674 #
675 size_chunk1 = 35*1024
676 size_chunk2 = 10*1024
677
678 self.send_response(200)
679 self.send_header('Content-type', 'application/octet-stream')
680 self.send_header('Cache-Control', 'max-age=0')
681 if send_length:
682 self.send_header('Content-Length', size_chunk1 + size_chunk2)
683 self.end_headers()
684
685 # First chunk of data:
686 self.wfile.write("*" * size_chunk1)
687 self.wfile.flush()
688
689 # handle requests until one of them clears this flag.
690 self.server.waitForDownload = True
691 while self.server.waitForDownload:
692 self.server.handle_request()
693
694 # Second chunk of data:
695 self.wfile.write("*" * size_chunk2)
696 return True
697
698 def DownloadFinishHandler(self):
699 """This handler just tells the server to finish the current download."""
700
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000701 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000702 return False
703
704 self.server.waitForDownload = False
705 self.send_response(200)
706 self.send_header('Content-type', 'text/html')
707 self.send_header('Cache-Control', 'max-age=0')
708 self.end_headers()
709 return True
710
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000711 def _ReplaceFileData(self, data, query_parameters):
712 """Replaces matching substrings in a file.
713
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000714 If the 'replace_text' URL query parameter is present, it is expected to be
715 of the form old_text:new_text, which indicates that any old_text strings in
716 the file are replaced with new_text. Multiple 'replace_text' parameters may
717 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000718
719 If the parameters are not present, |data| is returned.
720 """
721 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000722 replace_text_values = query_dict.get('replace_text', [])
723 for replace_text_value in replace_text_values:
724 replace_text_args = replace_text_value.split(':')
725 if len(replace_text_args) != 2:
726 raise ValueError(
727 'replace_text must be of form old_text:new_text. Actual value: %s' %
728 replace_text_value)
729 old_text_b64, new_text_b64 = replace_text_args
730 old_text = base64.urlsafe_b64decode(old_text_b64)
731 new_text = base64.urlsafe_b64decode(new_text_b64)
732 data = data.replace(old_text, new_text)
733 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000734
initial.commit94958cf2008-07-26 22:42:52 +0000735 def FileHandler(self):
736 """This handler sends the contents of the requested file. Wow, it's like
737 a real webserver!"""
738
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000739 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000740 if not self.path.startswith(prefix):
741 return False
742
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000743 # Consume a request body if present.
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000744 if self.command == 'POST' or self.command == 'PUT' :
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000745 self.rfile.read(int(self.headers.getheader('content-length')))
746
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000747 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
748 sub_path = url_path[len(prefix):]
749 entries = sub_path.split('/')
750 file_path = os.path.join(self.server.data_dir, *entries)
751 if os.path.isdir(file_path):
752 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000753
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000754 if not os.path.isfile(file_path):
755 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000756 self.send_error(404)
757 return True
758
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000759 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000760 data = f.read()
761 f.close()
762
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000763 data = self._ReplaceFileData(data, query)
764
initial.commit94958cf2008-07-26 22:42:52 +0000765 # If file.mock-http-headers exists, it contains the headers we
766 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000767 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000768 if os.path.isfile(headers_path):
769 f = open(headers_path, "r")
770
771 # "HTTP/1.1 200 OK"
772 response = f.readline()
773 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
774 self.send_response(int(status_code))
775
776 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000777 header_values = re.findall('(\S+):\s*(.*)', line)
778 if len(header_values) > 0:
779 # "name: value"
780 name, value = header_values[0]
781 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000782 f.close()
783 else:
784 # Could be more generic once we support mime-type sniffing, but for
785 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000786
787 range = self.headers.get('Range')
788 if range and range.startswith('bytes='):
789 # Note this doesn't handle all valid byte range values (i.e. open ended
790 # ones), just enough for what we needed so far.
791 range = range[6:].split('-')
792 start = int(range[0])
793 end = int(range[1])
794
795 self.send_response(206)
796 content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
797 str(len(data))
798 self.send_header('Content-Range', content_range)
799 data = data[start: end + 1]
800 else:
801 self.send_response(200)
802
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000803 self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000804 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000805 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000806 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000807 self.end_headers()
808
809 self.wfile.write(data)
810
811 return True
812
813 def RealFileWithCommonHeaderHandler(self):
814 """This handler sends the contents of the requested file without the pseudo
815 http head!"""
816
817 prefix='/realfiles/'
818 if not self.path.startswith(prefix):
819 return False
820
821 file = self.path[len(prefix):]
822 path = os.path.join(self.server.data_dir, file)
823
824 try:
825 f = open(path, "rb")
826 data = f.read()
827 f.close()
828
829 # just simply set the MIME as octal stream
830 self.send_response(200)
831 self.send_header('Content-type', 'application/octet-stream')
832 self.end_headers()
833
834 self.wfile.write(data)
835 except:
836 self.send_error(404)
837
838 return True
839
840 def RealBZ2FileWithCommonHeaderHandler(self):
841 """This handler sends the bzip2 contents of the requested file with
842 corresponding Content-Encoding field in http head!"""
843
844 prefix='/realbz2files/'
845 if not self.path.startswith(prefix):
846 return False
847
848 parts = self.path.split('?')
849 file = parts[0][len(prefix):]
850 path = os.path.join(self.server.data_dir, file) + '.bz2'
851
852 if len(parts) > 1:
853 options = parts[1]
854 else:
855 options = ''
856
857 try:
858 self.send_response(200)
859 accept_encoding = self.headers.get("Accept-Encoding")
860 if accept_encoding.find("bzip2") != -1:
861 f = open(path, "rb")
862 data = f.read()
863 f.close()
864 self.send_header('Content-Encoding', 'bzip2')
865 self.send_header('Content-type', 'application/x-bzip2')
866 self.end_headers()
867 if options == 'incremental-header':
868 self.wfile.write(data[:1])
869 self.wfile.flush()
870 time.sleep(1.0)
871 self.wfile.write(data[1:])
872 else:
873 self.wfile.write(data)
874 else:
875 """client do not support bzip2 format, send pseudo content
876 """
877 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
878 self.end_headers()
879 self.wfile.write("you do not support bzip2 encoding")
880 except:
881 self.send_error(404)
882
883 return True
884
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000885 def SetCookieHandler(self):
886 """This handler just sets a cookie, for testing cookie handling."""
887
888 if not self._ShouldHandleRequest("/set-cookie"):
889 return False
890
891 query_char = self.path.find('?')
892 if query_char != -1:
893 cookie_values = self.path[query_char + 1:].split('&')
894 else:
895 cookie_values = ("",)
896 self.send_response(200)
897 self.send_header('Content-type', 'text/html')
898 for cookie_value in cookie_values:
899 self.send_header('Set-Cookie', '%s' % cookie_value)
900 self.end_headers()
901 for cookie_value in cookie_values:
902 self.wfile.write('%s' % cookie_value)
903 return True
904
initial.commit94958cf2008-07-26 22:42:52 +0000905 def AuthBasicHandler(self):
906 """This handler tests 'Basic' authentication. It just sends a page with
907 title 'user/pass' if you succeed."""
908
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000909 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000910 return False
911
912 username = userpass = password = b64str = ""
913
ericroman@google.com239b4d82009-03-27 04:00:22 +0000914 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
915
initial.commit94958cf2008-07-26 22:42:52 +0000916 auth = self.headers.getheader('authorization')
917 try:
918 if not auth:
919 raise Exception('no auth')
920 b64str = re.findall(r'Basic (\S+)', auth)[0]
921 userpass = base64.b64decode(b64str)
922 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
923 if password != 'secret':
924 raise Exception('wrong password')
925 except Exception, e:
926 # Authentication failed.
927 self.send_response(401)
928 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
929 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000930 if set_cookie_if_challenged:
931 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000932 self.end_headers()
933 self.wfile.write('<html><head>')
934 self.wfile.write('<title>Denied: %s</title>' % e)
935 self.wfile.write('</head><body>')
936 self.wfile.write('auth=%s<p>' % auth)
937 self.wfile.write('b64str=%s<p>' % b64str)
938 self.wfile.write('username: %s<p>' % username)
939 self.wfile.write('userpass: %s<p>' % userpass)
940 self.wfile.write('password: %s<p>' % password)
941 self.wfile.write('You sent:<br>%s<p>' % self.headers)
942 self.wfile.write('</body></html>')
943 return True
944
945 # Authentication successful. (Return a cachable response to allow for
946 # testing cached pages that require authentication.)
947 if_none_match = self.headers.getheader('if-none-match')
948 if if_none_match == "abc":
949 self.send_response(304)
950 self.end_headers()
951 else:
952 self.send_response(200)
953 self.send_header('Content-type', 'text/html')
954 self.send_header('Cache-control', 'max-age=60000')
955 self.send_header('Etag', 'abc')
956 self.end_headers()
957 self.wfile.write('<html><head>')
958 self.wfile.write('<title>%s/%s</title>' % (username, password))
959 self.wfile.write('</head><body>')
960 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000961 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000962 self.wfile.write('</body></html>')
963
964 return True
965
tonyg@chromium.org75054202010-03-31 22:06:10 +0000966 def GetNonce(self, force_reset=False):
967 """Returns a nonce that's stable per request path for the server's lifetime.
initial.commit94958cf2008-07-26 22:42:52 +0000968
tonyg@chromium.org75054202010-03-31 22:06:10 +0000969 This is a fake implementation. A real implementation would only use a given
970 nonce a single time (hence the name n-once). However, for the purposes of
971 unittesting, we don't care about the security of the nonce.
972
973 Args:
974 force_reset: Iff set, the nonce will be changed. Useful for testing the
975 "stale" response.
976 """
977 if force_reset or not self.server.nonce_time:
978 self.server.nonce_time = time.time()
979 return _new_md5('privatekey%s%d' %
980 (self.path, self.server.nonce_time)).hexdigest()
981
982 def AuthDigestHandler(self):
983 """This handler tests 'Digest' authentication.
984
985 It just sends a page with title 'user/pass' if you succeed.
986
987 A stale response is sent iff "stale" is present in the request path.
988 """
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000989 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000990 return False
991
tonyg@chromium.org75054202010-03-31 22:06:10 +0000992 stale = 'stale' in self.path
993 nonce = self.GetNonce(force_reset=stale)
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000994 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000995 password = 'secret'
996 realm = 'testrealm'
997
998 auth = self.headers.getheader('authorization')
999 pairs = {}
1000 try:
1001 if not auth:
1002 raise Exception('no auth')
1003 if not auth.startswith('Digest'):
1004 raise Exception('not digest')
1005 # Pull out all the name="value" pairs as a dictionary.
1006 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1007
1008 # Make sure it's all valid.
1009 if pairs['nonce'] != nonce:
1010 raise Exception('wrong nonce')
1011 if pairs['opaque'] != opaque:
1012 raise Exception('wrong opaque')
1013
1014 # Check the 'response' value and make sure it matches our magic hash.
1015 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +00001016 hash_a1 = _new_md5(
1017 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001018 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001019 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001020 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +00001021 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1022 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001023 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001024
1025 if pairs['response'] != response:
1026 raise Exception('wrong password')
1027 except Exception, e:
1028 # Authentication failed.
1029 self.send_response(401)
1030 hdr = ('Digest '
1031 'realm="%s", '
1032 'domain="/", '
1033 'qop="auth", '
1034 'algorithm=MD5, '
1035 'nonce="%s", '
1036 'opaque="%s"') % (realm, nonce, opaque)
1037 if stale:
1038 hdr += ', stale="TRUE"'
1039 self.send_header('WWW-Authenticate', hdr)
1040 self.send_header('Content-type', 'text/html')
1041 self.end_headers()
1042 self.wfile.write('<html><head>')
1043 self.wfile.write('<title>Denied: %s</title>' % e)
1044 self.wfile.write('</head><body>')
1045 self.wfile.write('auth=%s<p>' % auth)
1046 self.wfile.write('pairs=%s<p>' % pairs)
1047 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1048 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1049 self.wfile.write('</body></html>')
1050 return True
1051
1052 # Authentication successful.
1053 self.send_response(200)
1054 self.send_header('Content-type', 'text/html')
1055 self.end_headers()
1056 self.wfile.write('<html><head>')
1057 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1058 self.wfile.write('</head><body>')
1059 self.wfile.write('auth=%s<p>' % auth)
1060 self.wfile.write('pairs=%s<p>' % pairs)
1061 self.wfile.write('</body></html>')
1062
1063 return True
1064
1065 def SlowServerHandler(self):
1066 """Wait for the user suggested time before responding. The syntax is
1067 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001068 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +00001069 return False
1070 query_char = self.path.find('?')
1071 wait_sec = 1.0
1072 if query_char >= 0:
1073 try:
1074 wait_sec = int(self.path[query_char + 1:])
1075 except ValueError:
1076 pass
1077 time.sleep(wait_sec)
1078 self.send_response(200)
1079 self.send_header('Content-type', 'text/plain')
1080 self.end_headers()
1081 self.wfile.write("waited %d seconds" % wait_sec)
1082 return True
1083
1084 def ContentTypeHandler(self):
1085 """Returns a string of html with the given content type. E.g.,
1086 /contenttype?text/css returns an html file with the Content-Type
1087 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001088 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001089 return False
1090 query_char = self.path.find('?')
1091 content_type = self.path[query_char + 1:].strip()
1092 if not content_type:
1093 content_type = 'text/html'
1094 self.send_response(200)
1095 self.send_header('Content-Type', content_type)
1096 self.end_headers()
1097 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1098 return True
1099
1100 def ServerRedirectHandler(self):
1101 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001102 '/server-redirect?http://foo.bar/asdf' to redirect to
1103 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001104
1105 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001106 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001107 return False
1108
1109 query_char = self.path.find('?')
1110 if query_char < 0 or len(self.path) <= query_char + 1:
1111 self.sendRedirectHelp(test_name)
1112 return True
1113 dest = self.path[query_char + 1:]
1114
1115 self.send_response(301) # moved permanently
1116 self.send_header('Location', dest)
1117 self.send_header('Content-type', 'text/html')
1118 self.end_headers()
1119 self.wfile.write('<html><head>')
1120 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1121
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001122 return True
initial.commit94958cf2008-07-26 22:42:52 +00001123
1124 def ClientRedirectHandler(self):
1125 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001126 '/client-redirect?http://foo.bar/asdf' to redirect to
1127 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001128
1129 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001130 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001131 return False
1132
1133 query_char = self.path.find('?');
1134 if query_char < 0 or len(self.path) <= query_char + 1:
1135 self.sendRedirectHelp(test_name)
1136 return True
1137 dest = self.path[query_char + 1:]
1138
1139 self.send_response(200)
1140 self.send_header('Content-type', 'text/html')
1141 self.end_headers()
1142 self.wfile.write('<html><head>')
1143 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1144 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1145
1146 return True
1147
tony@chromium.org03266982010-03-05 03:18:42 +00001148 def MultipartHandler(self):
1149 """Send a multipart response (10 text/html pages)."""
1150 test_name = "/multipart"
1151 if not self._ShouldHandleRequest(test_name):
1152 return False
1153
1154 num_frames = 10
1155 bound = '12345'
1156 self.send_response(200)
1157 self.send_header('Content-type',
1158 'multipart/x-mixed-replace;boundary=' + bound)
1159 self.end_headers()
1160
1161 for i in xrange(num_frames):
1162 self.wfile.write('--' + bound + '\r\n')
1163 self.wfile.write('Content-type: text/html\r\n\r\n')
1164 self.wfile.write('<title>page ' + str(i) + '</title>')
1165 self.wfile.write('page ' + str(i))
1166
1167 self.wfile.write('--' + bound + '--')
1168 return True
1169
initial.commit94958cf2008-07-26 22:42:52 +00001170 def DefaultResponseHandler(self):
1171 """This is the catch-all response handler for requests that aren't handled
1172 by one of the special handlers above.
1173 Note that we specify the content-length as without it the https connection
1174 is not closed properly (and the browser keeps expecting data)."""
1175
1176 contents = "Default response given for path: " + self.path
1177 self.send_response(200)
1178 self.send_header('Content-type', 'text/html')
1179 self.send_header("Content-Length", len(contents))
1180 self.end_headers()
1181 self.wfile.write(contents)
1182 return True
1183
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001184 def RedirectConnectHandler(self):
1185 """Sends a redirect to the CONNECT request for www.redirect.com. This
1186 response is not specified by the RFC, so the browser should not follow
1187 the redirect."""
1188
1189 if (self.path.find("www.redirect.com") < 0):
1190 return False
1191
1192 dest = "http://www.destination.com/foo.js"
1193
1194 self.send_response(302) # moved temporarily
1195 self.send_header('Location', dest)
1196 self.send_header('Connection', 'close')
1197 self.end_headers()
1198 return True
1199
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001200 def ServerAuthConnectHandler(self):
1201 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1202 response doesn't make sense because the proxy server cannot request
1203 server authentication."""
1204
1205 if (self.path.find("www.server-auth.com") < 0):
1206 return False
1207
1208 challenge = 'Basic realm="WallyWorld"'
1209
1210 self.send_response(401) # unauthorized
1211 self.send_header('WWW-Authenticate', challenge)
1212 self.send_header('Connection', 'close')
1213 self.end_headers()
1214 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001215
1216 def DefaultConnectResponseHandler(self):
1217 """This is the catch-all response handler for CONNECT requests that aren't
1218 handled by one of the special handlers above. Real Web servers respond
1219 with 400 to CONNECT requests."""
1220
1221 contents = "Your client has issued a malformed or illegal request."
1222 self.send_response(400) # bad request
1223 self.send_header('Content-type', 'text/html')
1224 self.send_header("Content-Length", len(contents))
1225 self.end_headers()
1226 self.wfile.write(contents)
1227 return True
1228
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001229 def DeviceManagementHandler(self):
1230 """Delegates to the device management service used for cloud policy."""
1231 if not self._ShouldHandleRequest("/device_management"):
1232 return False
1233
1234 length = int(self.headers.getheader('content-length'))
1235 raw_request = self.rfile.read(length)
1236
1237 if not self.server._device_management_handler:
1238 import device_management
1239 policy_path = os.path.join(self.server.data_dir, 'device_management')
1240 self.server._device_management_handler = (
1241 device_management.TestServer(policy_path))
1242
1243 http_response, raw_reply = (
1244 self.server._device_management_handler.HandleRequest(self.path,
1245 self.headers,
1246 raw_request))
1247 self.send_response(http_response)
1248 self.end_headers()
1249 self.wfile.write(raw_reply)
1250 return True
1251
initial.commit94958cf2008-07-26 22:42:52 +00001252 # called by the redirect handling function when there is no parameter
1253 def sendRedirectHelp(self, redirect_name):
1254 self.send_response(200)
1255 self.send_header('Content-type', 'text/html')
1256 self.end_headers()
1257 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1258 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1259 self.wfile.write('</body></html>')
1260
akalin@chromium.org154bb132010-11-12 02:20:27 +00001261
1262class SyncPageHandler(BasePageHandler):
1263 """Handler for the main HTTP sync server."""
1264
1265 def __init__(self, request, client_address, sync_http_server):
1266 get_handlers = [self.ChromiumSyncTimeHandler]
1267 post_handlers = [self.ChromiumSyncCommandHandler]
1268 BasePageHandler.__init__(self, request, client_address,
1269 sync_http_server, [], get_handlers,
1270 post_handlers, [])
1271
1272 def ChromiumSyncTimeHandler(self):
1273 """Handle Chromium sync .../time requests.
1274
1275 The syncer sometimes checks server reachability by examining /time.
1276 """
1277 test_name = "/chromiumsync/time"
1278 if not self._ShouldHandleRequest(test_name):
1279 return False
1280
1281 self.send_response(200)
1282 self.send_header('Content-type', 'text/html')
1283 self.end_headers()
1284 return True
1285
1286 def ChromiumSyncCommandHandler(self):
1287 """Handle a chromiumsync command arriving via http.
1288
1289 This covers all sync protocol commands: authentication, getupdates, and
1290 commit.
1291 """
1292 test_name = "/chromiumsync/command"
1293 if not self._ShouldHandleRequest(test_name):
1294 return False
1295
1296 length = int(self.headers.getheader('content-length'))
1297 raw_request = self.rfile.read(length)
1298
1299 http_response, raw_reply = self.server.HandleCommand(
1300 self.path, raw_request)
1301 self.send_response(http_response)
1302 self.end_headers()
1303 self.wfile.write(raw_reply)
1304 return True
1305
1306
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001307def MakeDataDir():
1308 if options.data_dir:
1309 if not os.path.isdir(options.data_dir):
1310 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1311 return None
1312 my_data_dir = options.data_dir
1313 else:
1314 # Create the default path to our data dir, relative to the exe dir.
1315 my_data_dir = os.path.dirname(sys.argv[0])
1316 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00001317 "test", "data")
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001318
1319 #TODO(ibrar): Must use Find* funtion defined in google\tools
1320 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1321
1322 return my_data_dir
1323
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001324class FileMultiplexer:
1325 def __init__(self, fd1, fd2) :
1326 self.__fd1 = fd1
1327 self.__fd2 = fd2
1328
1329 def __del__(self) :
1330 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1331 self.__fd1.close()
1332 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1333 self.__fd2.close()
1334
1335 def write(self, text) :
1336 self.__fd1.write(text)
1337 self.__fd2.write(text)
1338
1339 def flush(self) :
1340 self.__fd1.flush()
1341 self.__fd2.flush()
1342
initial.commit94958cf2008-07-26 22:42:52 +00001343def main(options, args):
initial.commit94958cf2008-07-26 22:42:52 +00001344 logfile = open('testserver.log', 'w')
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001345 sys.stdout = FileMultiplexer(sys.stdout, logfile)
1346 sys.stderr = FileMultiplexer(sys.stderr, logfile)
initial.commit94958cf2008-07-26 22:42:52 +00001347
1348 port = options.port
1349
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001350 server_data = {}
1351
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001352 if options.server_type == SERVER_HTTP:
1353 if options.cert:
1354 # let's make sure the cert file exists.
1355 if not os.path.isfile(options.cert):
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001356 print 'specified server cert file not found: ' + options.cert + \
1357 ' exiting...'
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001358 return
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001359 for ca_cert in options.ssl_client_ca:
1360 if not os.path.isfile(ca_cert):
1361 print 'specified trusted client CA file not found: ' + ca_cert + \
1362 ' exiting...'
1363 return
davidben@chromium.org31282a12010-08-07 01:10:02 +00001364 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001365 options.ssl_client_auth, options.ssl_client_ca,
1366 options.ssl_bulk_cipher)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001367 print 'HTTPS server started on port %d...' % server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001368 else:
phajdan.jr@chromium.orgfa49b5f2010-07-23 20:38:56 +00001369 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001370 print 'HTTP server started on port %d...' % server.server_port
erikkay@google.com70397b62008-12-30 21:49:21 +00001371
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001372 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001373 server.file_root_url = options.file_root_url
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001374 server_data['port'] = server.server_port
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001375 server._device_management_handler = None
akalin@chromium.org154bb132010-11-12 02:20:27 +00001376 elif options.server_type == SERVER_SYNC:
1377 server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1378 print 'Sync HTTP server started on port %d...' % server.server_port
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001379 print 'Sync XMPP server started on port %d...' % server.xmpp_port
1380 server_data['port'] = server.server_port
1381 server_data['xmpp_port'] = server.xmpp_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001382 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001383 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001384 my_data_dir = MakeDataDir()
1385
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001386 # Instantiate a dummy authorizer for managing 'virtual' users
1387 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1388
1389 # Define a new user having full r/w permissions and a read-only
1390 # anonymous user
1391 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1392
1393 authorizer.add_anonymous(my_data_dir)
1394
1395 # Instantiate FTP handler class
1396 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1397 ftp_handler.authorizer = authorizer
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001398
1399 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001400 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1401 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001402
1403 # Instantiate FTP server class and listen to 127.0.0.1:port
1404 address = ('127.0.0.1', port)
1405 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001406 server_data['port'] = server.socket.getsockname()[1]
1407 print 'FTP server started on port %d...' % server_data['port']
initial.commit94958cf2008-07-26 22:42:52 +00001408
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001409 # Notify the parent that we've started. (BaseServer subclasses
1410 # bind their sockets on construction.)
1411 if options.startup_pipe is not None:
akalin@chromium.org73b7b5c2010-11-26 19:42:26 +00001412 server_data_json = simplejson.dumps(server_data)
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001413 server_data_len = len(server_data_json)
1414 print 'sending server_data: %s (%d bytes)' % (
1415 server_data_json, server_data_len)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001416 if sys.platform == 'win32':
1417 fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1418 else:
1419 fd = options.startup_pipe
1420 startup_pipe = os.fdopen(fd, "w")
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001421 # First write the data length as an unsigned 4-byte value. This
1422 # is _not_ using network byte ordering since the other end of the
1423 # pipe is on the same machine.
1424 startup_pipe.write(struct.pack('=L', server_data_len))
1425 startup_pipe.write(server_data_json)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001426 startup_pipe.close()
1427
initial.commit94958cf2008-07-26 22:42:52 +00001428 try:
1429 server.serve_forever()
1430 except KeyboardInterrupt:
1431 print 'shutting down server'
1432 server.stop = True
1433
1434if __name__ == '__main__':
1435 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001436 option_parser.add_option("-f", '--ftp', action='store_const',
1437 const=SERVER_FTP, default=SERVER_HTTP,
1438 dest='server_type',
akalin@chromium.org154bb132010-11-12 02:20:27 +00001439 help='start up an FTP server.')
1440 option_parser.add_option('', '--sync', action='store_const',
1441 const=SERVER_SYNC, default=SERVER_HTTP,
1442 dest='server_type',
1443 help='start up a sync server.')
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001444 option_parser.add_option('', '--port', default='0', type='int',
1445 help='Port used by the server. If unspecified, the '
1446 'server will listen on an ephemeral port.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001447 option_parser.add_option('', '--data-dir', dest='data_dir',
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001448 help='Directory from which to read the files.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001449 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001450 help='Specify that https should be used, specify '
1451 'the path to the cert containing the private key '
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001452 'the server should use.')
davidben@chromium.org31282a12010-08-07 01:10:02 +00001453 option_parser.add_option('', '--ssl-client-auth', action='store_true',
1454 help='Require SSL client auth on every connection.')
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001455 option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1456 help='Specify that the client certificate request '
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001457 'should include the CA named in the subject of '
1458 'the DER-encoded certificate contained in the '
1459 'specified file. This option may appear multiple '
1460 'times, indicating multiple CA names should be '
1461 'sent in the request.')
1462 option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1463 help='Specify the bulk encryption algorithm(s)'
1464 'that will be accepted by the SSL server. Valid '
1465 'values are "aes256", "aes128", "3des", "rc4". If '
1466 'omitted, all algorithms will be used. This '
1467 'option may appear multiple times, indicating '
1468 'multiple algorithms should be enabled.');
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001469 option_parser.add_option('', '--file-root-url', default='/files/',
1470 help='Specify a root URL for files served.')
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001471 option_parser.add_option('', '--startup-pipe', type='int',
1472 dest='startup_pipe',
1473 help='File handle of pipe to parent process')
initial.commit94958cf2008-07-26 22:42:52 +00001474 options, args = option_parser.parse_args()
1475
1476 sys.exit(main(options, args))