blob: 0cd663d8434551dc1f4b442edf67c8bbd222059e [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',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000305 'exe' : 'application/octet-stream',
maruel@google.come250a9b2009-03-10 17:39:46 +0000306 'gif': 'image/gif',
307 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000308 'jpg' : 'image/jpeg',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000309 'pdf' : 'application/pdf',
310 'xml' : 'text/xml'
maruel@google.come250a9b2009-03-10 17:39:46 +0000311 }
initial.commit94958cf2008-07-26 22:42:52 +0000312 self._default_mime_type = 'text/html'
313
akalin@chromium.org154bb132010-11-12 02:20:27 +0000314 BasePageHandler.__init__(self, request, client_address, socket_server,
315 connect_handlers, get_handlers, post_handlers,
316 put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000317
initial.commit94958cf2008-07-26 22:42:52 +0000318 def GetMIMETypeFromName(self, file_name):
319 """Returns the mime type for the specified file_name. So far it only looks
320 at the file extension."""
321
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000322 (shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000323 if len(extension) == 0:
324 # no extension.
325 return self._default_mime_type
326
ericroman@google.comc17ca532009-05-07 03:51:05 +0000327 # extension starts with a dot, so we need to remove it
328 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000329
initial.commit94958cf2008-07-26 22:42:52 +0000330 def NoCacheMaxAgeTimeHandler(self):
331 """This request handler yields a page with the title set to the current
332 system time, and no caching requested."""
333
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000334 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000335 return False
336
337 self.send_response(200)
338 self.send_header('Cache-Control', 'max-age=0')
339 self.send_header('Content-type', 'text/html')
340 self.end_headers()
341
maruel@google.come250a9b2009-03-10 17:39:46 +0000342 self.wfile.write('<html><head><title>%s</title></head></html>' %
343 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000344
345 return True
346
347 def NoCacheTimeHandler(self):
348 """This request handler yields a page with the title set to the current
349 system time, and no caching requested."""
350
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000351 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000352 return False
353
354 self.send_response(200)
355 self.send_header('Cache-Control', 'no-cache')
356 self.send_header('Content-type', 'text/html')
357 self.end_headers()
358
maruel@google.come250a9b2009-03-10 17:39:46 +0000359 self.wfile.write('<html><head><title>%s</title></head></html>' %
360 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000361
362 return True
363
364 def CacheTimeHandler(self):
365 """This request handler yields a page with the title set to the current
366 system time, and allows caching for one minute."""
367
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000368 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000369 return False
370
371 self.send_response(200)
372 self.send_header('Cache-Control', 'max-age=60')
373 self.send_header('Content-type', 'text/html')
374 self.end_headers()
375
maruel@google.come250a9b2009-03-10 17:39:46 +0000376 self.wfile.write('<html><head><title>%s</title></head></html>' %
377 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000378
379 return True
380
381 def CacheExpiresHandler(self):
382 """This request handler yields a page with the title set to the current
383 system time, and set the page to expire on 1 Jan 2099."""
384
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000385 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000386 return False
387
388 self.send_response(200)
389 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
390 self.send_header('Content-type', 'text/html')
391 self.end_headers()
392
maruel@google.come250a9b2009-03-10 17:39:46 +0000393 self.wfile.write('<html><head><title>%s</title></head></html>' %
394 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000395
396 return True
397
398 def CacheProxyRevalidateHandler(self):
399 """This request handler yields a page with the title set to the current
400 system time, and allows caching for 60 seconds"""
401
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000402 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000403 return False
404
405 self.send_response(200)
406 self.send_header('Content-type', 'text/html')
407 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
408 self.end_headers()
409
maruel@google.come250a9b2009-03-10 17:39:46 +0000410 self.wfile.write('<html><head><title>%s</title></head></html>' %
411 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000412
413 return True
414
415 def CachePrivateHandler(self):
416 """This request handler yields a page with the title set to the current
417 system time, and allows caching for 5 seconds."""
418
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000419 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000420 return False
421
422 self.send_response(200)
423 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000424 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000425 self.end_headers()
426
maruel@google.come250a9b2009-03-10 17:39:46 +0000427 self.wfile.write('<html><head><title>%s</title></head></html>' %
428 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000429
430 return True
431
432 def CachePublicHandler(self):
433 """This request handler yields a page with the title set to the current
434 system time, and allows caching for 5 seconds."""
435
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000436 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000437 return False
438
439 self.send_response(200)
440 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000441 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000442 self.end_headers()
443
maruel@google.come250a9b2009-03-10 17:39:46 +0000444 self.wfile.write('<html><head><title>%s</title></head></html>' %
445 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000446
447 return True
448
449 def CacheSMaxAgeHandler(self):
450 """This request handler yields a page with the title set to the current
451 system time, and does not allow for caching."""
452
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000453 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000454 return False
455
456 self.send_response(200)
457 self.send_header('Content-type', 'text/html')
458 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
459 self.end_headers()
460
maruel@google.come250a9b2009-03-10 17:39:46 +0000461 self.wfile.write('<html><head><title>%s</title></head></html>' %
462 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000463
464 return True
465
466 def CacheMustRevalidateHandler(self):
467 """This request handler yields a page with the title set to the current
468 system time, and does not allow caching."""
469
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000470 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000471 return False
472
473 self.send_response(200)
474 self.send_header('Content-type', 'text/html')
475 self.send_header('Cache-Control', 'must-revalidate')
476 self.end_headers()
477
maruel@google.come250a9b2009-03-10 17:39:46 +0000478 self.wfile.write('<html><head><title>%s</title></head></html>' %
479 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000480
481 return True
482
483 def CacheMustRevalidateMaxAgeHandler(self):
484 """This request handler yields a page with the title set to the current
485 system time, and does not allow caching event though max-age of 60
486 seconds is specified."""
487
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000488 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000489 return False
490
491 self.send_response(200)
492 self.send_header('Content-type', 'text/html')
493 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
494 self.end_headers()
495
maruel@google.come250a9b2009-03-10 17:39:46 +0000496 self.wfile.write('<html><head><title>%s</title></head></html>' %
497 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000498
499 return True
500
initial.commit94958cf2008-07-26 22:42:52 +0000501 def CacheNoStoreHandler(self):
502 """This request handler yields a page with the title set to the current
503 system time, and does not allow the page to be stored."""
504
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000505 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000506 return False
507
508 self.send_response(200)
509 self.send_header('Content-type', 'text/html')
510 self.send_header('Cache-Control', 'no-store')
511 self.end_headers()
512
maruel@google.come250a9b2009-03-10 17:39:46 +0000513 self.wfile.write('<html><head><title>%s</title></head></html>' %
514 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000515
516 return True
517
518 def CacheNoStoreMaxAgeHandler(self):
519 """This request handler yields a page with the title set to the current
520 system time, and does not allow the page to be stored even though max-age
521 of 60 seconds is specified."""
522
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000523 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000524 return False
525
526 self.send_response(200)
527 self.send_header('Content-type', 'text/html')
528 self.send_header('Cache-Control', 'max-age=60, no-store')
529 self.end_headers()
530
maruel@google.come250a9b2009-03-10 17:39:46 +0000531 self.wfile.write('<html><head><title>%s</title></head></html>' %
532 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000533
534 return True
535
536
537 def CacheNoTransformHandler(self):
538 """This request handler yields a page with the title set to the current
539 system time, and does not allow the content to transformed during
540 user-agent caching"""
541
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000542 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000543 return False
544
545 self.send_response(200)
546 self.send_header('Content-type', 'text/html')
547 self.send_header('Cache-Control', 'no-transform')
548 self.end_headers()
549
maruel@google.come250a9b2009-03-10 17:39:46 +0000550 self.wfile.write('<html><head><title>%s</title></head></html>' %
551 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000552
553 return True
554
555 def EchoHeader(self):
556 """This handler echoes back the value of a specific request header."""
ananta@chromium.org219b2062009-10-23 16:09:41 +0000557 """The only difference between this function and the EchoHeaderOverride"""
558 """function is in the parameter being passed to the helper function"""
559 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000560
ananta@chromium.org219b2062009-10-23 16:09:41 +0000561 def EchoHeaderOverride(self):
562 """This handler echoes back the value of a specific request header."""
563 """The UrlRequest unit tests also execute for ChromeFrame which uses"""
564 """IE to issue HTTP requests using the host network stack."""
565 """The Accept and Charset tests which expect the server to echo back"""
566 """the corresponding headers fail here as IE returns cached responses"""
567 """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
568 """treats this request as a new request and does not cache it."""
569 return self.EchoHeaderHelper("/echoheaderoverride")
570
571 def EchoHeaderHelper(self, echo_header):
572 """This function echoes back the value of the request header passed in."""
573 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000574 return False
575
576 query_char = self.path.find('?')
577 if query_char != -1:
578 header_name = self.path[query_char+1:]
579
580 self.send_response(200)
581 self.send_header('Content-type', 'text/plain')
582 self.send_header('Cache-control', 'max-age=60000')
583 # insert a vary header to properly indicate that the cachability of this
584 # request is subject to value of the request header being echoed.
585 if len(header_name) > 0:
586 self.send_header('Vary', header_name)
587 self.end_headers()
588
589 if len(header_name) > 0:
590 self.wfile.write(self.headers.getheader(header_name))
591
592 return True
593
594 def EchoHandler(self):
595 """This handler just echoes back the payload of the request, for testing
596 form submission."""
597
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000598 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000599 return False
600
601 self.send_response(200)
602 self.send_header('Content-type', 'text/html')
603 self.end_headers()
604 length = int(self.headers.getheader('content-length'))
605 request = self.rfile.read(length)
606 self.wfile.write(request)
607 return True
608
609 def EchoTitleHandler(self):
610 """This handler is like Echo, but sets the page title to the request."""
611
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000612 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000613 return False
614
615 self.send_response(200)
616 self.send_header('Content-type', 'text/html')
617 self.end_headers()
618 length = int(self.headers.getheader('content-length'))
619 request = self.rfile.read(length)
620 self.wfile.write('<html><head><title>')
621 self.wfile.write(request)
622 self.wfile.write('</title></head></html>')
623 return True
624
625 def EchoAllHandler(self):
626 """This handler yields a (more) human-readable page listing information
627 about the request header & contents."""
628
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000629 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000630 return False
631
632 self.send_response(200)
633 self.send_header('Content-type', 'text/html')
634 self.end_headers()
635 self.wfile.write('<html><head><style>'
636 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
637 '</style></head><body>'
638 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000639 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000640 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000641
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000642 if self.command == 'POST' or self.command == 'PUT':
ericroman@google.coma47622b2008-11-15 04:36:51 +0000643 length = int(self.headers.getheader('content-length'))
644 qs = self.rfile.read(length)
645 params = cgi.parse_qs(qs, keep_blank_values=1)
646
647 for param in params:
648 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000649
650 self.wfile.write('</pre>')
651
652 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
653
654 self.wfile.write('</body></html>')
655 return True
656
657 def DownloadHandler(self):
658 """This handler sends a downloadable file with or without reporting
659 the size (6K)."""
660
661 if self.path.startswith("/download-unknown-size"):
662 send_length = False
663 elif self.path.startswith("/download-known-size"):
664 send_length = True
665 else:
666 return False
667
668 #
669 # The test which uses this functionality is attempting to send
670 # small chunks of data to the client. Use a fairly large buffer
671 # so that we'll fill chrome's IO buffer enough to force it to
672 # actually write the data.
673 # See also the comments in the client-side of this test in
674 # download_uitest.cc
675 #
676 size_chunk1 = 35*1024
677 size_chunk2 = 10*1024
678
679 self.send_response(200)
680 self.send_header('Content-type', 'application/octet-stream')
681 self.send_header('Cache-Control', 'max-age=0')
682 if send_length:
683 self.send_header('Content-Length', size_chunk1 + size_chunk2)
684 self.end_headers()
685
686 # First chunk of data:
687 self.wfile.write("*" * size_chunk1)
688 self.wfile.flush()
689
690 # handle requests until one of them clears this flag.
691 self.server.waitForDownload = True
692 while self.server.waitForDownload:
693 self.server.handle_request()
694
695 # Second chunk of data:
696 self.wfile.write("*" * size_chunk2)
697 return True
698
699 def DownloadFinishHandler(self):
700 """This handler just tells the server to finish the current download."""
701
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000702 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000703 return False
704
705 self.server.waitForDownload = False
706 self.send_response(200)
707 self.send_header('Content-type', 'text/html')
708 self.send_header('Cache-Control', 'max-age=0')
709 self.end_headers()
710 return True
711
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000712 def _ReplaceFileData(self, data, query_parameters):
713 """Replaces matching substrings in a file.
714
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000715 If the 'replace_text' URL query parameter is present, it is expected to be
716 of the form old_text:new_text, which indicates that any old_text strings in
717 the file are replaced with new_text. Multiple 'replace_text' parameters may
718 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000719
720 If the parameters are not present, |data| is returned.
721 """
722 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000723 replace_text_values = query_dict.get('replace_text', [])
724 for replace_text_value in replace_text_values:
725 replace_text_args = replace_text_value.split(':')
726 if len(replace_text_args) != 2:
727 raise ValueError(
728 'replace_text must be of form old_text:new_text. Actual value: %s' %
729 replace_text_value)
730 old_text_b64, new_text_b64 = replace_text_args
731 old_text = base64.urlsafe_b64decode(old_text_b64)
732 new_text = base64.urlsafe_b64decode(new_text_b64)
733 data = data.replace(old_text, new_text)
734 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000735
initial.commit94958cf2008-07-26 22:42:52 +0000736 def FileHandler(self):
737 """This handler sends the contents of the requested file. Wow, it's like
738 a real webserver!"""
739
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000740 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000741 if not self.path.startswith(prefix):
742 return False
743
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000744 # Consume a request body if present.
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000745 if self.command == 'POST' or self.command == 'PUT' :
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000746 self.rfile.read(int(self.headers.getheader('content-length')))
747
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000748 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
749 sub_path = url_path[len(prefix):]
750 entries = sub_path.split('/')
751 file_path = os.path.join(self.server.data_dir, *entries)
752 if os.path.isdir(file_path):
753 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000754
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000755 if not os.path.isfile(file_path):
756 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000757 self.send_error(404)
758 return True
759
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000760 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000761 data = f.read()
762 f.close()
763
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000764 data = self._ReplaceFileData(data, query)
765
initial.commit94958cf2008-07-26 22:42:52 +0000766 # If file.mock-http-headers exists, it contains the headers we
767 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000768 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000769 if os.path.isfile(headers_path):
770 f = open(headers_path, "r")
771
772 # "HTTP/1.1 200 OK"
773 response = f.readline()
774 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
775 self.send_response(int(status_code))
776
777 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000778 header_values = re.findall('(\S+):\s*(.*)', line)
779 if len(header_values) > 0:
780 # "name: value"
781 name, value = header_values[0]
782 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000783 f.close()
784 else:
785 # Could be more generic once we support mime-type sniffing, but for
786 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000787
788 range = self.headers.get('Range')
789 if range and range.startswith('bytes='):
790 # Note this doesn't handle all valid byte range values (i.e. open ended
791 # ones), just enough for what we needed so far.
792 range = range[6:].split('-')
793 start = int(range[0])
794 end = int(range[1])
795
796 self.send_response(206)
797 content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
798 str(len(data))
799 self.send_header('Content-Range', content_range)
800 data = data[start: end + 1]
801 else:
802 self.send_response(200)
803
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000804 self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000805 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000806 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000807 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000808 self.end_headers()
809
810 self.wfile.write(data)
811
812 return True
813
814 def RealFileWithCommonHeaderHandler(self):
815 """This handler sends the contents of the requested file without the pseudo
816 http head!"""
817
818 prefix='/realfiles/'
819 if not self.path.startswith(prefix):
820 return False
821
822 file = self.path[len(prefix):]
823 path = os.path.join(self.server.data_dir, file)
824
825 try:
826 f = open(path, "rb")
827 data = f.read()
828 f.close()
829
830 # just simply set the MIME as octal stream
831 self.send_response(200)
832 self.send_header('Content-type', 'application/octet-stream')
833 self.end_headers()
834
835 self.wfile.write(data)
836 except:
837 self.send_error(404)
838
839 return True
840
841 def RealBZ2FileWithCommonHeaderHandler(self):
842 """This handler sends the bzip2 contents of the requested file with
843 corresponding Content-Encoding field in http head!"""
844
845 prefix='/realbz2files/'
846 if not self.path.startswith(prefix):
847 return False
848
849 parts = self.path.split('?')
850 file = parts[0][len(prefix):]
851 path = os.path.join(self.server.data_dir, file) + '.bz2'
852
853 if len(parts) > 1:
854 options = parts[1]
855 else:
856 options = ''
857
858 try:
859 self.send_response(200)
860 accept_encoding = self.headers.get("Accept-Encoding")
861 if accept_encoding.find("bzip2") != -1:
862 f = open(path, "rb")
863 data = f.read()
864 f.close()
865 self.send_header('Content-Encoding', 'bzip2')
866 self.send_header('Content-type', 'application/x-bzip2')
867 self.end_headers()
868 if options == 'incremental-header':
869 self.wfile.write(data[:1])
870 self.wfile.flush()
871 time.sleep(1.0)
872 self.wfile.write(data[1:])
873 else:
874 self.wfile.write(data)
875 else:
876 """client do not support bzip2 format, send pseudo content
877 """
878 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
879 self.end_headers()
880 self.wfile.write("you do not support bzip2 encoding")
881 except:
882 self.send_error(404)
883
884 return True
885
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000886 def SetCookieHandler(self):
887 """This handler just sets a cookie, for testing cookie handling."""
888
889 if not self._ShouldHandleRequest("/set-cookie"):
890 return False
891
892 query_char = self.path.find('?')
893 if query_char != -1:
894 cookie_values = self.path[query_char + 1:].split('&')
895 else:
896 cookie_values = ("",)
897 self.send_response(200)
898 self.send_header('Content-type', 'text/html')
899 for cookie_value in cookie_values:
900 self.send_header('Set-Cookie', '%s' % cookie_value)
901 self.end_headers()
902 for cookie_value in cookie_values:
903 self.wfile.write('%s' % cookie_value)
904 return True
905
initial.commit94958cf2008-07-26 22:42:52 +0000906 def AuthBasicHandler(self):
907 """This handler tests 'Basic' authentication. It just sends a page with
908 title 'user/pass' if you succeed."""
909
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000910 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000911 return False
912
913 username = userpass = password = b64str = ""
914
ericroman@google.com239b4d82009-03-27 04:00:22 +0000915 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
916
initial.commit94958cf2008-07-26 22:42:52 +0000917 auth = self.headers.getheader('authorization')
918 try:
919 if not auth:
920 raise Exception('no auth')
921 b64str = re.findall(r'Basic (\S+)', auth)[0]
922 userpass = base64.b64decode(b64str)
923 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
924 if password != 'secret':
925 raise Exception('wrong password')
926 except Exception, e:
927 # Authentication failed.
928 self.send_response(401)
929 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
930 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000931 if set_cookie_if_challenged:
932 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000933 self.end_headers()
934 self.wfile.write('<html><head>')
935 self.wfile.write('<title>Denied: %s</title>' % e)
936 self.wfile.write('</head><body>')
937 self.wfile.write('auth=%s<p>' % auth)
938 self.wfile.write('b64str=%s<p>' % b64str)
939 self.wfile.write('username: %s<p>' % username)
940 self.wfile.write('userpass: %s<p>' % userpass)
941 self.wfile.write('password: %s<p>' % password)
942 self.wfile.write('You sent:<br>%s<p>' % self.headers)
943 self.wfile.write('</body></html>')
944 return True
945
946 # Authentication successful. (Return a cachable response to allow for
947 # testing cached pages that require authentication.)
948 if_none_match = self.headers.getheader('if-none-match')
949 if if_none_match == "abc":
950 self.send_response(304)
951 self.end_headers()
952 else:
953 self.send_response(200)
954 self.send_header('Content-type', 'text/html')
955 self.send_header('Cache-control', 'max-age=60000')
956 self.send_header('Etag', 'abc')
957 self.end_headers()
958 self.wfile.write('<html><head>')
959 self.wfile.write('<title>%s/%s</title>' % (username, password))
960 self.wfile.write('</head><body>')
961 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000962 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000963 self.wfile.write('</body></html>')
964
965 return True
966
tonyg@chromium.org75054202010-03-31 22:06:10 +0000967 def GetNonce(self, force_reset=False):
968 """Returns a nonce that's stable per request path for the server's lifetime.
initial.commit94958cf2008-07-26 22:42:52 +0000969
tonyg@chromium.org75054202010-03-31 22:06:10 +0000970 This is a fake implementation. A real implementation would only use a given
971 nonce a single time (hence the name n-once). However, for the purposes of
972 unittesting, we don't care about the security of the nonce.
973
974 Args:
975 force_reset: Iff set, the nonce will be changed. Useful for testing the
976 "stale" response.
977 """
978 if force_reset or not self.server.nonce_time:
979 self.server.nonce_time = time.time()
980 return _new_md5('privatekey%s%d' %
981 (self.path, self.server.nonce_time)).hexdigest()
982
983 def AuthDigestHandler(self):
984 """This handler tests 'Digest' authentication.
985
986 It just sends a page with title 'user/pass' if you succeed.
987
988 A stale response is sent iff "stale" is present in the request path.
989 """
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000990 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000991 return False
992
tonyg@chromium.org75054202010-03-31 22:06:10 +0000993 stale = 'stale' in self.path
994 nonce = self.GetNonce(force_reset=stale)
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000995 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000996 password = 'secret'
997 realm = 'testrealm'
998
999 auth = self.headers.getheader('authorization')
1000 pairs = {}
1001 try:
1002 if not auth:
1003 raise Exception('no auth')
1004 if not auth.startswith('Digest'):
1005 raise Exception('not digest')
1006 # Pull out all the name="value" pairs as a dictionary.
1007 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1008
1009 # Make sure it's all valid.
1010 if pairs['nonce'] != nonce:
1011 raise Exception('wrong nonce')
1012 if pairs['opaque'] != opaque:
1013 raise Exception('wrong opaque')
1014
1015 # Check the 'response' value and make sure it matches our magic hash.
1016 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +00001017 hash_a1 = _new_md5(
1018 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001019 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001020 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001021 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +00001022 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1023 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001024 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001025
1026 if pairs['response'] != response:
1027 raise Exception('wrong password')
1028 except Exception, e:
1029 # Authentication failed.
1030 self.send_response(401)
1031 hdr = ('Digest '
1032 'realm="%s", '
1033 'domain="/", '
1034 'qop="auth", '
1035 'algorithm=MD5, '
1036 'nonce="%s", '
1037 'opaque="%s"') % (realm, nonce, opaque)
1038 if stale:
1039 hdr += ', stale="TRUE"'
1040 self.send_header('WWW-Authenticate', hdr)
1041 self.send_header('Content-type', 'text/html')
1042 self.end_headers()
1043 self.wfile.write('<html><head>')
1044 self.wfile.write('<title>Denied: %s</title>' % e)
1045 self.wfile.write('</head><body>')
1046 self.wfile.write('auth=%s<p>' % auth)
1047 self.wfile.write('pairs=%s<p>' % pairs)
1048 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1049 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1050 self.wfile.write('</body></html>')
1051 return True
1052
1053 # Authentication successful.
1054 self.send_response(200)
1055 self.send_header('Content-type', 'text/html')
1056 self.end_headers()
1057 self.wfile.write('<html><head>')
1058 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1059 self.wfile.write('</head><body>')
1060 self.wfile.write('auth=%s<p>' % auth)
1061 self.wfile.write('pairs=%s<p>' % pairs)
1062 self.wfile.write('</body></html>')
1063
1064 return True
1065
1066 def SlowServerHandler(self):
1067 """Wait for the user suggested time before responding. The syntax is
1068 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001069 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +00001070 return False
1071 query_char = self.path.find('?')
1072 wait_sec = 1.0
1073 if query_char >= 0:
1074 try:
1075 wait_sec = int(self.path[query_char + 1:])
1076 except ValueError:
1077 pass
1078 time.sleep(wait_sec)
1079 self.send_response(200)
1080 self.send_header('Content-type', 'text/plain')
1081 self.end_headers()
1082 self.wfile.write("waited %d seconds" % wait_sec)
1083 return True
1084
1085 def ContentTypeHandler(self):
1086 """Returns a string of html with the given content type. E.g.,
1087 /contenttype?text/css returns an html file with the Content-Type
1088 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001089 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001090 return False
1091 query_char = self.path.find('?')
1092 content_type = self.path[query_char + 1:].strip()
1093 if not content_type:
1094 content_type = 'text/html'
1095 self.send_response(200)
1096 self.send_header('Content-Type', content_type)
1097 self.end_headers()
1098 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1099 return True
1100
1101 def ServerRedirectHandler(self):
1102 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001103 '/server-redirect?http://foo.bar/asdf' to redirect to
1104 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001105
1106 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001107 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001108 return False
1109
1110 query_char = self.path.find('?')
1111 if query_char < 0 or len(self.path) <= query_char + 1:
1112 self.sendRedirectHelp(test_name)
1113 return True
1114 dest = self.path[query_char + 1:]
1115
1116 self.send_response(301) # moved permanently
1117 self.send_header('Location', dest)
1118 self.send_header('Content-type', 'text/html')
1119 self.end_headers()
1120 self.wfile.write('<html><head>')
1121 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1122
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001123 return True
initial.commit94958cf2008-07-26 22:42:52 +00001124
1125 def ClientRedirectHandler(self):
1126 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001127 '/client-redirect?http://foo.bar/asdf' to redirect to
1128 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001129
1130 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001131 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001132 return False
1133
1134 query_char = self.path.find('?');
1135 if query_char < 0 or len(self.path) <= query_char + 1:
1136 self.sendRedirectHelp(test_name)
1137 return True
1138 dest = self.path[query_char + 1:]
1139
1140 self.send_response(200)
1141 self.send_header('Content-type', 'text/html')
1142 self.end_headers()
1143 self.wfile.write('<html><head>')
1144 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1145 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1146
1147 return True
1148
tony@chromium.org03266982010-03-05 03:18:42 +00001149 def MultipartHandler(self):
1150 """Send a multipart response (10 text/html pages)."""
1151 test_name = "/multipart"
1152 if not self._ShouldHandleRequest(test_name):
1153 return False
1154
1155 num_frames = 10
1156 bound = '12345'
1157 self.send_response(200)
1158 self.send_header('Content-type',
1159 'multipart/x-mixed-replace;boundary=' + bound)
1160 self.end_headers()
1161
1162 for i in xrange(num_frames):
1163 self.wfile.write('--' + bound + '\r\n')
1164 self.wfile.write('Content-type: text/html\r\n\r\n')
1165 self.wfile.write('<title>page ' + str(i) + '</title>')
1166 self.wfile.write('page ' + str(i))
1167
1168 self.wfile.write('--' + bound + '--')
1169 return True
1170
initial.commit94958cf2008-07-26 22:42:52 +00001171 def DefaultResponseHandler(self):
1172 """This is the catch-all response handler for requests that aren't handled
1173 by one of the special handlers above.
1174 Note that we specify the content-length as without it the https connection
1175 is not closed properly (and the browser keeps expecting data)."""
1176
1177 contents = "Default response given for path: " + self.path
1178 self.send_response(200)
1179 self.send_header('Content-type', 'text/html')
1180 self.send_header("Content-Length", len(contents))
1181 self.end_headers()
1182 self.wfile.write(contents)
1183 return True
1184
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001185 def RedirectConnectHandler(self):
1186 """Sends a redirect to the CONNECT request for www.redirect.com. This
1187 response is not specified by the RFC, so the browser should not follow
1188 the redirect."""
1189
1190 if (self.path.find("www.redirect.com") < 0):
1191 return False
1192
1193 dest = "http://www.destination.com/foo.js"
1194
1195 self.send_response(302) # moved temporarily
1196 self.send_header('Location', dest)
1197 self.send_header('Connection', 'close')
1198 self.end_headers()
1199 return True
1200
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001201 def ServerAuthConnectHandler(self):
1202 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1203 response doesn't make sense because the proxy server cannot request
1204 server authentication."""
1205
1206 if (self.path.find("www.server-auth.com") < 0):
1207 return False
1208
1209 challenge = 'Basic realm="WallyWorld"'
1210
1211 self.send_response(401) # unauthorized
1212 self.send_header('WWW-Authenticate', challenge)
1213 self.send_header('Connection', 'close')
1214 self.end_headers()
1215 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001216
1217 def DefaultConnectResponseHandler(self):
1218 """This is the catch-all response handler for CONNECT requests that aren't
1219 handled by one of the special handlers above. Real Web servers respond
1220 with 400 to CONNECT requests."""
1221
1222 contents = "Your client has issued a malformed or illegal request."
1223 self.send_response(400) # bad request
1224 self.send_header('Content-type', 'text/html')
1225 self.send_header("Content-Length", len(contents))
1226 self.end_headers()
1227 self.wfile.write(contents)
1228 return True
1229
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001230 def DeviceManagementHandler(self):
1231 """Delegates to the device management service used for cloud policy."""
1232 if not self._ShouldHandleRequest("/device_management"):
1233 return False
1234
1235 length = int(self.headers.getheader('content-length'))
1236 raw_request = self.rfile.read(length)
1237
1238 if not self.server._device_management_handler:
1239 import device_management
1240 policy_path = os.path.join(self.server.data_dir, 'device_management')
1241 self.server._device_management_handler = (
1242 device_management.TestServer(policy_path))
1243
1244 http_response, raw_reply = (
1245 self.server._device_management_handler.HandleRequest(self.path,
1246 self.headers,
1247 raw_request))
1248 self.send_response(http_response)
1249 self.end_headers()
1250 self.wfile.write(raw_reply)
1251 return True
1252
initial.commit94958cf2008-07-26 22:42:52 +00001253 # called by the redirect handling function when there is no parameter
1254 def sendRedirectHelp(self, redirect_name):
1255 self.send_response(200)
1256 self.send_header('Content-type', 'text/html')
1257 self.end_headers()
1258 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1259 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1260 self.wfile.write('</body></html>')
1261
akalin@chromium.org154bb132010-11-12 02:20:27 +00001262
1263class SyncPageHandler(BasePageHandler):
1264 """Handler for the main HTTP sync server."""
1265
1266 def __init__(self, request, client_address, sync_http_server):
1267 get_handlers = [self.ChromiumSyncTimeHandler]
1268 post_handlers = [self.ChromiumSyncCommandHandler]
1269 BasePageHandler.__init__(self, request, client_address,
1270 sync_http_server, [], get_handlers,
1271 post_handlers, [])
1272
1273 def ChromiumSyncTimeHandler(self):
1274 """Handle Chromium sync .../time requests.
1275
1276 The syncer sometimes checks server reachability by examining /time.
1277 """
1278 test_name = "/chromiumsync/time"
1279 if not self._ShouldHandleRequest(test_name):
1280 return False
1281
1282 self.send_response(200)
1283 self.send_header('Content-type', 'text/html')
1284 self.end_headers()
1285 return True
1286
1287 def ChromiumSyncCommandHandler(self):
1288 """Handle a chromiumsync command arriving via http.
1289
1290 This covers all sync protocol commands: authentication, getupdates, and
1291 commit.
1292 """
1293 test_name = "/chromiumsync/command"
1294 if not self._ShouldHandleRequest(test_name):
1295 return False
1296
1297 length = int(self.headers.getheader('content-length'))
1298 raw_request = self.rfile.read(length)
1299
1300 http_response, raw_reply = self.server.HandleCommand(
1301 self.path, raw_request)
1302 self.send_response(http_response)
1303 self.end_headers()
1304 self.wfile.write(raw_reply)
1305 return True
1306
1307
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001308def MakeDataDir():
1309 if options.data_dir:
1310 if not os.path.isdir(options.data_dir):
1311 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1312 return None
1313 my_data_dir = options.data_dir
1314 else:
1315 # Create the default path to our data dir, relative to the exe dir.
1316 my_data_dir = os.path.dirname(sys.argv[0])
1317 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00001318 "test", "data")
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001319
1320 #TODO(ibrar): Must use Find* funtion defined in google\tools
1321 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1322
1323 return my_data_dir
1324
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001325class FileMultiplexer:
1326 def __init__(self, fd1, fd2) :
1327 self.__fd1 = fd1
1328 self.__fd2 = fd2
1329
1330 def __del__(self) :
1331 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1332 self.__fd1.close()
1333 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1334 self.__fd2.close()
1335
1336 def write(self, text) :
1337 self.__fd1.write(text)
1338 self.__fd2.write(text)
1339
1340 def flush(self) :
1341 self.__fd1.flush()
1342 self.__fd2.flush()
1343
initial.commit94958cf2008-07-26 22:42:52 +00001344def main(options, args):
initial.commit94958cf2008-07-26 22:42:52 +00001345 logfile = open('testserver.log', 'w')
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001346 sys.stdout = FileMultiplexer(sys.stdout, logfile)
1347 sys.stderr = FileMultiplexer(sys.stderr, logfile)
initial.commit94958cf2008-07-26 22:42:52 +00001348
1349 port = options.port
1350
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001351 server_data = {}
1352
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001353 if options.server_type == SERVER_HTTP:
1354 if options.cert:
1355 # let's make sure the cert file exists.
1356 if not os.path.isfile(options.cert):
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001357 print 'specified server cert file not found: ' + options.cert + \
1358 ' exiting...'
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001359 return
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001360 for ca_cert in options.ssl_client_ca:
1361 if not os.path.isfile(ca_cert):
1362 print 'specified trusted client CA file not found: ' + ca_cert + \
1363 ' exiting...'
1364 return
davidben@chromium.org31282a12010-08-07 01:10:02 +00001365 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001366 options.ssl_client_auth, options.ssl_client_ca,
1367 options.ssl_bulk_cipher)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001368 print 'HTTPS server started on port %d...' % server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001369 else:
phajdan.jr@chromium.orgfa49b5f2010-07-23 20:38:56 +00001370 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001371 print 'HTTP server started on port %d...' % server.server_port
erikkay@google.com70397b62008-12-30 21:49:21 +00001372
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001373 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001374 server.file_root_url = options.file_root_url
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001375 server_data['port'] = server.server_port
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001376 server._device_management_handler = None
akalin@chromium.org154bb132010-11-12 02:20:27 +00001377 elif options.server_type == SERVER_SYNC:
1378 server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1379 print 'Sync HTTP server started on port %d...' % server.server_port
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001380 print 'Sync XMPP server started on port %d...' % server.xmpp_port
1381 server_data['port'] = server.server_port
1382 server_data['xmpp_port'] = server.xmpp_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001383 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001384 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001385 my_data_dir = MakeDataDir()
1386
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001387 # Instantiate a dummy authorizer for managing 'virtual' users
1388 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1389
1390 # Define a new user having full r/w permissions and a read-only
1391 # anonymous user
1392 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1393
1394 authorizer.add_anonymous(my_data_dir)
1395
1396 # Instantiate FTP handler class
1397 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1398 ftp_handler.authorizer = authorizer
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001399
1400 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001401 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1402 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001403
1404 # Instantiate FTP server class and listen to 127.0.0.1:port
1405 address = ('127.0.0.1', port)
1406 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001407 server_data['port'] = server.socket.getsockname()[1]
1408 print 'FTP server started on port %d...' % server_data['port']
initial.commit94958cf2008-07-26 22:42:52 +00001409
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001410 # Notify the parent that we've started. (BaseServer subclasses
1411 # bind their sockets on construction.)
1412 if options.startup_pipe is not None:
akalin@chromium.org73b7b5c2010-11-26 19:42:26 +00001413 server_data_json = simplejson.dumps(server_data)
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001414 server_data_len = len(server_data_json)
1415 print 'sending server_data: %s (%d bytes)' % (
1416 server_data_json, server_data_len)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001417 if sys.platform == 'win32':
1418 fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1419 else:
1420 fd = options.startup_pipe
1421 startup_pipe = os.fdopen(fd, "w")
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001422 # First write the data length as an unsigned 4-byte value. This
1423 # is _not_ using network byte ordering since the other end of the
1424 # pipe is on the same machine.
1425 startup_pipe.write(struct.pack('=L', server_data_len))
1426 startup_pipe.write(server_data_json)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001427 startup_pipe.close()
1428
initial.commit94958cf2008-07-26 22:42:52 +00001429 try:
1430 server.serve_forever()
1431 except KeyboardInterrupt:
1432 print 'shutting down server'
1433 server.stop = True
1434
1435if __name__ == '__main__':
1436 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001437 option_parser.add_option("-f", '--ftp', action='store_const',
1438 const=SERVER_FTP, default=SERVER_HTTP,
1439 dest='server_type',
akalin@chromium.org154bb132010-11-12 02:20:27 +00001440 help='start up an FTP server.')
1441 option_parser.add_option('', '--sync', action='store_const',
1442 const=SERVER_SYNC, default=SERVER_HTTP,
1443 dest='server_type',
1444 help='start up a sync server.')
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001445 option_parser.add_option('', '--port', default='0', type='int',
1446 help='Port used by the server. If unspecified, the '
1447 'server will listen on an ephemeral port.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001448 option_parser.add_option('', '--data-dir', dest='data_dir',
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001449 help='Directory from which to read the files.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001450 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001451 help='Specify that https should be used, specify '
1452 'the path to the cert containing the private key '
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001453 'the server should use.')
davidben@chromium.org31282a12010-08-07 01:10:02 +00001454 option_parser.add_option('', '--ssl-client-auth', action='store_true',
1455 help='Require SSL client auth on every connection.')
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001456 option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1457 help='Specify that the client certificate request '
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001458 'should include the CA named in the subject of '
1459 'the DER-encoded certificate contained in the '
1460 'specified file. This option may appear multiple '
1461 'times, indicating multiple CA names should be '
1462 'sent in the request.')
1463 option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1464 help='Specify the bulk encryption algorithm(s)'
1465 'that will be accepted by the SSL server. Valid '
1466 'values are "aes256", "aes128", "3des", "rc4". If '
1467 'omitted, all algorithms will be used. This '
1468 'option may appear multiple times, indicating '
1469 'multiple algorithms should be enabled.');
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001470 option_parser.add_option('', '--file-root-url', default='/files/',
1471 help='Specify a root URL for files served.')
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001472 option_parser.add_option('', '--startup-pipe', type='int',
1473 dest='startup_pipe',
1474 help='File handle of pipe to parent process')
initial.commit94958cf2008-07-26 22:42:52 +00001475 options, args = option_parser.parse_args()
1476
1477 sys.exit(main(options, args))