blob: 255113954960ffbfe18190d0d591981e1eb840de [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
16import base64
17import BaseHTTPServer
18import cgi
initial.commit94958cf2008-07-26 22:42:52 +000019import optparse
20import os
21import re
stoyan@chromium.org372692c2009-01-30 17:01:52 +000022import shutil
initial.commit94958cf2008-07-26 22:42:52 +000023import SocketServer
24import sys
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +000025import struct
initial.commit94958cf2008-07-26 22:42:52 +000026import time
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +000027import urlparse
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +000028import warnings
29
30# Ignore deprecation warnings, they make our output more cluttered.
31warnings.filterwarnings("ignore", category=DeprecationWarning)
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000032
33import pyftpdlib.ftpserver
initial.commit94958cf2008-07-26 22:42:52 +000034import tlslite
35import tlslite.api
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000036
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +000037try:
38 import hashlib
39 _new_md5 = hashlib.md5
40except ImportError:
41 import md5
42 _new_md5 = md5.new
43
davidben@chromium.org06fcf202010-09-22 18:15:23 +000044if sys.platform == 'win32':
45 import msvcrt
46
maruel@chromium.org756cf982009-03-05 12:46:38 +000047SERVER_HTTP = 0
erikkay@google.comd5182ff2009-01-08 20:45:27 +000048SERVER_FTP = 1
akalin@chromium.org154bb132010-11-12 02:20:27 +000049SERVER_SYNC = 2
initial.commit94958cf2008-07-26 22:42:52 +000050
51debug_output = sys.stderr
52def debug(str):
53 debug_output.write(str + "\n")
54 debug_output.flush()
55
56class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
57 """This is a specialization of of BaseHTTPServer to allow it
58 to be exited cleanly (by setting its "stop" member to True)."""
59
60 def serve_forever(self):
61 self.stop = False
tonyg@chromium.org75054202010-03-31 22:06:10 +000062 self.nonce_time = None
initial.commit94958cf2008-07-26 22:42:52 +000063 while not self.stop:
64 self.handle_request()
65 self.socket.close()
66
67class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
68 """This is a specialization of StoppableHTTPerver that add https support."""
69
davidben@chromium.org31282a12010-08-07 01:10:02 +000070 def __init__(self, server_address, request_hander_class, cert_path,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000071 ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers):
initial.commit94958cf2008-07-26 22:42:52 +000072 s = open(cert_path).read()
73 x509 = tlslite.api.X509()
74 x509.parse(s)
75 self.cert_chain = tlslite.api.X509CertChain([x509])
76 s = open(cert_path).read()
77 self.private_key = tlslite.api.parsePEMKey(s, private=True)
davidben@chromium.org31282a12010-08-07 01:10:02 +000078 self.ssl_client_auth = ssl_client_auth
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +000079 self.ssl_client_cas = []
80 for ca_file in ssl_client_cas:
81 s = open(ca_file).read()
82 x509 = tlslite.api.X509()
83 x509.parse(s)
84 self.ssl_client_cas.append(x509.subject)
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000085 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
86 if ssl_bulk_ciphers is not None:
87 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
initial.commit94958cf2008-07-26 22:42:52 +000088
89 self.session_cache = tlslite.api.SessionCache()
90 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
91
92 def handshake(self, tlsConnection):
93 """Creates the SSL connection."""
94 try:
95 tlsConnection.handshakeServer(certChain=self.cert_chain,
96 privateKey=self.private_key,
davidben@chromium.org31282a12010-08-07 01:10:02 +000097 sessionCache=self.session_cache,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +000098 reqCert=self.ssl_client_auth,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000099 settings=self.ssl_handshake_settings,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000100 reqCAs=self.ssl_client_cas)
initial.commit94958cf2008-07-26 22:42:52 +0000101 tlsConnection.ignoreAbruptClose = True
102 return True
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +0000103 except tlslite.api.TLSAbruptCloseError:
104 # Ignore abrupt close.
105 return True
initial.commit94958cf2008-07-26 22:42:52 +0000106 except tlslite.api.TLSError, error:
107 print "Handshake failure:", str(error)
108 return False
109
akalin@chromium.org154bb132010-11-12 02:20:27 +0000110
111class SyncHTTPServer(StoppableHTTPServer):
112 """An HTTP server that handles sync commands."""
113
114 def __init__(self, server_address, request_handler_class):
115 # We import here to avoid pulling in chromiumsync's dependencies
116 # unless strictly necessary.
117 import chromiumsync
118 self._sync_handler = chromiumsync.TestServer()
119 StoppableHTTPServer.__init__(self, server_address, request_handler_class)
120
121 def HandleCommand(self, query, raw_request):
122 return self._sync_handler.HandleCommand(query, raw_request)
123
124
125class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
126
127 def __init__(self, request, client_address, socket_server,
128 connect_handlers, get_handlers, post_handlers, put_handlers):
129 self._connect_handlers = connect_handlers
130 self._get_handlers = get_handlers
131 self._post_handlers = post_handlers
132 self._put_handlers = put_handlers
133 BaseHTTPServer.BaseHTTPRequestHandler.__init__(
134 self, request, client_address, socket_server)
135
136 def log_request(self, *args, **kwargs):
137 # Disable request logging to declutter test log output.
138 pass
139
140 def _ShouldHandleRequest(self, handler_name):
141 """Determines if the path can be handled by the handler.
142
143 We consider a handler valid if the path begins with the
144 handler name. It can optionally be followed by "?*", "/*".
145 """
146
147 pattern = re.compile('%s($|\?|/).*' % handler_name)
148 return pattern.match(self.path)
149
150 def do_CONNECT(self):
151 for handler in self._connect_handlers:
152 if handler():
153 return
154
155 def do_GET(self):
156 for handler in self._get_handlers:
157 if handler():
158 return
159
160 def do_POST(self):
161 for handler in self._post_handlers:
162 if handler():
163 return
164
165 def do_PUT(self):
166 for handler in self._put_handlers:
167 if handler():
168 return
169
170
171class TestPageHandler(BasePageHandler):
initial.commit94958cf2008-07-26 22:42:52 +0000172
173 def __init__(self, request, client_address, socket_server):
akalin@chromium.org154bb132010-11-12 02:20:27 +0000174 connect_handlers = [
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000175 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000176 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000177 self.DefaultConnectResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000178 get_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000179 self.NoCacheMaxAgeTimeHandler,
180 self.NoCacheTimeHandler,
181 self.CacheTimeHandler,
182 self.CacheExpiresHandler,
183 self.CacheProxyRevalidateHandler,
184 self.CachePrivateHandler,
185 self.CachePublicHandler,
186 self.CacheSMaxAgeHandler,
187 self.CacheMustRevalidateHandler,
188 self.CacheMustRevalidateMaxAgeHandler,
189 self.CacheNoStoreHandler,
190 self.CacheNoStoreMaxAgeHandler,
191 self.CacheNoTransformHandler,
192 self.DownloadHandler,
193 self.DownloadFinishHandler,
194 self.EchoHeader,
ananta@chromium.org219b2062009-10-23 16:09:41 +0000195 self.EchoHeaderOverride,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000196 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000197 self.FileHandler,
198 self.RealFileWithCommonHeaderHandler,
199 self.RealBZ2FileWithCommonHeaderHandler,
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000200 self.SetCookieHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000201 self.AuthBasicHandler,
202 self.AuthDigestHandler,
203 self.SlowServerHandler,
204 self.ContentTypeHandler,
205 self.ServerRedirectHandler,
206 self.ClientRedirectHandler,
tony@chromium.org03266982010-03-05 03:18:42 +0000207 self.MultipartHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000208 self.DefaultResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000209 post_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000210 self.EchoTitleHandler,
211 self.EchoAllHandler,
mnissler@chromium.org7c939802010-11-11 08:47:14 +0000212 self.EchoHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000213 self.DeviceManagementHandler] + get_handlers
214 put_handlers = [
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000215 self.EchoTitleHandler,
216 self.EchoAllHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000217 self.EchoHandler] + get_handlers
initial.commit94958cf2008-07-26 22:42:52 +0000218
maruel@google.come250a9b2009-03-10 17:39:46 +0000219 self._mime_types = {
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000220 'crx' : 'application/x-chrome-extension',
maruel@google.come250a9b2009-03-10 17:39:46 +0000221 'gif': 'image/gif',
222 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000223 'jpg' : 'image/jpeg',
jam@chromium.org41550782010-11-17 23:47:50 +0000224 'xml' : 'text/xml',
225 'pdf' : 'application/pdf'
maruel@google.come250a9b2009-03-10 17:39:46 +0000226 }
initial.commit94958cf2008-07-26 22:42:52 +0000227 self._default_mime_type = 'text/html'
228
akalin@chromium.org154bb132010-11-12 02:20:27 +0000229 BasePageHandler.__init__(self, request, client_address, socket_server,
230 connect_handlers, get_handlers, post_handlers,
231 put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000232
initial.commit94958cf2008-07-26 22:42:52 +0000233 def GetMIMETypeFromName(self, file_name):
234 """Returns the mime type for the specified file_name. So far it only looks
235 at the file extension."""
236
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000237 (shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000238 if len(extension) == 0:
239 # no extension.
240 return self._default_mime_type
241
ericroman@google.comc17ca532009-05-07 03:51:05 +0000242 # extension starts with a dot, so we need to remove it
243 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000244
initial.commit94958cf2008-07-26 22:42:52 +0000245 def NoCacheMaxAgeTimeHandler(self):
246 """This request handler yields a page with the title set to the current
247 system time, and no caching requested."""
248
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000249 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000250 return False
251
252 self.send_response(200)
253 self.send_header('Cache-Control', 'max-age=0')
254 self.send_header('Content-type', 'text/html')
255 self.end_headers()
256
maruel@google.come250a9b2009-03-10 17:39:46 +0000257 self.wfile.write('<html><head><title>%s</title></head></html>' %
258 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000259
260 return True
261
262 def NoCacheTimeHandler(self):
263 """This request handler yields a page with the title set to the current
264 system time, and no caching requested."""
265
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000266 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000267 return False
268
269 self.send_response(200)
270 self.send_header('Cache-Control', 'no-cache')
271 self.send_header('Content-type', 'text/html')
272 self.end_headers()
273
maruel@google.come250a9b2009-03-10 17:39:46 +0000274 self.wfile.write('<html><head><title>%s</title></head></html>' %
275 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000276
277 return True
278
279 def CacheTimeHandler(self):
280 """This request handler yields a page with the title set to the current
281 system time, and allows caching for one minute."""
282
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000283 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000284 return False
285
286 self.send_response(200)
287 self.send_header('Cache-Control', 'max-age=60')
288 self.send_header('Content-type', 'text/html')
289 self.end_headers()
290
maruel@google.come250a9b2009-03-10 17:39:46 +0000291 self.wfile.write('<html><head><title>%s</title></head></html>' %
292 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000293
294 return True
295
296 def CacheExpiresHandler(self):
297 """This request handler yields a page with the title set to the current
298 system time, and set the page to expire on 1 Jan 2099."""
299
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000300 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000301 return False
302
303 self.send_response(200)
304 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
305 self.send_header('Content-type', 'text/html')
306 self.end_headers()
307
maruel@google.come250a9b2009-03-10 17:39:46 +0000308 self.wfile.write('<html><head><title>%s</title></head></html>' %
309 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000310
311 return True
312
313 def CacheProxyRevalidateHandler(self):
314 """This request handler yields a page with the title set to the current
315 system time, and allows caching for 60 seconds"""
316
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000317 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000318 return False
319
320 self.send_response(200)
321 self.send_header('Content-type', 'text/html')
322 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
323 self.end_headers()
324
maruel@google.come250a9b2009-03-10 17:39:46 +0000325 self.wfile.write('<html><head><title>%s</title></head></html>' %
326 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000327
328 return True
329
330 def CachePrivateHandler(self):
331 """This request handler yields a page with the title set to the current
332 system time, and allows caching for 5 seconds."""
333
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000334 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000335 return False
336
337 self.send_response(200)
338 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000339 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000340 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 CachePublicHandler(self):
348 """This request handler yields a page with the title set to the current
349 system time, and allows caching for 5 seconds."""
350
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000351 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000352 return False
353
354 self.send_response(200)
355 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000356 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000357 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 CacheSMaxAgeHandler(self):
365 """This request handler yields a page with the title set to the current
366 system time, and does not allow for caching."""
367
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000368 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000369 return False
370
371 self.send_response(200)
372 self.send_header('Content-type', 'text/html')
373 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
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 CacheMustRevalidateHandler(self):
382 """This request handler yields a page with the title set to the current
383 system time, and does not allow caching."""
384
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000385 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000386 return False
387
388 self.send_response(200)
389 self.send_header('Content-type', 'text/html')
390 self.send_header('Cache-Control', 'must-revalidate')
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 CacheMustRevalidateMaxAgeHandler(self):
399 """This request handler yields a page with the title set to the current
400 system time, and does not allow caching event though max-age of 60
401 seconds is specified."""
402
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000403 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000404 return False
405
406 self.send_response(200)
407 self.send_header('Content-type', 'text/html')
408 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
409 self.end_headers()
410
maruel@google.come250a9b2009-03-10 17:39:46 +0000411 self.wfile.write('<html><head><title>%s</title></head></html>' %
412 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000413
414 return True
415
initial.commit94958cf2008-07-26 22:42:52 +0000416 def CacheNoStoreHandler(self):
417 """This request handler yields a page with the title set to the current
418 system time, and does not allow the page to be stored."""
419
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000420 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000421 return False
422
423 self.send_response(200)
424 self.send_header('Content-type', 'text/html')
425 self.send_header('Cache-Control', 'no-store')
426 self.end_headers()
427
maruel@google.come250a9b2009-03-10 17:39:46 +0000428 self.wfile.write('<html><head><title>%s</title></head></html>' %
429 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000430
431 return True
432
433 def CacheNoStoreMaxAgeHandler(self):
434 """This request handler yields a page with the title set to the current
435 system time, and does not allow the page to be stored even though max-age
436 of 60 seconds is specified."""
437
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000438 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000439 return False
440
441 self.send_response(200)
442 self.send_header('Content-type', 'text/html')
443 self.send_header('Cache-Control', 'max-age=60, no-store')
444 self.end_headers()
445
maruel@google.come250a9b2009-03-10 17:39:46 +0000446 self.wfile.write('<html><head><title>%s</title></head></html>' %
447 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000448
449 return True
450
451
452 def CacheNoTransformHandler(self):
453 """This request handler yields a page with the title set to the current
454 system time, and does not allow the content to transformed during
455 user-agent caching"""
456
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000457 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000458 return False
459
460 self.send_response(200)
461 self.send_header('Content-type', 'text/html')
462 self.send_header('Cache-Control', 'no-transform')
463 self.end_headers()
464
maruel@google.come250a9b2009-03-10 17:39:46 +0000465 self.wfile.write('<html><head><title>%s</title></head></html>' %
466 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000467
468 return True
469
470 def EchoHeader(self):
471 """This handler echoes back the value of a specific request header."""
ananta@chromium.org219b2062009-10-23 16:09:41 +0000472 """The only difference between this function and the EchoHeaderOverride"""
473 """function is in the parameter being passed to the helper function"""
474 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000475
ananta@chromium.org219b2062009-10-23 16:09:41 +0000476 def EchoHeaderOverride(self):
477 """This handler echoes back the value of a specific request header."""
478 """The UrlRequest unit tests also execute for ChromeFrame which uses"""
479 """IE to issue HTTP requests using the host network stack."""
480 """The Accept and Charset tests which expect the server to echo back"""
481 """the corresponding headers fail here as IE returns cached responses"""
482 """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
483 """treats this request as a new request and does not cache it."""
484 return self.EchoHeaderHelper("/echoheaderoverride")
485
486 def EchoHeaderHelper(self, echo_header):
487 """This function echoes back the value of the request header passed in."""
488 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000489 return False
490
491 query_char = self.path.find('?')
492 if query_char != -1:
493 header_name = self.path[query_char+1:]
494
495 self.send_response(200)
496 self.send_header('Content-type', 'text/plain')
497 self.send_header('Cache-control', 'max-age=60000')
498 # insert a vary header to properly indicate that the cachability of this
499 # request is subject to value of the request header being echoed.
500 if len(header_name) > 0:
501 self.send_header('Vary', header_name)
502 self.end_headers()
503
504 if len(header_name) > 0:
505 self.wfile.write(self.headers.getheader(header_name))
506
507 return True
508
509 def EchoHandler(self):
510 """This handler just echoes back the payload of the request, for testing
511 form submission."""
512
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000513 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000514 return False
515
516 self.send_response(200)
517 self.send_header('Content-type', 'text/html')
518 self.end_headers()
519 length = int(self.headers.getheader('content-length'))
520 request = self.rfile.read(length)
521 self.wfile.write(request)
522 return True
523
524 def EchoTitleHandler(self):
525 """This handler is like Echo, but sets the page title to the request."""
526
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000527 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000528 return False
529
530 self.send_response(200)
531 self.send_header('Content-type', 'text/html')
532 self.end_headers()
533 length = int(self.headers.getheader('content-length'))
534 request = self.rfile.read(length)
535 self.wfile.write('<html><head><title>')
536 self.wfile.write(request)
537 self.wfile.write('</title></head></html>')
538 return True
539
540 def EchoAllHandler(self):
541 """This handler yields a (more) human-readable page listing information
542 about the request header & contents."""
543
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000544 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000545 return False
546
547 self.send_response(200)
548 self.send_header('Content-type', 'text/html')
549 self.end_headers()
550 self.wfile.write('<html><head><style>'
551 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
552 '</style></head><body>'
553 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000554 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000555 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000556
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000557 if self.command == 'POST' or self.command == 'PUT':
ericroman@google.coma47622b2008-11-15 04:36:51 +0000558 length = int(self.headers.getheader('content-length'))
559 qs = self.rfile.read(length)
560 params = cgi.parse_qs(qs, keep_blank_values=1)
561
562 for param in params:
563 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000564
565 self.wfile.write('</pre>')
566
567 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
568
569 self.wfile.write('</body></html>')
570 return True
571
572 def DownloadHandler(self):
573 """This handler sends a downloadable file with or without reporting
574 the size (6K)."""
575
576 if self.path.startswith("/download-unknown-size"):
577 send_length = False
578 elif self.path.startswith("/download-known-size"):
579 send_length = True
580 else:
581 return False
582
583 #
584 # The test which uses this functionality is attempting to send
585 # small chunks of data to the client. Use a fairly large buffer
586 # so that we'll fill chrome's IO buffer enough to force it to
587 # actually write the data.
588 # See also the comments in the client-side of this test in
589 # download_uitest.cc
590 #
591 size_chunk1 = 35*1024
592 size_chunk2 = 10*1024
593
594 self.send_response(200)
595 self.send_header('Content-type', 'application/octet-stream')
596 self.send_header('Cache-Control', 'max-age=0')
597 if send_length:
598 self.send_header('Content-Length', size_chunk1 + size_chunk2)
599 self.end_headers()
600
601 # First chunk of data:
602 self.wfile.write("*" * size_chunk1)
603 self.wfile.flush()
604
605 # handle requests until one of them clears this flag.
606 self.server.waitForDownload = True
607 while self.server.waitForDownload:
608 self.server.handle_request()
609
610 # Second chunk of data:
611 self.wfile.write("*" * size_chunk2)
612 return True
613
614 def DownloadFinishHandler(self):
615 """This handler just tells the server to finish the current download."""
616
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000617 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000618 return False
619
620 self.server.waitForDownload = False
621 self.send_response(200)
622 self.send_header('Content-type', 'text/html')
623 self.send_header('Cache-Control', 'max-age=0')
624 self.end_headers()
625 return True
626
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000627 def _ReplaceFileData(self, data, query_parameters):
628 """Replaces matching substrings in a file.
629
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000630 If the 'replace_text' URL query parameter is present, it is expected to be
631 of the form old_text:new_text, which indicates that any old_text strings in
632 the file are replaced with new_text. Multiple 'replace_text' parameters may
633 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000634
635 If the parameters are not present, |data| is returned.
636 """
637 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000638 replace_text_values = query_dict.get('replace_text', [])
639 for replace_text_value in replace_text_values:
640 replace_text_args = replace_text_value.split(':')
641 if len(replace_text_args) != 2:
642 raise ValueError(
643 'replace_text must be of form old_text:new_text. Actual value: %s' %
644 replace_text_value)
645 old_text_b64, new_text_b64 = replace_text_args
646 old_text = base64.urlsafe_b64decode(old_text_b64)
647 new_text = base64.urlsafe_b64decode(new_text_b64)
648 data = data.replace(old_text, new_text)
649 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000650
initial.commit94958cf2008-07-26 22:42:52 +0000651 def FileHandler(self):
652 """This handler sends the contents of the requested file. Wow, it's like
653 a real webserver!"""
654
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000655 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000656 if not self.path.startswith(prefix):
657 return False
658
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000659 # Consume a request body if present.
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000660 if self.command == 'POST' or self.command == 'PUT' :
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000661 self.rfile.read(int(self.headers.getheader('content-length')))
662
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000663 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
664 sub_path = url_path[len(prefix):]
665 entries = sub_path.split('/')
666 file_path = os.path.join(self.server.data_dir, *entries)
667 if os.path.isdir(file_path):
668 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000669
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000670 if not os.path.isfile(file_path):
671 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000672 self.send_error(404)
673 return True
674
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000675 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000676 data = f.read()
677 f.close()
678
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000679 data = self._ReplaceFileData(data, query)
680
initial.commit94958cf2008-07-26 22:42:52 +0000681 # If file.mock-http-headers exists, it contains the headers we
682 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000683 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000684 if os.path.isfile(headers_path):
685 f = open(headers_path, "r")
686
687 # "HTTP/1.1 200 OK"
688 response = f.readline()
689 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
690 self.send_response(int(status_code))
691
692 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000693 header_values = re.findall('(\S+):\s*(.*)', line)
694 if len(header_values) > 0:
695 # "name: value"
696 name, value = header_values[0]
697 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000698 f.close()
699 else:
700 # Could be more generic once we support mime-type sniffing, but for
701 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000702
703 range = self.headers.get('Range')
704 if range and range.startswith('bytes='):
705 # Note this doesn't handle all valid byte range values (i.e. open ended
706 # ones), just enough for what we needed so far.
707 range = range[6:].split('-')
708 start = int(range[0])
709 end = int(range[1])
710
711 self.send_response(206)
712 content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
713 str(len(data))
714 self.send_header('Content-Range', content_range)
715 data = data[start: end + 1]
716 else:
717 self.send_response(200)
718
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000719 self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000720 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000721 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000722 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000723 self.end_headers()
724
725 self.wfile.write(data)
726
727 return True
728
729 def RealFileWithCommonHeaderHandler(self):
730 """This handler sends the contents of the requested file without the pseudo
731 http head!"""
732
733 prefix='/realfiles/'
734 if not self.path.startswith(prefix):
735 return False
736
737 file = self.path[len(prefix):]
738 path = os.path.join(self.server.data_dir, file)
739
740 try:
741 f = open(path, "rb")
742 data = f.read()
743 f.close()
744
745 # just simply set the MIME as octal stream
746 self.send_response(200)
747 self.send_header('Content-type', 'application/octet-stream')
748 self.end_headers()
749
750 self.wfile.write(data)
751 except:
752 self.send_error(404)
753
754 return True
755
756 def RealBZ2FileWithCommonHeaderHandler(self):
757 """This handler sends the bzip2 contents of the requested file with
758 corresponding Content-Encoding field in http head!"""
759
760 prefix='/realbz2files/'
761 if not self.path.startswith(prefix):
762 return False
763
764 parts = self.path.split('?')
765 file = parts[0][len(prefix):]
766 path = os.path.join(self.server.data_dir, file) + '.bz2'
767
768 if len(parts) > 1:
769 options = parts[1]
770 else:
771 options = ''
772
773 try:
774 self.send_response(200)
775 accept_encoding = self.headers.get("Accept-Encoding")
776 if accept_encoding.find("bzip2") != -1:
777 f = open(path, "rb")
778 data = f.read()
779 f.close()
780 self.send_header('Content-Encoding', 'bzip2')
781 self.send_header('Content-type', 'application/x-bzip2')
782 self.end_headers()
783 if options == 'incremental-header':
784 self.wfile.write(data[:1])
785 self.wfile.flush()
786 time.sleep(1.0)
787 self.wfile.write(data[1:])
788 else:
789 self.wfile.write(data)
790 else:
791 """client do not support bzip2 format, send pseudo content
792 """
793 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
794 self.end_headers()
795 self.wfile.write("you do not support bzip2 encoding")
796 except:
797 self.send_error(404)
798
799 return True
800
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000801 def SetCookieHandler(self):
802 """This handler just sets a cookie, for testing cookie handling."""
803
804 if not self._ShouldHandleRequest("/set-cookie"):
805 return False
806
807 query_char = self.path.find('?')
808 if query_char != -1:
809 cookie_values = self.path[query_char + 1:].split('&')
810 else:
811 cookie_values = ("",)
812 self.send_response(200)
813 self.send_header('Content-type', 'text/html')
814 for cookie_value in cookie_values:
815 self.send_header('Set-Cookie', '%s' % cookie_value)
816 self.end_headers()
817 for cookie_value in cookie_values:
818 self.wfile.write('%s' % cookie_value)
819 return True
820
initial.commit94958cf2008-07-26 22:42:52 +0000821 def AuthBasicHandler(self):
822 """This handler tests 'Basic' authentication. It just sends a page with
823 title 'user/pass' if you succeed."""
824
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000825 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000826 return False
827
828 username = userpass = password = b64str = ""
829
ericroman@google.com239b4d82009-03-27 04:00:22 +0000830 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
831
initial.commit94958cf2008-07-26 22:42:52 +0000832 auth = self.headers.getheader('authorization')
833 try:
834 if not auth:
835 raise Exception('no auth')
836 b64str = re.findall(r'Basic (\S+)', auth)[0]
837 userpass = base64.b64decode(b64str)
838 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
839 if password != 'secret':
840 raise Exception('wrong password')
841 except Exception, e:
842 # Authentication failed.
843 self.send_response(401)
844 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
845 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000846 if set_cookie_if_challenged:
847 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000848 self.end_headers()
849 self.wfile.write('<html><head>')
850 self.wfile.write('<title>Denied: %s</title>' % e)
851 self.wfile.write('</head><body>')
852 self.wfile.write('auth=%s<p>' % auth)
853 self.wfile.write('b64str=%s<p>' % b64str)
854 self.wfile.write('username: %s<p>' % username)
855 self.wfile.write('userpass: %s<p>' % userpass)
856 self.wfile.write('password: %s<p>' % password)
857 self.wfile.write('You sent:<br>%s<p>' % self.headers)
858 self.wfile.write('</body></html>')
859 return True
860
861 # Authentication successful. (Return a cachable response to allow for
862 # testing cached pages that require authentication.)
863 if_none_match = self.headers.getheader('if-none-match')
864 if if_none_match == "abc":
865 self.send_response(304)
866 self.end_headers()
867 else:
868 self.send_response(200)
869 self.send_header('Content-type', 'text/html')
870 self.send_header('Cache-control', 'max-age=60000')
871 self.send_header('Etag', 'abc')
872 self.end_headers()
873 self.wfile.write('<html><head>')
874 self.wfile.write('<title>%s/%s</title>' % (username, password))
875 self.wfile.write('</head><body>')
876 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000877 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000878 self.wfile.write('</body></html>')
879
880 return True
881
tonyg@chromium.org75054202010-03-31 22:06:10 +0000882 def GetNonce(self, force_reset=False):
883 """Returns a nonce that's stable per request path for the server's lifetime.
initial.commit94958cf2008-07-26 22:42:52 +0000884
tonyg@chromium.org75054202010-03-31 22:06:10 +0000885 This is a fake implementation. A real implementation would only use a given
886 nonce a single time (hence the name n-once). However, for the purposes of
887 unittesting, we don't care about the security of the nonce.
888
889 Args:
890 force_reset: Iff set, the nonce will be changed. Useful for testing the
891 "stale" response.
892 """
893 if force_reset or not self.server.nonce_time:
894 self.server.nonce_time = time.time()
895 return _new_md5('privatekey%s%d' %
896 (self.path, self.server.nonce_time)).hexdigest()
897
898 def AuthDigestHandler(self):
899 """This handler tests 'Digest' authentication.
900
901 It just sends a page with title 'user/pass' if you succeed.
902
903 A stale response is sent iff "stale" is present in the request path.
904 """
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000905 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000906 return False
907
tonyg@chromium.org75054202010-03-31 22:06:10 +0000908 stale = 'stale' in self.path
909 nonce = self.GetNonce(force_reset=stale)
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000910 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000911 password = 'secret'
912 realm = 'testrealm'
913
914 auth = self.headers.getheader('authorization')
915 pairs = {}
916 try:
917 if not auth:
918 raise Exception('no auth')
919 if not auth.startswith('Digest'):
920 raise Exception('not digest')
921 # Pull out all the name="value" pairs as a dictionary.
922 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
923
924 # Make sure it's all valid.
925 if pairs['nonce'] != nonce:
926 raise Exception('wrong nonce')
927 if pairs['opaque'] != opaque:
928 raise Exception('wrong opaque')
929
930 # Check the 'response' value and make sure it matches our magic hash.
931 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000932 hash_a1 = _new_md5(
933 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000934 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000935 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000936 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000937 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
938 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000939 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000940
941 if pairs['response'] != response:
942 raise Exception('wrong password')
943 except Exception, e:
944 # Authentication failed.
945 self.send_response(401)
946 hdr = ('Digest '
947 'realm="%s", '
948 'domain="/", '
949 'qop="auth", '
950 'algorithm=MD5, '
951 'nonce="%s", '
952 'opaque="%s"') % (realm, nonce, opaque)
953 if stale:
954 hdr += ', stale="TRUE"'
955 self.send_header('WWW-Authenticate', hdr)
956 self.send_header('Content-type', 'text/html')
957 self.end_headers()
958 self.wfile.write('<html><head>')
959 self.wfile.write('<title>Denied: %s</title>' % e)
960 self.wfile.write('</head><body>')
961 self.wfile.write('auth=%s<p>' % auth)
962 self.wfile.write('pairs=%s<p>' % pairs)
963 self.wfile.write('You sent:<br>%s<p>' % self.headers)
964 self.wfile.write('We are replying:<br>%s<p>' % hdr)
965 self.wfile.write('</body></html>')
966 return True
967
968 # Authentication successful.
969 self.send_response(200)
970 self.send_header('Content-type', 'text/html')
971 self.end_headers()
972 self.wfile.write('<html><head>')
973 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
974 self.wfile.write('</head><body>')
975 self.wfile.write('auth=%s<p>' % auth)
976 self.wfile.write('pairs=%s<p>' % pairs)
977 self.wfile.write('</body></html>')
978
979 return True
980
981 def SlowServerHandler(self):
982 """Wait for the user suggested time before responding. The syntax is
983 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000984 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000985 return False
986 query_char = self.path.find('?')
987 wait_sec = 1.0
988 if query_char >= 0:
989 try:
990 wait_sec = int(self.path[query_char + 1:])
991 except ValueError:
992 pass
993 time.sleep(wait_sec)
994 self.send_response(200)
995 self.send_header('Content-type', 'text/plain')
996 self.end_headers()
997 self.wfile.write("waited %d seconds" % wait_sec)
998 return True
999
1000 def ContentTypeHandler(self):
1001 """Returns a string of html with the given content type. E.g.,
1002 /contenttype?text/css returns an html file with the Content-Type
1003 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001004 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001005 return False
1006 query_char = self.path.find('?')
1007 content_type = self.path[query_char + 1:].strip()
1008 if not content_type:
1009 content_type = 'text/html'
1010 self.send_response(200)
1011 self.send_header('Content-Type', content_type)
1012 self.end_headers()
1013 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1014 return True
1015
1016 def ServerRedirectHandler(self):
1017 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001018 '/server-redirect?http://foo.bar/asdf' to redirect to
1019 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001020
1021 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001022 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001023 return False
1024
1025 query_char = self.path.find('?')
1026 if query_char < 0 or len(self.path) <= query_char + 1:
1027 self.sendRedirectHelp(test_name)
1028 return True
1029 dest = self.path[query_char + 1:]
1030
1031 self.send_response(301) # moved permanently
1032 self.send_header('Location', dest)
1033 self.send_header('Content-type', 'text/html')
1034 self.end_headers()
1035 self.wfile.write('<html><head>')
1036 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1037
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001038 return True
initial.commit94958cf2008-07-26 22:42:52 +00001039
1040 def ClientRedirectHandler(self):
1041 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001042 '/client-redirect?http://foo.bar/asdf' to redirect to
1043 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001044
1045 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001046 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001047 return False
1048
1049 query_char = self.path.find('?');
1050 if query_char < 0 or len(self.path) <= query_char + 1:
1051 self.sendRedirectHelp(test_name)
1052 return True
1053 dest = self.path[query_char + 1:]
1054
1055 self.send_response(200)
1056 self.send_header('Content-type', 'text/html')
1057 self.end_headers()
1058 self.wfile.write('<html><head>')
1059 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1060 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1061
1062 return True
1063
tony@chromium.org03266982010-03-05 03:18:42 +00001064 def MultipartHandler(self):
1065 """Send a multipart response (10 text/html pages)."""
1066 test_name = "/multipart"
1067 if not self._ShouldHandleRequest(test_name):
1068 return False
1069
1070 num_frames = 10
1071 bound = '12345'
1072 self.send_response(200)
1073 self.send_header('Content-type',
1074 'multipart/x-mixed-replace;boundary=' + bound)
1075 self.end_headers()
1076
1077 for i in xrange(num_frames):
1078 self.wfile.write('--' + bound + '\r\n')
1079 self.wfile.write('Content-type: text/html\r\n\r\n')
1080 self.wfile.write('<title>page ' + str(i) + '</title>')
1081 self.wfile.write('page ' + str(i))
1082
1083 self.wfile.write('--' + bound + '--')
1084 return True
1085
initial.commit94958cf2008-07-26 22:42:52 +00001086 def DefaultResponseHandler(self):
1087 """This is the catch-all response handler for requests that aren't handled
1088 by one of the special handlers above.
1089 Note that we specify the content-length as without it the https connection
1090 is not closed properly (and the browser keeps expecting data)."""
1091
1092 contents = "Default response given for path: " + self.path
1093 self.send_response(200)
1094 self.send_header('Content-type', 'text/html')
1095 self.send_header("Content-Length", len(contents))
1096 self.end_headers()
1097 self.wfile.write(contents)
1098 return True
1099
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001100 def RedirectConnectHandler(self):
1101 """Sends a redirect to the CONNECT request for www.redirect.com. This
1102 response is not specified by the RFC, so the browser should not follow
1103 the redirect."""
1104
1105 if (self.path.find("www.redirect.com") < 0):
1106 return False
1107
1108 dest = "http://www.destination.com/foo.js"
1109
1110 self.send_response(302) # moved temporarily
1111 self.send_header('Location', dest)
1112 self.send_header('Connection', 'close')
1113 self.end_headers()
1114 return True
1115
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001116 def ServerAuthConnectHandler(self):
1117 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1118 response doesn't make sense because the proxy server cannot request
1119 server authentication."""
1120
1121 if (self.path.find("www.server-auth.com") < 0):
1122 return False
1123
1124 challenge = 'Basic realm="WallyWorld"'
1125
1126 self.send_response(401) # unauthorized
1127 self.send_header('WWW-Authenticate', challenge)
1128 self.send_header('Connection', 'close')
1129 self.end_headers()
1130 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001131
1132 def DefaultConnectResponseHandler(self):
1133 """This is the catch-all response handler for CONNECT requests that aren't
1134 handled by one of the special handlers above. Real Web servers respond
1135 with 400 to CONNECT requests."""
1136
1137 contents = "Your client has issued a malformed or illegal request."
1138 self.send_response(400) # bad request
1139 self.send_header('Content-type', 'text/html')
1140 self.send_header("Content-Length", len(contents))
1141 self.end_headers()
1142 self.wfile.write(contents)
1143 return True
1144
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001145 def DeviceManagementHandler(self):
1146 """Delegates to the device management service used for cloud policy."""
1147 if not self._ShouldHandleRequest("/device_management"):
1148 return False
1149
1150 length = int(self.headers.getheader('content-length'))
1151 raw_request = self.rfile.read(length)
1152
1153 if not self.server._device_management_handler:
1154 import device_management
1155 policy_path = os.path.join(self.server.data_dir, 'device_management')
1156 self.server._device_management_handler = (
1157 device_management.TestServer(policy_path))
1158
1159 http_response, raw_reply = (
1160 self.server._device_management_handler.HandleRequest(self.path,
1161 self.headers,
1162 raw_request))
1163 self.send_response(http_response)
1164 self.end_headers()
1165 self.wfile.write(raw_reply)
1166 return True
1167
initial.commit94958cf2008-07-26 22:42:52 +00001168 # called by the redirect handling function when there is no parameter
1169 def sendRedirectHelp(self, redirect_name):
1170 self.send_response(200)
1171 self.send_header('Content-type', 'text/html')
1172 self.end_headers()
1173 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1174 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1175 self.wfile.write('</body></html>')
1176
akalin@chromium.org154bb132010-11-12 02:20:27 +00001177
1178class SyncPageHandler(BasePageHandler):
1179 """Handler for the main HTTP sync server."""
1180
1181 def __init__(self, request, client_address, sync_http_server):
1182 get_handlers = [self.ChromiumSyncTimeHandler]
1183 post_handlers = [self.ChromiumSyncCommandHandler]
1184 BasePageHandler.__init__(self, request, client_address,
1185 sync_http_server, [], get_handlers,
1186 post_handlers, [])
1187
1188 def ChromiumSyncTimeHandler(self):
1189 """Handle Chromium sync .../time requests.
1190
1191 The syncer sometimes checks server reachability by examining /time.
1192 """
1193 test_name = "/chromiumsync/time"
1194 if not self._ShouldHandleRequest(test_name):
1195 return False
1196
1197 self.send_response(200)
1198 self.send_header('Content-type', 'text/html')
1199 self.end_headers()
1200 return True
1201
1202 def ChromiumSyncCommandHandler(self):
1203 """Handle a chromiumsync command arriving via http.
1204
1205 This covers all sync protocol commands: authentication, getupdates, and
1206 commit.
1207 """
1208 test_name = "/chromiumsync/command"
1209 if not self._ShouldHandleRequest(test_name):
1210 return False
1211
1212 length = int(self.headers.getheader('content-length'))
1213 raw_request = self.rfile.read(length)
1214
1215 http_response, raw_reply = self.server.HandleCommand(
1216 self.path, raw_request)
1217 self.send_response(http_response)
1218 self.end_headers()
1219 self.wfile.write(raw_reply)
1220 return True
1221
1222
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001223def MakeDataDir():
1224 if options.data_dir:
1225 if not os.path.isdir(options.data_dir):
1226 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1227 return None
1228 my_data_dir = options.data_dir
1229 else:
1230 # Create the default path to our data dir, relative to the exe dir.
1231 my_data_dir = os.path.dirname(sys.argv[0])
1232 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00001233 "test", "data")
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001234
1235 #TODO(ibrar): Must use Find* funtion defined in google\tools
1236 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1237
1238 return my_data_dir
1239
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001240class FileMultiplexer:
1241 def __init__(self, fd1, fd2) :
1242 self.__fd1 = fd1
1243 self.__fd2 = fd2
1244
1245 def __del__(self) :
1246 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1247 self.__fd1.close()
1248 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1249 self.__fd2.close()
1250
1251 def write(self, text) :
1252 self.__fd1.write(text)
1253 self.__fd2.write(text)
1254
1255 def flush(self) :
1256 self.__fd1.flush()
1257 self.__fd2.flush()
1258
initial.commit94958cf2008-07-26 22:42:52 +00001259def main(options, args):
initial.commit94958cf2008-07-26 22:42:52 +00001260 logfile = open('testserver.log', 'w')
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001261 sys.stdout = FileMultiplexer(sys.stdout, logfile)
1262 sys.stderr = FileMultiplexer(sys.stderr, logfile)
initial.commit94958cf2008-07-26 22:42:52 +00001263
1264 port = options.port
1265
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001266 if options.server_type == SERVER_HTTP:
1267 if options.cert:
1268 # let's make sure the cert file exists.
1269 if not os.path.isfile(options.cert):
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001270 print 'specified server cert file not found: ' + options.cert + \
1271 ' exiting...'
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001272 return
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001273 for ca_cert in options.ssl_client_ca:
1274 if not os.path.isfile(ca_cert):
1275 print 'specified trusted client CA file not found: ' + ca_cert + \
1276 ' exiting...'
1277 return
davidben@chromium.org31282a12010-08-07 01:10:02 +00001278 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001279 options.ssl_client_auth, options.ssl_client_ca,
1280 options.ssl_bulk_cipher)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001281 print 'HTTPS server started on port %d...' % server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001282 else:
phajdan.jr@chromium.orgfa49b5f2010-07-23 20:38:56 +00001283 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001284 print 'HTTP server started on port %d...' % server.server_port
erikkay@google.com70397b62008-12-30 21:49:21 +00001285
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001286 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001287 server.file_root_url = options.file_root_url
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001288 listen_port = server.server_port
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001289 server._device_management_handler = None
akalin@chromium.org154bb132010-11-12 02:20:27 +00001290 elif options.server_type == SERVER_SYNC:
1291 server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1292 print 'Sync HTTP server started on port %d...' % server.server_port
1293 listen_port = server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001294 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001295 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001296 my_data_dir = MakeDataDir()
1297
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001298 # Instantiate a dummy authorizer for managing 'virtual' users
1299 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1300
1301 # Define a new user having full r/w permissions and a read-only
1302 # anonymous user
1303 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1304
1305 authorizer.add_anonymous(my_data_dir)
1306
1307 # Instantiate FTP handler class
1308 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1309 ftp_handler.authorizer = authorizer
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001310
1311 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001312 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1313 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001314
1315 # Instantiate FTP server class and listen to 127.0.0.1:port
1316 address = ('127.0.0.1', port)
1317 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001318 listen_port = server.socket.getsockname()[1]
1319 print 'FTP server started on port %d...' % listen_port
initial.commit94958cf2008-07-26 22:42:52 +00001320
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001321 # Notify the parent that we've started. (BaseServer subclasses
1322 # bind their sockets on construction.)
1323 if options.startup_pipe is not None:
1324 if sys.platform == 'win32':
1325 fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1326 else:
1327 fd = options.startup_pipe
1328 startup_pipe = os.fdopen(fd, "w")
akalin@chromium.org2dec8822010-11-25 11:00:09 +00001329 # Write the listening port as a 2 byte value. This is _not_ using
1330 # network byte ordering since the other end of the pipe is on the same
1331 # machine.
akalin@chromium.org2c7605d2010-11-26 03:21:37 +00001332 startup_pipe.write(struct.pack('@H', listen_port))
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001333 startup_pipe.close()
1334
initial.commit94958cf2008-07-26 22:42:52 +00001335 try:
1336 server.serve_forever()
1337 except KeyboardInterrupt:
1338 print 'shutting down server'
1339 server.stop = True
1340
1341if __name__ == '__main__':
1342 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001343 option_parser.add_option("-f", '--ftp', action='store_const',
1344 const=SERVER_FTP, default=SERVER_HTTP,
1345 dest='server_type',
akalin@chromium.org154bb132010-11-12 02:20:27 +00001346 help='start up an FTP server.')
1347 option_parser.add_option('', '--sync', action='store_const',
1348 const=SERVER_SYNC, default=SERVER_HTTP,
1349 dest='server_type',
1350 help='start up a sync server.')
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001351 option_parser.add_option('', '--port', default='0', type='int',
1352 help='Port used by the server. If unspecified, the '
1353 'server will listen on an ephemeral port.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001354 option_parser.add_option('', '--data-dir', dest='data_dir',
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001355 help='Directory from which to read the files.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001356 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001357 help='Specify that https should be used, specify '
1358 'the path to the cert containing the private key '
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001359 'the server should use.')
davidben@chromium.org31282a12010-08-07 01:10:02 +00001360 option_parser.add_option('', '--ssl-client-auth', action='store_true',
1361 help='Require SSL client auth on every connection.')
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001362 option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1363 help='Specify that the client certificate request '
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001364 'should include the CA named in the subject of '
1365 'the DER-encoded certificate contained in the '
1366 'specified file. This option may appear multiple '
1367 'times, indicating multiple CA names should be '
1368 'sent in the request.')
1369 option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1370 help='Specify the bulk encryption algorithm(s)'
1371 'that will be accepted by the SSL server. Valid '
1372 'values are "aes256", "aes128", "3des", "rc4". If '
1373 'omitted, all algorithms will be used. This '
1374 'option may appear multiple times, indicating '
1375 'multiple algorithms should be enabled.');
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001376 option_parser.add_option('', '--file-root-url', default='/files/',
1377 help='Specify a root URL for files served.')
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001378 option_parser.add_option('', '--startup-pipe', type='int',
1379 dest='startup_pipe',
1380 help='File handle of pipe to parent process')
initial.commit94958cf2008-07-26 22:42:52 +00001381 options, args = option_parser.parse_args()
1382
1383 sys.exit(main(options, args))