blob: 14ae182e614f86d89f36f1fbab6c715f747760a6 [file] [log] [blame]
initial.commit94958cf2008-07-26 22:42:52 +00001#!/usr/bin/python2.4
license.botf3378c22008-08-24 00:55:55 +00002# Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
3# 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.
9It defaults to living on localhost:8888.
10It can use https if you specify the flag --https=CERT where CERT is the path
11to a pem file containing the certificate and private key that should be used.
12To shut it down properly, visit localhost:8888/kill.
13"""
14
15import base64
16import BaseHTTPServer
17import cgi
initial.commit94958cf2008-07-26 22:42:52 +000018import optparse
19import os
20import re
stoyan@chromium.org372692c2009-01-30 17:01:52 +000021import shutil
initial.commit94958cf2008-07-26 22:42:52 +000022import SocketServer
23import sys
24import time
25import tlslite
26import tlslite.api
erikkay@google.comd5182ff2009-01-08 20:45:27 +000027import pyftpdlib.ftpserver
28
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +000029try:
30 import hashlib
31 _new_md5 = hashlib.md5
32except ImportError:
33 import md5
34 _new_md5 = md5.new
35
maruel@chromium.org756cf982009-03-05 12:46:38 +000036SERVER_HTTP = 0
erikkay@google.comd5182ff2009-01-08 20:45:27 +000037SERVER_FTP = 1
initial.commit94958cf2008-07-26 22:42:52 +000038
39debug_output = sys.stderr
40def debug(str):
41 debug_output.write(str + "\n")
42 debug_output.flush()
43
44class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
45 """This is a specialization of of BaseHTTPServer to allow it
46 to be exited cleanly (by setting its "stop" member to True)."""
47
48 def serve_forever(self):
49 self.stop = False
50 self.nonce = None
51 while not self.stop:
52 self.handle_request()
53 self.socket.close()
54
55class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
56 """This is a specialization of StoppableHTTPerver that add https support."""
57
58 def __init__(self, server_address, request_hander_class, cert_path):
59 s = open(cert_path).read()
60 x509 = tlslite.api.X509()
61 x509.parse(s)
62 self.cert_chain = tlslite.api.X509CertChain([x509])
63 s = open(cert_path).read()
64 self.private_key = tlslite.api.parsePEMKey(s, private=True)
65
66 self.session_cache = tlslite.api.SessionCache()
67 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
68
69 def handshake(self, tlsConnection):
70 """Creates the SSL connection."""
71 try:
72 tlsConnection.handshakeServer(certChain=self.cert_chain,
73 privateKey=self.private_key,
74 sessionCache=self.session_cache)
75 tlsConnection.ignoreAbruptClose = True
76 return True
77 except tlslite.api.TLSError, error:
78 print "Handshake failure:", str(error)
79 return False
80
81class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
82
83 def __init__(self, request, client_address, socket_server):
wtc@chromium.org743d77b2009-02-11 02:48:15 +000084 self._connect_handlers = [
85 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +000086 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +000087 self.DefaultConnectResponseHandler]
initial.commit94958cf2008-07-26 22:42:52 +000088 self._get_handlers = [
89 self.KillHandler,
90 self.NoCacheMaxAgeTimeHandler,
91 self.NoCacheTimeHandler,
92 self.CacheTimeHandler,
93 self.CacheExpiresHandler,
94 self.CacheProxyRevalidateHandler,
95 self.CachePrivateHandler,
96 self.CachePublicHandler,
97 self.CacheSMaxAgeHandler,
98 self.CacheMustRevalidateHandler,
99 self.CacheMustRevalidateMaxAgeHandler,
100 self.CacheNoStoreHandler,
101 self.CacheNoStoreMaxAgeHandler,
102 self.CacheNoTransformHandler,
103 self.DownloadHandler,
104 self.DownloadFinishHandler,
105 self.EchoHeader,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000106 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000107 self.FileHandler,
108 self.RealFileWithCommonHeaderHandler,
109 self.RealBZ2FileWithCommonHeaderHandler,
110 self.AuthBasicHandler,
111 self.AuthDigestHandler,
112 self.SlowServerHandler,
113 self.ContentTypeHandler,
114 self.ServerRedirectHandler,
115 self.ClientRedirectHandler,
116 self.DefaultResponseHandler]
117 self._post_handlers = [
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000118 self.WriteFile,
initial.commit94958cf2008-07-26 22:42:52 +0000119 self.EchoTitleHandler,
120 self.EchoAllHandler,
121 self.EchoHandler] + self._get_handlers
122
maruel@google.come250a9b2009-03-10 17:39:46 +0000123 self._mime_types = {
124 'gif': 'image/gif',
125 'jpeg' : 'image/jpeg',
126 'jpg' : 'image/jpeg'
127 }
initial.commit94958cf2008-07-26 22:42:52 +0000128 self._default_mime_type = 'text/html'
129
maruel@google.come250a9b2009-03-10 17:39:46 +0000130 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
131 client_address,
132 socket_server)
initial.commit94958cf2008-07-26 22:42:52 +0000133
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000134 def _ShouldHandleRequest(self, handler_name):
135 """Determines if the path can be handled by the handler.
136
137 We consider a handler valid if the path begins with the
138 handler name. It can optionally be followed by "?*", "/*".
139 """
140
141 pattern = re.compile('%s($|\?|/).*' % handler_name)
142 return pattern.match(self.path)
143
initial.commit94958cf2008-07-26 22:42:52 +0000144 def GetMIMETypeFromName(self, file_name):
145 """Returns the mime type for the specified file_name. So far it only looks
146 at the file extension."""
147
148 (shortname, extension) = os.path.splitext(file_name)
149 if len(extension) == 0:
150 # no extension.
151 return self._default_mime_type
152
ericroman@google.comc17ca532009-05-07 03:51:05 +0000153 # extension starts with a dot, so we need to remove it
154 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000155
156 def KillHandler(self):
157 """This request handler kills the server, for use when we're done"
158 with the a particular test."""
159
160 if (self.path.find("kill") < 0):
161 return False
162
163 self.send_response(200)
164 self.send_header('Content-type', 'text/html')
165 self.send_header('Cache-Control', 'max-age=0')
166 self.end_headers()
167 self.wfile.write("Time to die")
168 self.server.stop = True
169
170 return True
171
172 def NoCacheMaxAgeTimeHandler(self):
173 """This request handler yields a page with the title set to the current
174 system time, and no caching requested."""
175
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000176 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000177 return False
178
179 self.send_response(200)
180 self.send_header('Cache-Control', 'max-age=0')
181 self.send_header('Content-type', 'text/html')
182 self.end_headers()
183
maruel@google.come250a9b2009-03-10 17:39:46 +0000184 self.wfile.write('<html><head><title>%s</title></head></html>' %
185 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000186
187 return True
188
189 def NoCacheTimeHandler(self):
190 """This request handler yields a page with the title set to the current
191 system time, and no caching requested."""
192
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000193 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000194 return False
195
196 self.send_response(200)
197 self.send_header('Cache-Control', 'no-cache')
198 self.send_header('Content-type', 'text/html')
199 self.end_headers()
200
maruel@google.come250a9b2009-03-10 17:39:46 +0000201 self.wfile.write('<html><head><title>%s</title></head></html>' %
202 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000203
204 return True
205
206 def CacheTimeHandler(self):
207 """This request handler yields a page with the title set to the current
208 system time, and allows caching for one minute."""
209
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000210 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000211 return False
212
213 self.send_response(200)
214 self.send_header('Cache-Control', 'max-age=60')
215 self.send_header('Content-type', 'text/html')
216 self.end_headers()
217
maruel@google.come250a9b2009-03-10 17:39:46 +0000218 self.wfile.write('<html><head><title>%s</title></head></html>' %
219 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000220
221 return True
222
223 def CacheExpiresHandler(self):
224 """This request handler yields a page with the title set to the current
225 system time, and set the page to expire on 1 Jan 2099."""
226
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000227 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000228 return False
229
230 self.send_response(200)
231 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
232 self.send_header('Content-type', 'text/html')
233 self.end_headers()
234
maruel@google.come250a9b2009-03-10 17:39:46 +0000235 self.wfile.write('<html><head><title>%s</title></head></html>' %
236 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000237
238 return True
239
240 def CacheProxyRevalidateHandler(self):
241 """This request handler yields a page with the title set to the current
242 system time, and allows caching for 60 seconds"""
243
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000244 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000245 return False
246
247 self.send_response(200)
248 self.send_header('Content-type', 'text/html')
249 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
250 self.end_headers()
251
maruel@google.come250a9b2009-03-10 17:39:46 +0000252 self.wfile.write('<html><head><title>%s</title></head></html>' %
253 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000254
255 return True
256
257 def CachePrivateHandler(self):
258 """This request handler yields a page with the title set to the current
259 system time, and allows caching for 5 seconds."""
260
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000261 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000262 return False
263
264 self.send_response(200)
265 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000266 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000267 self.end_headers()
268
maruel@google.come250a9b2009-03-10 17:39:46 +0000269 self.wfile.write('<html><head><title>%s</title></head></html>' %
270 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000271
272 return True
273
274 def CachePublicHandler(self):
275 """This request handler yields a page with the title set to the current
276 system time, and allows caching for 5 seconds."""
277
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000278 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000279 return False
280
281 self.send_response(200)
282 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000283 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000284 self.end_headers()
285
maruel@google.come250a9b2009-03-10 17:39:46 +0000286 self.wfile.write('<html><head><title>%s</title></head></html>' %
287 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000288
289 return True
290
291 def CacheSMaxAgeHandler(self):
292 """This request handler yields a page with the title set to the current
293 system time, and does not allow for caching."""
294
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000295 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000296 return False
297
298 self.send_response(200)
299 self.send_header('Content-type', 'text/html')
300 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
301 self.end_headers()
302
maruel@google.come250a9b2009-03-10 17:39:46 +0000303 self.wfile.write('<html><head><title>%s</title></head></html>' %
304 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000305
306 return True
307
308 def CacheMustRevalidateHandler(self):
309 """This request handler yields a page with the title set to the current
310 system time, and does not allow caching."""
311
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000312 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000313 return False
314
315 self.send_response(200)
316 self.send_header('Content-type', 'text/html')
317 self.send_header('Cache-Control', 'must-revalidate')
318 self.end_headers()
319
maruel@google.come250a9b2009-03-10 17:39:46 +0000320 self.wfile.write('<html><head><title>%s</title></head></html>' %
321 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000322
323 return True
324
325 def CacheMustRevalidateMaxAgeHandler(self):
326 """This request handler yields a page with the title set to the current
327 system time, and does not allow caching event though max-age of 60
328 seconds is specified."""
329
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000330 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000331 return False
332
333 self.send_response(200)
334 self.send_header('Content-type', 'text/html')
335 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
336 self.end_headers()
337
maruel@google.come250a9b2009-03-10 17:39:46 +0000338 self.wfile.write('<html><head><title>%s</title></head></html>' %
339 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000340
341 return True
342
initial.commit94958cf2008-07-26 22:42:52 +0000343 def CacheNoStoreHandler(self):
344 """This request handler yields a page with the title set to the current
345 system time, and does not allow the page to be stored."""
346
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000347 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000348 return False
349
350 self.send_response(200)
351 self.send_header('Content-type', 'text/html')
352 self.send_header('Cache-Control', 'no-store')
353 self.end_headers()
354
maruel@google.come250a9b2009-03-10 17:39:46 +0000355 self.wfile.write('<html><head><title>%s</title></head></html>' %
356 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000357
358 return True
359
360 def CacheNoStoreMaxAgeHandler(self):
361 """This request handler yields a page with the title set to the current
362 system time, and does not allow the page to be stored even though max-age
363 of 60 seconds is specified."""
364
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000365 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000366 return False
367
368 self.send_response(200)
369 self.send_header('Content-type', 'text/html')
370 self.send_header('Cache-Control', 'max-age=60, no-store')
371 self.end_headers()
372
maruel@google.come250a9b2009-03-10 17:39:46 +0000373 self.wfile.write('<html><head><title>%s</title></head></html>' %
374 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000375
376 return True
377
378
379 def CacheNoTransformHandler(self):
380 """This request handler yields a page with the title set to the current
381 system time, and does not allow the content to transformed during
382 user-agent caching"""
383
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000384 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000385 return False
386
387 self.send_response(200)
388 self.send_header('Content-type', 'text/html')
389 self.send_header('Cache-Control', 'no-transform')
390 self.end_headers()
391
maruel@google.come250a9b2009-03-10 17:39:46 +0000392 self.wfile.write('<html><head><title>%s</title></head></html>' %
393 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000394
395 return True
396
397 def EchoHeader(self):
398 """This handler echoes back the value of a specific request header."""
399
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000400 if not self._ShouldHandleRequest("/echoheader"):
initial.commit94958cf2008-07-26 22:42:52 +0000401 return False
402
403 query_char = self.path.find('?')
404 if query_char != -1:
405 header_name = self.path[query_char+1:]
406
407 self.send_response(200)
408 self.send_header('Content-type', 'text/plain')
409 self.send_header('Cache-control', 'max-age=60000')
410 # insert a vary header to properly indicate that the cachability of this
411 # request is subject to value of the request header being echoed.
412 if len(header_name) > 0:
413 self.send_header('Vary', header_name)
414 self.end_headers()
415
416 if len(header_name) > 0:
417 self.wfile.write(self.headers.getheader(header_name))
418
419 return True
420
421 def EchoHandler(self):
422 """This handler just echoes back the payload of the request, for testing
423 form submission."""
424
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000425 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000426 return False
427
428 self.send_response(200)
429 self.send_header('Content-type', 'text/html')
430 self.end_headers()
431 length = int(self.headers.getheader('content-length'))
432 request = self.rfile.read(length)
433 self.wfile.write(request)
434 return True
435
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000436 def WriteFile(self):
437 """This is handler dumps the content of POST request to a disk file into
438 the data_dir/dump. Sub-directories are not supported."""
maruel@chromium.org756cf982009-03-05 12:46:38 +0000439
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000440 prefix='/writefile/'
441 if not self.path.startswith(prefix):
442 return False
maruel@chromium.org756cf982009-03-05 12:46:38 +0000443
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000444 file_name = self.path[len(prefix):]
445
446 # do not allow fancy chars in file name
447 re.sub('[^a-zA-Z0-9_.-]+', '', file_name)
448 if len(file_name) and file_name[0] != '.':
449 path = os.path.join(self.server.data_dir, 'dump', file_name);
450 length = int(self.headers.getheader('content-length'))
451 request = self.rfile.read(length)
452 f = open(path, "wb")
453 f.write(request);
454 f.close()
maruel@chromium.org756cf982009-03-05 12:46:38 +0000455
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000456 self.send_response(200)
457 self.send_header('Content-type', 'text/html')
458 self.end_headers()
459 self.wfile.write('<html>%s</html>' % file_name)
460 return True
maruel@chromium.org756cf982009-03-05 12:46:38 +0000461
initial.commit94958cf2008-07-26 22:42:52 +0000462 def EchoTitleHandler(self):
463 """This handler is like Echo, but sets the page title to the request."""
464
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000465 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000466 return False
467
468 self.send_response(200)
469 self.send_header('Content-type', 'text/html')
470 self.end_headers()
471 length = int(self.headers.getheader('content-length'))
472 request = self.rfile.read(length)
473 self.wfile.write('<html><head><title>')
474 self.wfile.write(request)
475 self.wfile.write('</title></head></html>')
476 return True
477
478 def EchoAllHandler(self):
479 """This handler yields a (more) human-readable page listing information
480 about the request header & contents."""
481
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000482 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000483 return False
484
485 self.send_response(200)
486 self.send_header('Content-type', 'text/html')
487 self.end_headers()
488 self.wfile.write('<html><head><style>'
489 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
490 '</style></head><body>'
491 '<div style="float: right">'
492 '<a href="http://localhost:8888/echo">back to referring page</a></div>'
493 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000494
ericroman@google.coma47622b2008-11-15 04:36:51 +0000495 if self.command == 'POST':
496 length = int(self.headers.getheader('content-length'))
497 qs = self.rfile.read(length)
498 params = cgi.parse_qs(qs, keep_blank_values=1)
499
500 for param in params:
501 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000502
503 self.wfile.write('</pre>')
504
505 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
506
507 self.wfile.write('</body></html>')
508 return True
509
510 def DownloadHandler(self):
511 """This handler sends a downloadable file with or without reporting
512 the size (6K)."""
513
514 if self.path.startswith("/download-unknown-size"):
515 send_length = False
516 elif self.path.startswith("/download-known-size"):
517 send_length = True
518 else:
519 return False
520
521 #
522 # The test which uses this functionality is attempting to send
523 # small chunks of data to the client. Use a fairly large buffer
524 # so that we'll fill chrome's IO buffer enough to force it to
525 # actually write the data.
526 # See also the comments in the client-side of this test in
527 # download_uitest.cc
528 #
529 size_chunk1 = 35*1024
530 size_chunk2 = 10*1024
531
532 self.send_response(200)
533 self.send_header('Content-type', 'application/octet-stream')
534 self.send_header('Cache-Control', 'max-age=0')
535 if send_length:
536 self.send_header('Content-Length', size_chunk1 + size_chunk2)
537 self.end_headers()
538
539 # First chunk of data:
540 self.wfile.write("*" * size_chunk1)
541 self.wfile.flush()
542
543 # handle requests until one of them clears this flag.
544 self.server.waitForDownload = True
545 while self.server.waitForDownload:
546 self.server.handle_request()
547
548 # Second chunk of data:
549 self.wfile.write("*" * size_chunk2)
550 return True
551
552 def DownloadFinishHandler(self):
553 """This handler just tells the server to finish the current download."""
554
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000555 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000556 return False
557
558 self.server.waitForDownload = False
559 self.send_response(200)
560 self.send_header('Content-type', 'text/html')
561 self.send_header('Cache-Control', 'max-age=0')
562 self.end_headers()
563 return True
564
565 def FileHandler(self):
566 """This handler sends the contents of the requested file. Wow, it's like
567 a real webserver!"""
568
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000569 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000570 if not self.path.startswith(prefix):
571 return False
572
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000573 # Consume a request body if present.
574 if self.command == 'POST':
575 self.rfile.read(int(self.headers.getheader('content-length')))
576
initial.commit94958cf2008-07-26 22:42:52 +0000577 file = self.path[len(prefix):]
578 entries = file.split('/');
579 path = os.path.join(self.server.data_dir, *entries)
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000580 if os.path.isdir(path):
581 path = os.path.join(path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000582
583 if not os.path.isfile(path):
584 print "File not found " + file + " full path:" + path
585 self.send_error(404)
586 return True
587
588 f = open(path, "rb")
589 data = f.read()
590 f.close()
591
592 # If file.mock-http-headers exists, it contains the headers we
593 # should send. Read them in and parse them.
594 headers_path = path + '.mock-http-headers'
595 if os.path.isfile(headers_path):
596 f = open(headers_path, "r")
597
598 # "HTTP/1.1 200 OK"
599 response = f.readline()
600 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
601 self.send_response(int(status_code))
602
603 for line in f:
604 # "name: value"
605 name, value = re.findall('(\S+):\s*(.*)', line)[0]
606 self.send_header(name, value)
607 f.close()
608 else:
609 # Could be more generic once we support mime-type sniffing, but for
610 # now we need to set it explicitly.
611 self.send_response(200)
612 self.send_header('Content-type', self.GetMIMETypeFromName(file))
613 self.send_header('Content-Length', len(data))
614 self.end_headers()
615
616 self.wfile.write(data)
617
618 return True
619
620 def RealFileWithCommonHeaderHandler(self):
621 """This handler sends the contents of the requested file without the pseudo
622 http head!"""
623
624 prefix='/realfiles/'
625 if not self.path.startswith(prefix):
626 return False
627
628 file = self.path[len(prefix):]
629 path = os.path.join(self.server.data_dir, file)
630
631 try:
632 f = open(path, "rb")
633 data = f.read()
634 f.close()
635
636 # just simply set the MIME as octal stream
637 self.send_response(200)
638 self.send_header('Content-type', 'application/octet-stream')
639 self.end_headers()
640
641 self.wfile.write(data)
642 except:
643 self.send_error(404)
644
645 return True
646
647 def RealBZ2FileWithCommonHeaderHandler(self):
648 """This handler sends the bzip2 contents of the requested file with
649 corresponding Content-Encoding field in http head!"""
650
651 prefix='/realbz2files/'
652 if not self.path.startswith(prefix):
653 return False
654
655 parts = self.path.split('?')
656 file = parts[0][len(prefix):]
657 path = os.path.join(self.server.data_dir, file) + '.bz2'
658
659 if len(parts) > 1:
660 options = parts[1]
661 else:
662 options = ''
663
664 try:
665 self.send_response(200)
666 accept_encoding = self.headers.get("Accept-Encoding")
667 if accept_encoding.find("bzip2") != -1:
668 f = open(path, "rb")
669 data = f.read()
670 f.close()
671 self.send_header('Content-Encoding', 'bzip2')
672 self.send_header('Content-type', 'application/x-bzip2')
673 self.end_headers()
674 if options == 'incremental-header':
675 self.wfile.write(data[:1])
676 self.wfile.flush()
677 time.sleep(1.0)
678 self.wfile.write(data[1:])
679 else:
680 self.wfile.write(data)
681 else:
682 """client do not support bzip2 format, send pseudo content
683 """
684 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
685 self.end_headers()
686 self.wfile.write("you do not support bzip2 encoding")
687 except:
688 self.send_error(404)
689
690 return True
691
692 def AuthBasicHandler(self):
693 """This handler tests 'Basic' authentication. It just sends a page with
694 title 'user/pass' if you succeed."""
695
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000696 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000697 return False
698
699 username = userpass = password = b64str = ""
700
ericroman@google.com239b4d82009-03-27 04:00:22 +0000701 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
702
initial.commit94958cf2008-07-26 22:42:52 +0000703 auth = self.headers.getheader('authorization')
704 try:
705 if not auth:
706 raise Exception('no auth')
707 b64str = re.findall(r'Basic (\S+)', auth)[0]
708 userpass = base64.b64decode(b64str)
709 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
710 if password != 'secret':
711 raise Exception('wrong password')
712 except Exception, e:
713 # Authentication failed.
714 self.send_response(401)
715 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
716 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000717 if set_cookie_if_challenged:
718 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000719 self.end_headers()
720 self.wfile.write('<html><head>')
721 self.wfile.write('<title>Denied: %s</title>' % e)
722 self.wfile.write('</head><body>')
723 self.wfile.write('auth=%s<p>' % auth)
724 self.wfile.write('b64str=%s<p>' % b64str)
725 self.wfile.write('username: %s<p>' % username)
726 self.wfile.write('userpass: %s<p>' % userpass)
727 self.wfile.write('password: %s<p>' % password)
728 self.wfile.write('You sent:<br>%s<p>' % self.headers)
729 self.wfile.write('</body></html>')
730 return True
731
732 # Authentication successful. (Return a cachable response to allow for
733 # testing cached pages that require authentication.)
734 if_none_match = self.headers.getheader('if-none-match')
735 if if_none_match == "abc":
736 self.send_response(304)
737 self.end_headers()
738 else:
739 self.send_response(200)
740 self.send_header('Content-type', 'text/html')
741 self.send_header('Cache-control', 'max-age=60000')
742 self.send_header('Etag', 'abc')
743 self.end_headers()
744 self.wfile.write('<html><head>')
745 self.wfile.write('<title>%s/%s</title>' % (username, password))
746 self.wfile.write('</head><body>')
747 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000748 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000749 self.wfile.write('</body></html>')
750
751 return True
752
753 def AuthDigestHandler(self):
754 """This handler tests 'Digest' authentication. It just sends a page with
755 title 'user/pass' if you succeed."""
756
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000757 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000758 return False
759
760 # Periodically generate a new nonce. Technically we should incorporate
761 # the request URL into this, but we don't care for testing.
762 nonce_life = 10
763 stale = False
maruel@google.come250a9b2009-03-10 17:39:46 +0000764 if (not self.server.nonce or
765 (time.time() - self.server.nonce_time > nonce_life)):
initial.commit94958cf2008-07-26 22:42:52 +0000766 if self.server.nonce:
767 stale = True
768 self.server.nonce_time = time.time()
769 self.server.nonce = \
maruel@google.come250a9b2009-03-10 17:39:46 +0000770 _new_md5(time.ctime(self.server.nonce_time) +
771 'privatekey').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000772
773 nonce = self.server.nonce
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000774 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000775 password = 'secret'
776 realm = 'testrealm'
777
778 auth = self.headers.getheader('authorization')
779 pairs = {}
780 try:
781 if not auth:
782 raise Exception('no auth')
783 if not auth.startswith('Digest'):
784 raise Exception('not digest')
785 # Pull out all the name="value" pairs as a dictionary.
786 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
787
788 # Make sure it's all valid.
789 if pairs['nonce'] != nonce:
790 raise Exception('wrong nonce')
791 if pairs['opaque'] != opaque:
792 raise Exception('wrong opaque')
793
794 # Check the 'response' value and make sure it matches our magic hash.
795 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000796 hash_a1 = _new_md5(
797 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000798 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000799 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000800 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000801 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
802 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000803 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000804
805 if pairs['response'] != response:
806 raise Exception('wrong password')
807 except Exception, e:
808 # Authentication failed.
809 self.send_response(401)
810 hdr = ('Digest '
811 'realm="%s", '
812 'domain="/", '
813 'qop="auth", '
814 'algorithm=MD5, '
815 'nonce="%s", '
816 'opaque="%s"') % (realm, nonce, opaque)
817 if stale:
818 hdr += ', stale="TRUE"'
819 self.send_header('WWW-Authenticate', hdr)
820 self.send_header('Content-type', 'text/html')
821 self.end_headers()
822 self.wfile.write('<html><head>')
823 self.wfile.write('<title>Denied: %s</title>' % e)
824 self.wfile.write('</head><body>')
825 self.wfile.write('auth=%s<p>' % auth)
826 self.wfile.write('pairs=%s<p>' % pairs)
827 self.wfile.write('You sent:<br>%s<p>' % self.headers)
828 self.wfile.write('We are replying:<br>%s<p>' % hdr)
829 self.wfile.write('</body></html>')
830 return True
831
832 # Authentication successful.
833 self.send_response(200)
834 self.send_header('Content-type', 'text/html')
835 self.end_headers()
836 self.wfile.write('<html><head>')
837 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
838 self.wfile.write('</head><body>')
839 self.wfile.write('auth=%s<p>' % auth)
840 self.wfile.write('pairs=%s<p>' % pairs)
841 self.wfile.write('</body></html>')
842
843 return True
844
845 def SlowServerHandler(self):
846 """Wait for the user suggested time before responding. The syntax is
847 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000848 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000849 return False
850 query_char = self.path.find('?')
851 wait_sec = 1.0
852 if query_char >= 0:
853 try:
854 wait_sec = int(self.path[query_char + 1:])
855 except ValueError:
856 pass
857 time.sleep(wait_sec)
858 self.send_response(200)
859 self.send_header('Content-type', 'text/plain')
860 self.end_headers()
861 self.wfile.write("waited %d seconds" % wait_sec)
862 return True
863
864 def ContentTypeHandler(self):
865 """Returns a string of html with the given content type. E.g.,
866 /contenttype?text/css returns an html file with the Content-Type
867 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000868 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +0000869 return False
870 query_char = self.path.find('?')
871 content_type = self.path[query_char + 1:].strip()
872 if not content_type:
873 content_type = 'text/html'
874 self.send_response(200)
875 self.send_header('Content-Type', content_type)
876 self.end_headers()
877 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
878 return True
879
880 def ServerRedirectHandler(self):
881 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000882 '/server-redirect?http://foo.bar/asdf' to redirect to
883 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000884
885 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000886 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000887 return False
888
889 query_char = self.path.find('?')
890 if query_char < 0 or len(self.path) <= query_char + 1:
891 self.sendRedirectHelp(test_name)
892 return True
893 dest = self.path[query_char + 1:]
894
895 self.send_response(301) # moved permanently
896 self.send_header('Location', dest)
897 self.send_header('Content-type', 'text/html')
898 self.end_headers()
899 self.wfile.write('<html><head>')
900 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
901
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000902 return True
initial.commit94958cf2008-07-26 22:42:52 +0000903
904 def ClientRedirectHandler(self):
905 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000906 '/client-redirect?http://foo.bar/asdf' to redirect to
907 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000908
909 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000910 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000911 return False
912
913 query_char = self.path.find('?');
914 if query_char < 0 or len(self.path) <= query_char + 1:
915 self.sendRedirectHelp(test_name)
916 return True
917 dest = self.path[query_char + 1:]
918
919 self.send_response(200)
920 self.send_header('Content-type', 'text/html')
921 self.end_headers()
922 self.wfile.write('<html><head>')
923 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
924 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
925
926 return True
927
928 def DefaultResponseHandler(self):
929 """This is the catch-all response handler for requests that aren't handled
930 by one of the special handlers above.
931 Note that we specify the content-length as without it the https connection
932 is not closed properly (and the browser keeps expecting data)."""
933
934 contents = "Default response given for path: " + self.path
935 self.send_response(200)
936 self.send_header('Content-type', 'text/html')
937 self.send_header("Content-Length", len(contents))
938 self.end_headers()
939 self.wfile.write(contents)
940 return True
941
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000942 def RedirectConnectHandler(self):
943 """Sends a redirect to the CONNECT request for www.redirect.com. This
944 response is not specified by the RFC, so the browser should not follow
945 the redirect."""
946
947 if (self.path.find("www.redirect.com") < 0):
948 return False
949
950 dest = "http://www.destination.com/foo.js"
951
952 self.send_response(302) # moved temporarily
953 self.send_header('Location', dest)
954 self.send_header('Connection', 'close')
955 self.end_headers()
956 return True
957
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000958 def ServerAuthConnectHandler(self):
959 """Sends a 401 to the CONNECT request for www.server-auth.com. This
960 response doesn't make sense because the proxy server cannot request
961 server authentication."""
962
963 if (self.path.find("www.server-auth.com") < 0):
964 return False
965
966 challenge = 'Basic realm="WallyWorld"'
967
968 self.send_response(401) # unauthorized
969 self.send_header('WWW-Authenticate', challenge)
970 self.send_header('Connection', 'close')
971 self.end_headers()
972 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000973
974 def DefaultConnectResponseHandler(self):
975 """This is the catch-all response handler for CONNECT requests that aren't
976 handled by one of the special handlers above. Real Web servers respond
977 with 400 to CONNECT requests."""
978
979 contents = "Your client has issued a malformed or illegal request."
980 self.send_response(400) # bad request
981 self.send_header('Content-type', 'text/html')
982 self.send_header("Content-Length", len(contents))
983 self.end_headers()
984 self.wfile.write(contents)
985 return True
986
987 def do_CONNECT(self):
988 for handler in self._connect_handlers:
989 if handler():
990 return
991
initial.commit94958cf2008-07-26 22:42:52 +0000992 def do_GET(self):
993 for handler in self._get_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000994 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000995 return
996
997 def do_POST(self):
998 for handler in self._post_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000999 if handler():
initial.commit94958cf2008-07-26 22:42:52 +00001000 return
1001
1002 # called by the redirect handling function when there is no parameter
1003 def sendRedirectHelp(self, redirect_name):
1004 self.send_response(200)
1005 self.send_header('Content-type', 'text/html')
1006 self.end_headers()
1007 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1008 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1009 self.wfile.write('</body></html>')
1010
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001011def MakeDumpDir(data_dir):
1012 """Create directory named 'dump' where uploaded data via HTTP POST request
1013 will be stored. If the directory already exists all files and subdirectories
1014 will be deleted."""
1015 dump_dir = os.path.join(data_dir, 'dump');
1016 if os.path.isdir(dump_dir):
1017 shutil.rmtree(dump_dir)
1018 os.mkdir(dump_dir)
1019
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001020def MakeDataDir():
1021 if options.data_dir:
1022 if not os.path.isdir(options.data_dir):
1023 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1024 return None
1025 my_data_dir = options.data_dir
1026 else:
1027 # Create the default path to our data dir, relative to the exe dir.
1028 my_data_dir = os.path.dirname(sys.argv[0])
1029 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
1030 "test", "data")
1031
1032 #TODO(ibrar): Must use Find* funtion defined in google\tools
1033 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1034
1035 return my_data_dir
1036
initial.commit94958cf2008-07-26 22:42:52 +00001037def main(options, args):
1038 # redirect output to a log file so it doesn't spam the unit test output
1039 logfile = open('testserver.log', 'w')
1040 sys.stderr = sys.stdout = logfile
1041
1042 port = options.port
1043
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001044 if options.server_type == SERVER_HTTP:
1045 if options.cert:
1046 # let's make sure the cert file exists.
1047 if not os.path.isfile(options.cert):
1048 print 'specified cert file not found: ' + options.cert + ' exiting...'
1049 return
1050 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
1051 print 'HTTPS server started on port %d...' % port
1052 else:
1053 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
1054 print 'HTTP server started on port %d...' % port
erikkay@google.com70397b62008-12-30 21:49:21 +00001055
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001056 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001057 server.file_root_url = options.file_root_url
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001058 MakeDumpDir(server.data_dir)
maruel@chromium.org756cf982009-03-05 12:46:38 +00001059
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001060 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001061 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001062 my_data_dir = MakeDataDir()
1063
1064 def line_logger(msg):
1065 if (msg.find("kill") >= 0):
1066 server.stop = True
1067 print 'shutting down server'
1068 sys.exit(0)
1069
1070 # Instantiate a dummy authorizer for managing 'virtual' users
1071 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1072
1073 # Define a new user having full r/w permissions and a read-only
1074 # anonymous user
1075 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1076
1077 authorizer.add_anonymous(my_data_dir)
1078
1079 # Instantiate FTP handler class
1080 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1081 ftp_handler.authorizer = authorizer
1082 pyftpdlib.ftpserver.logline = line_logger
1083
1084 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001085 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1086 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001087
1088 # Instantiate FTP server class and listen to 127.0.0.1:port
1089 address = ('127.0.0.1', port)
1090 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
1091 print 'FTP server started on port %d...' % port
initial.commit94958cf2008-07-26 22:42:52 +00001092
1093 try:
1094 server.serve_forever()
1095 except KeyboardInterrupt:
1096 print 'shutting down server'
1097 server.stop = True
1098
1099if __name__ == '__main__':
1100 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001101 option_parser.add_option("-f", '--ftp', action='store_const',
1102 const=SERVER_FTP, default=SERVER_HTTP,
1103 dest='server_type',
1104 help='FTP or HTTP server default HTTP')
initial.commit94958cf2008-07-26 22:42:52 +00001105 option_parser.add_option('', '--port', default='8888', type='int',
1106 help='Port used by the server')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001107 option_parser.add_option('', '--data-dir', dest='data_dir',
initial.commit94958cf2008-07-26 22:42:52 +00001108 help='Directory from which to read the files')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001109 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001110 help='Specify that https should be used, specify '
1111 'the path to the cert containing the private key '
1112 'the server should use')
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001113 option_parser.add_option('', '--file-root-url', default='/files/',
1114 help='Specify a root URL for files served.')
initial.commit94958cf2008-07-26 22:42:52 +00001115 options, args = option_parser.parse_args()
1116
1117 sys.exit(main(options, args))