blob: 9ab77a8a723dad506813c479f90dac6b8283294f [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
153 return self._mime_types.get(extension, self._default_mime_type)
154
155 def KillHandler(self):
156 """This request handler kills the server, for use when we're done"
157 with the a particular test."""
158
159 if (self.path.find("kill") < 0):
160 return False
161
162 self.send_response(200)
163 self.send_header('Content-type', 'text/html')
164 self.send_header('Cache-Control', 'max-age=0')
165 self.end_headers()
166 self.wfile.write("Time to die")
167 self.server.stop = True
168
169 return True
170
171 def NoCacheMaxAgeTimeHandler(self):
172 """This request handler yields a page with the title set to the current
173 system time, and no caching requested."""
174
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000175 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000176 return False
177
178 self.send_response(200)
179 self.send_header('Cache-Control', 'max-age=0')
180 self.send_header('Content-type', 'text/html')
181 self.end_headers()
182
maruel@google.come250a9b2009-03-10 17:39:46 +0000183 self.wfile.write('<html><head><title>%s</title></head></html>' %
184 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000185
186 return True
187
188 def NoCacheTimeHandler(self):
189 """This request handler yields a page with the title set to the current
190 system time, and no caching requested."""
191
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000192 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000193 return False
194
195 self.send_response(200)
196 self.send_header('Cache-Control', 'no-cache')
197 self.send_header('Content-type', 'text/html')
198 self.end_headers()
199
maruel@google.come250a9b2009-03-10 17:39:46 +0000200 self.wfile.write('<html><head><title>%s</title></head></html>' %
201 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000202
203 return True
204
205 def CacheTimeHandler(self):
206 """This request handler yields a page with the title set to the current
207 system time, and allows caching for one minute."""
208
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000209 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000210 return False
211
212 self.send_response(200)
213 self.send_header('Cache-Control', 'max-age=60')
214 self.send_header('Content-type', 'text/html')
215 self.end_headers()
216
maruel@google.come250a9b2009-03-10 17:39:46 +0000217 self.wfile.write('<html><head><title>%s</title></head></html>' %
218 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000219
220 return True
221
222 def CacheExpiresHandler(self):
223 """This request handler yields a page with the title set to the current
224 system time, and set the page to expire on 1 Jan 2099."""
225
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000226 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000227 return False
228
229 self.send_response(200)
230 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
231 self.send_header('Content-type', 'text/html')
232 self.end_headers()
233
maruel@google.come250a9b2009-03-10 17:39:46 +0000234 self.wfile.write('<html><head><title>%s</title></head></html>' %
235 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000236
237 return True
238
239 def CacheProxyRevalidateHandler(self):
240 """This request handler yields a page with the title set to the current
241 system time, and allows caching for 60 seconds"""
242
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000243 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000244 return False
245
246 self.send_response(200)
247 self.send_header('Content-type', 'text/html')
248 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
249 self.end_headers()
250
maruel@google.come250a9b2009-03-10 17:39:46 +0000251 self.wfile.write('<html><head><title>%s</title></head></html>' %
252 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000253
254 return True
255
256 def CachePrivateHandler(self):
257 """This request handler yields a page with the title set to the current
258 system time, and allows caching for 5 seconds."""
259
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000260 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000261 return False
262
263 self.send_response(200)
264 self.send_header('Content-type', 'text/html')
265 self.send_header('Cache-Control', 'max-age=5, private')
266 self.end_headers()
267
maruel@google.come250a9b2009-03-10 17:39:46 +0000268 self.wfile.write('<html><head><title>%s</title></head></html>' %
269 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000270
271 return True
272
273 def CachePublicHandler(self):
274 """This request handler yields a page with the title set to the current
275 system time, and allows caching for 5 seconds."""
276
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000277 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000278 return False
279
280 self.send_response(200)
281 self.send_header('Content-type', 'text/html')
282 self.send_header('Cache-Control', 'max-age=5, public')
283 self.end_headers()
284
maruel@google.come250a9b2009-03-10 17:39:46 +0000285 self.wfile.write('<html><head><title>%s</title></head></html>' %
286 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000287
288 return True
289
290 def CacheSMaxAgeHandler(self):
291 """This request handler yields a page with the title set to the current
292 system time, and does not allow for caching."""
293
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000294 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000295 return False
296
297 self.send_response(200)
298 self.send_header('Content-type', 'text/html')
299 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
300 self.end_headers()
301
maruel@google.come250a9b2009-03-10 17:39:46 +0000302 self.wfile.write('<html><head><title>%s</title></head></html>' %
303 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000304
305 return True
306
307 def CacheMustRevalidateHandler(self):
308 """This request handler yields a page with the title set to the current
309 system time, and does not allow caching."""
310
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000311 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000312 return False
313
314 self.send_response(200)
315 self.send_header('Content-type', 'text/html')
316 self.send_header('Cache-Control', 'must-revalidate')
317 self.end_headers()
318
maruel@google.come250a9b2009-03-10 17:39:46 +0000319 self.wfile.write('<html><head><title>%s</title></head></html>' %
320 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000321
322 return True
323
324 def CacheMustRevalidateMaxAgeHandler(self):
325 """This request handler yields a page with the title set to the current
326 system time, and does not allow caching event though max-age of 60
327 seconds is specified."""
328
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000329 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000330 return False
331
332 self.send_response(200)
333 self.send_header('Content-type', 'text/html')
334 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
335 self.end_headers()
336
maruel@google.come250a9b2009-03-10 17:39:46 +0000337 self.wfile.write('<html><head><title>%s</title></head></html>' %
338 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000339
340 return True
341
initial.commit94958cf2008-07-26 22:42:52 +0000342 def CacheNoStoreHandler(self):
343 """This request handler yields a page with the title set to the current
344 system time, and does not allow the page to be stored."""
345
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000346 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000347 return False
348
349 self.send_response(200)
350 self.send_header('Content-type', 'text/html')
351 self.send_header('Cache-Control', 'no-store')
352 self.end_headers()
353
maruel@google.come250a9b2009-03-10 17:39:46 +0000354 self.wfile.write('<html><head><title>%s</title></head></html>' %
355 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000356
357 return True
358
359 def CacheNoStoreMaxAgeHandler(self):
360 """This request handler yields a page with the title set to the current
361 system time, and does not allow the page to be stored even though max-age
362 of 60 seconds is specified."""
363
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000364 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000365 return False
366
367 self.send_response(200)
368 self.send_header('Content-type', 'text/html')
369 self.send_header('Cache-Control', 'max-age=60, no-store')
370 self.end_headers()
371
maruel@google.come250a9b2009-03-10 17:39:46 +0000372 self.wfile.write('<html><head><title>%s</title></head></html>' %
373 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000374
375 return True
376
377
378 def CacheNoTransformHandler(self):
379 """This request handler yields a page with the title set to the current
380 system time, and does not allow the content to transformed during
381 user-agent caching"""
382
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000383 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000384 return False
385
386 self.send_response(200)
387 self.send_header('Content-type', 'text/html')
388 self.send_header('Cache-Control', 'no-transform')
389 self.end_headers()
390
maruel@google.come250a9b2009-03-10 17:39:46 +0000391 self.wfile.write('<html><head><title>%s</title></head></html>' %
392 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000393
394 return True
395
396 def EchoHeader(self):
397 """This handler echoes back the value of a specific request header."""
398
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000399 if not self._ShouldHandleRequest("/echoheader"):
initial.commit94958cf2008-07-26 22:42:52 +0000400 return False
401
402 query_char = self.path.find('?')
403 if query_char != -1:
404 header_name = self.path[query_char+1:]
405
406 self.send_response(200)
407 self.send_header('Content-type', 'text/plain')
408 self.send_header('Cache-control', 'max-age=60000')
409 # insert a vary header to properly indicate that the cachability of this
410 # request is subject to value of the request header being echoed.
411 if len(header_name) > 0:
412 self.send_header('Vary', header_name)
413 self.end_headers()
414
415 if len(header_name) > 0:
416 self.wfile.write(self.headers.getheader(header_name))
417
418 return True
419
420 def EchoHandler(self):
421 """This handler just echoes back the payload of the request, for testing
422 form submission."""
423
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000424 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000425 return False
426
427 self.send_response(200)
428 self.send_header('Content-type', 'text/html')
429 self.end_headers()
430 length = int(self.headers.getheader('content-length'))
431 request = self.rfile.read(length)
432 self.wfile.write(request)
433 return True
434
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000435 def WriteFile(self):
436 """This is handler dumps the content of POST request to a disk file into
437 the data_dir/dump. Sub-directories are not supported."""
maruel@chromium.org756cf982009-03-05 12:46:38 +0000438
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000439 prefix='/writefile/'
440 if not self.path.startswith(prefix):
441 return False
maruel@chromium.org756cf982009-03-05 12:46:38 +0000442
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000443 file_name = self.path[len(prefix):]
444
445 # do not allow fancy chars in file name
446 re.sub('[^a-zA-Z0-9_.-]+', '', file_name)
447 if len(file_name) and file_name[0] != '.':
448 path = os.path.join(self.server.data_dir, 'dump', file_name);
449 length = int(self.headers.getheader('content-length'))
450 request = self.rfile.read(length)
451 f = open(path, "wb")
452 f.write(request);
453 f.close()
maruel@chromium.org756cf982009-03-05 12:46:38 +0000454
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000455 self.send_response(200)
456 self.send_header('Content-type', 'text/html')
457 self.end_headers()
458 self.wfile.write('<html>%s</html>' % file_name)
459 return True
maruel@chromium.org756cf982009-03-05 12:46:38 +0000460
initial.commit94958cf2008-07-26 22:42:52 +0000461 def EchoTitleHandler(self):
462 """This handler is like Echo, but sets the page title to the request."""
463
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000464 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000465 return False
466
467 self.send_response(200)
468 self.send_header('Content-type', 'text/html')
469 self.end_headers()
470 length = int(self.headers.getheader('content-length'))
471 request = self.rfile.read(length)
472 self.wfile.write('<html><head><title>')
473 self.wfile.write(request)
474 self.wfile.write('</title></head></html>')
475 return True
476
477 def EchoAllHandler(self):
478 """This handler yields a (more) human-readable page listing information
479 about the request header & contents."""
480
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000481 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000482 return False
483
484 self.send_response(200)
485 self.send_header('Content-type', 'text/html')
486 self.end_headers()
487 self.wfile.write('<html><head><style>'
488 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
489 '</style></head><body>'
490 '<div style="float: right">'
491 '<a href="http://localhost:8888/echo">back to referring page</a></div>'
492 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000493
ericroman@google.coma47622b2008-11-15 04:36:51 +0000494 if self.command == 'POST':
495 length = int(self.headers.getheader('content-length'))
496 qs = self.rfile.read(length)
497 params = cgi.parse_qs(qs, keep_blank_values=1)
498
499 for param in params:
500 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000501
502 self.wfile.write('</pre>')
503
504 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
505
506 self.wfile.write('</body></html>')
507 return True
508
509 def DownloadHandler(self):
510 """This handler sends a downloadable file with or without reporting
511 the size (6K)."""
512
513 if self.path.startswith("/download-unknown-size"):
514 send_length = False
515 elif self.path.startswith("/download-known-size"):
516 send_length = True
517 else:
518 return False
519
520 #
521 # The test which uses this functionality is attempting to send
522 # small chunks of data to the client. Use a fairly large buffer
523 # so that we'll fill chrome's IO buffer enough to force it to
524 # actually write the data.
525 # See also the comments in the client-side of this test in
526 # download_uitest.cc
527 #
528 size_chunk1 = 35*1024
529 size_chunk2 = 10*1024
530
531 self.send_response(200)
532 self.send_header('Content-type', 'application/octet-stream')
533 self.send_header('Cache-Control', 'max-age=0')
534 if send_length:
535 self.send_header('Content-Length', size_chunk1 + size_chunk2)
536 self.end_headers()
537
538 # First chunk of data:
539 self.wfile.write("*" * size_chunk1)
540 self.wfile.flush()
541
542 # handle requests until one of them clears this flag.
543 self.server.waitForDownload = True
544 while self.server.waitForDownload:
545 self.server.handle_request()
546
547 # Second chunk of data:
548 self.wfile.write("*" * size_chunk2)
549 return True
550
551 def DownloadFinishHandler(self):
552 """This handler just tells the server to finish the current download."""
553
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000554 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000555 return False
556
557 self.server.waitForDownload = False
558 self.send_response(200)
559 self.send_header('Content-type', 'text/html')
560 self.send_header('Cache-Control', 'max-age=0')
561 self.end_headers()
562 return True
563
564 def FileHandler(self):
565 """This handler sends the contents of the requested file. Wow, it's like
566 a real webserver!"""
567
ben@chromium.org260c0172009-04-09 19:10:41 +0000568 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000569 if not self.path.startswith(prefix):
570 return False
571
572 file = self.path[len(prefix):]
573 entries = file.split('/');
574 path = os.path.join(self.server.data_dir, *entries)
ben@chromium.org260c0172009-04-09 19:10:41 +0000575 if os.path.isdir(path):
576 path = os.path.join(path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000577
578 if not os.path.isfile(path):
579 print "File not found " + file + " full path:" + path
580 self.send_error(404)
581 return True
582
583 f = open(path, "rb")
584 data = f.read()
585 f.close()
586
587 # If file.mock-http-headers exists, it contains the headers we
588 # should send. Read them in and parse them.
589 headers_path = path + '.mock-http-headers'
590 if os.path.isfile(headers_path):
591 f = open(headers_path, "r")
592
593 # "HTTP/1.1 200 OK"
594 response = f.readline()
595 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
596 self.send_response(int(status_code))
597
598 for line in f:
599 # "name: value"
600 name, value = re.findall('(\S+):\s*(.*)', line)[0]
601 self.send_header(name, value)
602 f.close()
603 else:
604 # Could be more generic once we support mime-type sniffing, but for
605 # now we need to set it explicitly.
606 self.send_response(200)
607 self.send_header('Content-type', self.GetMIMETypeFromName(file))
608 self.send_header('Content-Length', len(data))
609 self.end_headers()
610
611 self.wfile.write(data)
612
613 return True
614
615 def RealFileWithCommonHeaderHandler(self):
616 """This handler sends the contents of the requested file without the pseudo
617 http head!"""
618
619 prefix='/realfiles/'
620 if not self.path.startswith(prefix):
621 return False
622
623 file = self.path[len(prefix):]
624 path = os.path.join(self.server.data_dir, file)
625
626 try:
627 f = open(path, "rb")
628 data = f.read()
629 f.close()
630
631 # just simply set the MIME as octal stream
632 self.send_response(200)
633 self.send_header('Content-type', 'application/octet-stream')
634 self.end_headers()
635
636 self.wfile.write(data)
637 except:
638 self.send_error(404)
639
640 return True
641
642 def RealBZ2FileWithCommonHeaderHandler(self):
643 """This handler sends the bzip2 contents of the requested file with
644 corresponding Content-Encoding field in http head!"""
645
646 prefix='/realbz2files/'
647 if not self.path.startswith(prefix):
648 return False
649
650 parts = self.path.split('?')
651 file = parts[0][len(prefix):]
652 path = os.path.join(self.server.data_dir, file) + '.bz2'
653
654 if len(parts) > 1:
655 options = parts[1]
656 else:
657 options = ''
658
659 try:
660 self.send_response(200)
661 accept_encoding = self.headers.get("Accept-Encoding")
662 if accept_encoding.find("bzip2") != -1:
663 f = open(path, "rb")
664 data = f.read()
665 f.close()
666 self.send_header('Content-Encoding', 'bzip2')
667 self.send_header('Content-type', 'application/x-bzip2')
668 self.end_headers()
669 if options == 'incremental-header':
670 self.wfile.write(data[:1])
671 self.wfile.flush()
672 time.sleep(1.0)
673 self.wfile.write(data[1:])
674 else:
675 self.wfile.write(data)
676 else:
677 """client do not support bzip2 format, send pseudo content
678 """
679 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
680 self.end_headers()
681 self.wfile.write("you do not support bzip2 encoding")
682 except:
683 self.send_error(404)
684
685 return True
686
687 def AuthBasicHandler(self):
688 """This handler tests 'Basic' authentication. It just sends a page with
689 title 'user/pass' if you succeed."""
690
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000691 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000692 return False
693
694 username = userpass = password = b64str = ""
695
ericroman@google.com239b4d82009-03-27 04:00:22 +0000696 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
697
initial.commit94958cf2008-07-26 22:42:52 +0000698 auth = self.headers.getheader('authorization')
699 try:
700 if not auth:
701 raise Exception('no auth')
702 b64str = re.findall(r'Basic (\S+)', auth)[0]
703 userpass = base64.b64decode(b64str)
704 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
705 if password != 'secret':
706 raise Exception('wrong password')
707 except Exception, e:
708 # Authentication failed.
709 self.send_response(401)
710 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
711 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000712 if set_cookie_if_challenged:
713 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000714 self.end_headers()
715 self.wfile.write('<html><head>')
716 self.wfile.write('<title>Denied: %s</title>' % e)
717 self.wfile.write('</head><body>')
718 self.wfile.write('auth=%s<p>' % auth)
719 self.wfile.write('b64str=%s<p>' % b64str)
720 self.wfile.write('username: %s<p>' % username)
721 self.wfile.write('userpass: %s<p>' % userpass)
722 self.wfile.write('password: %s<p>' % password)
723 self.wfile.write('You sent:<br>%s<p>' % self.headers)
724 self.wfile.write('</body></html>')
725 return True
726
727 # Authentication successful. (Return a cachable response to allow for
728 # testing cached pages that require authentication.)
729 if_none_match = self.headers.getheader('if-none-match')
730 if if_none_match == "abc":
731 self.send_response(304)
732 self.end_headers()
733 else:
734 self.send_response(200)
735 self.send_header('Content-type', 'text/html')
736 self.send_header('Cache-control', 'max-age=60000')
737 self.send_header('Etag', 'abc')
738 self.end_headers()
739 self.wfile.write('<html><head>')
740 self.wfile.write('<title>%s/%s</title>' % (username, password))
741 self.wfile.write('</head><body>')
742 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000743 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000744 self.wfile.write('</body></html>')
745
746 return True
747
748 def AuthDigestHandler(self):
749 """This handler tests 'Digest' authentication. It just sends a page with
750 title 'user/pass' if you succeed."""
751
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000752 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000753 return False
754
755 # Periodically generate a new nonce. Technically we should incorporate
756 # the request URL into this, but we don't care for testing.
757 nonce_life = 10
758 stale = False
maruel@google.come250a9b2009-03-10 17:39:46 +0000759 if (not self.server.nonce or
760 (time.time() - self.server.nonce_time > nonce_life)):
initial.commit94958cf2008-07-26 22:42:52 +0000761 if self.server.nonce:
762 stale = True
763 self.server.nonce_time = time.time()
764 self.server.nonce = \
maruel@google.come250a9b2009-03-10 17:39:46 +0000765 _new_md5(time.ctime(self.server.nonce_time) +
766 'privatekey').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000767
768 nonce = self.server.nonce
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000769 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000770 password = 'secret'
771 realm = 'testrealm'
772
773 auth = self.headers.getheader('authorization')
774 pairs = {}
775 try:
776 if not auth:
777 raise Exception('no auth')
778 if not auth.startswith('Digest'):
779 raise Exception('not digest')
780 # Pull out all the name="value" pairs as a dictionary.
781 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
782
783 # Make sure it's all valid.
784 if pairs['nonce'] != nonce:
785 raise Exception('wrong nonce')
786 if pairs['opaque'] != opaque:
787 raise Exception('wrong opaque')
788
789 # Check the 'response' value and make sure it matches our magic hash.
790 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000791 hash_a1 = _new_md5(
792 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000793 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000794 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000795 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000796 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
797 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000798 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000799
800 if pairs['response'] != response:
801 raise Exception('wrong password')
802 except Exception, e:
803 # Authentication failed.
804 self.send_response(401)
805 hdr = ('Digest '
806 'realm="%s", '
807 'domain="/", '
808 'qop="auth", '
809 'algorithm=MD5, '
810 'nonce="%s", '
811 'opaque="%s"') % (realm, nonce, opaque)
812 if stale:
813 hdr += ', stale="TRUE"'
814 self.send_header('WWW-Authenticate', hdr)
815 self.send_header('Content-type', 'text/html')
816 self.end_headers()
817 self.wfile.write('<html><head>')
818 self.wfile.write('<title>Denied: %s</title>' % e)
819 self.wfile.write('</head><body>')
820 self.wfile.write('auth=%s<p>' % auth)
821 self.wfile.write('pairs=%s<p>' % pairs)
822 self.wfile.write('You sent:<br>%s<p>' % self.headers)
823 self.wfile.write('We are replying:<br>%s<p>' % hdr)
824 self.wfile.write('</body></html>')
825 return True
826
827 # Authentication successful.
828 self.send_response(200)
829 self.send_header('Content-type', 'text/html')
830 self.end_headers()
831 self.wfile.write('<html><head>')
832 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
833 self.wfile.write('</head><body>')
834 self.wfile.write('auth=%s<p>' % auth)
835 self.wfile.write('pairs=%s<p>' % pairs)
836 self.wfile.write('</body></html>')
837
838 return True
839
840 def SlowServerHandler(self):
841 """Wait for the user suggested time before responding. The syntax is
842 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000843 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000844 return False
845 query_char = self.path.find('?')
846 wait_sec = 1.0
847 if query_char >= 0:
848 try:
849 wait_sec = int(self.path[query_char + 1:])
850 except ValueError:
851 pass
852 time.sleep(wait_sec)
853 self.send_response(200)
854 self.send_header('Content-type', 'text/plain')
855 self.end_headers()
856 self.wfile.write("waited %d seconds" % wait_sec)
857 return True
858
859 def ContentTypeHandler(self):
860 """Returns a string of html with the given content type. E.g.,
861 /contenttype?text/css returns an html file with the Content-Type
862 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000863 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +0000864 return False
865 query_char = self.path.find('?')
866 content_type = self.path[query_char + 1:].strip()
867 if not content_type:
868 content_type = 'text/html'
869 self.send_response(200)
870 self.send_header('Content-Type', content_type)
871 self.end_headers()
872 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
873 return True
874
875 def ServerRedirectHandler(self):
876 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000877 '/server-redirect?http://foo.bar/asdf' to redirect to
878 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000879
880 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000881 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000882 return False
883
884 query_char = self.path.find('?')
885 if query_char < 0 or len(self.path) <= query_char + 1:
886 self.sendRedirectHelp(test_name)
887 return True
888 dest = self.path[query_char + 1:]
889
890 self.send_response(301) # moved permanently
891 self.send_header('Location', dest)
892 self.send_header('Content-type', 'text/html')
893 self.end_headers()
894 self.wfile.write('<html><head>')
895 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
896
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000897 return True
initial.commit94958cf2008-07-26 22:42:52 +0000898
899 def ClientRedirectHandler(self):
900 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000901 '/client-redirect?http://foo.bar/asdf' to redirect to
902 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000903
904 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000905 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000906 return False
907
908 query_char = self.path.find('?');
909 if query_char < 0 or len(self.path) <= query_char + 1:
910 self.sendRedirectHelp(test_name)
911 return True
912 dest = self.path[query_char + 1:]
913
914 self.send_response(200)
915 self.send_header('Content-type', 'text/html')
916 self.end_headers()
917 self.wfile.write('<html><head>')
918 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
919 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
920
921 return True
922
923 def DefaultResponseHandler(self):
924 """This is the catch-all response handler for requests that aren't handled
925 by one of the special handlers above.
926 Note that we specify the content-length as without it the https connection
927 is not closed properly (and the browser keeps expecting data)."""
928
929 contents = "Default response given for path: " + self.path
930 self.send_response(200)
931 self.send_header('Content-type', 'text/html')
932 self.send_header("Content-Length", len(contents))
933 self.end_headers()
934 self.wfile.write(contents)
935 return True
936
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000937 def RedirectConnectHandler(self):
938 """Sends a redirect to the CONNECT request for www.redirect.com. This
939 response is not specified by the RFC, so the browser should not follow
940 the redirect."""
941
942 if (self.path.find("www.redirect.com") < 0):
943 return False
944
945 dest = "http://www.destination.com/foo.js"
946
947 self.send_response(302) # moved temporarily
948 self.send_header('Location', dest)
949 self.send_header('Connection', 'close')
950 self.end_headers()
951 return True
952
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000953 def ServerAuthConnectHandler(self):
954 """Sends a 401 to the CONNECT request for www.server-auth.com. This
955 response doesn't make sense because the proxy server cannot request
956 server authentication."""
957
958 if (self.path.find("www.server-auth.com") < 0):
959 return False
960
961 challenge = 'Basic realm="WallyWorld"'
962
963 self.send_response(401) # unauthorized
964 self.send_header('WWW-Authenticate', challenge)
965 self.send_header('Connection', 'close')
966 self.end_headers()
967 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000968
969 def DefaultConnectResponseHandler(self):
970 """This is the catch-all response handler for CONNECT requests that aren't
971 handled by one of the special handlers above. Real Web servers respond
972 with 400 to CONNECT requests."""
973
974 contents = "Your client has issued a malformed or illegal request."
975 self.send_response(400) # bad request
976 self.send_header('Content-type', 'text/html')
977 self.send_header("Content-Length", len(contents))
978 self.end_headers()
979 self.wfile.write(contents)
980 return True
981
982 def do_CONNECT(self):
983 for handler in self._connect_handlers:
984 if handler():
985 return
986
initial.commit94958cf2008-07-26 22:42:52 +0000987 def do_GET(self):
988 for handler in self._get_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000989 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000990 return
991
992 def do_POST(self):
993 for handler in self._post_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000994 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000995 return
996
997 # called by the redirect handling function when there is no parameter
998 def sendRedirectHelp(self, redirect_name):
999 self.send_response(200)
1000 self.send_header('Content-type', 'text/html')
1001 self.end_headers()
1002 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1003 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1004 self.wfile.write('</body></html>')
1005
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001006def MakeDumpDir(data_dir):
1007 """Create directory named 'dump' where uploaded data via HTTP POST request
1008 will be stored. If the directory already exists all files and subdirectories
1009 will be deleted."""
1010 dump_dir = os.path.join(data_dir, 'dump');
1011 if os.path.isdir(dump_dir):
1012 shutil.rmtree(dump_dir)
1013 os.mkdir(dump_dir)
1014
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001015def MakeDataDir():
1016 if options.data_dir:
1017 if not os.path.isdir(options.data_dir):
1018 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1019 return None
1020 my_data_dir = options.data_dir
1021 else:
1022 # Create the default path to our data dir, relative to the exe dir.
1023 my_data_dir = os.path.dirname(sys.argv[0])
1024 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
1025 "test", "data")
1026
1027 #TODO(ibrar): Must use Find* funtion defined in google\tools
1028 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1029
1030 return my_data_dir
1031
initial.commit94958cf2008-07-26 22:42:52 +00001032def main(options, args):
1033 # redirect output to a log file so it doesn't spam the unit test output
1034 logfile = open('testserver.log', 'w')
1035 sys.stderr = sys.stdout = logfile
1036
1037 port = options.port
1038
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001039 if options.server_type == SERVER_HTTP:
1040 if options.cert:
1041 # let's make sure the cert file exists.
1042 if not os.path.isfile(options.cert):
1043 print 'specified cert file not found: ' + options.cert + ' exiting...'
1044 return
1045 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
1046 print 'HTTPS server started on port %d...' % port
1047 else:
1048 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
1049 print 'HTTP server started on port %d...' % port
erikkay@google.com70397b62008-12-30 21:49:21 +00001050
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001051 server.data_dir = MakeDataDir()
ben@chromium.org260c0172009-04-09 19:10:41 +00001052 server.file_root_url = options.file_root_url
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001053 MakeDumpDir(server.data_dir)
maruel@chromium.org756cf982009-03-05 12:46:38 +00001054
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001055 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001056 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001057 my_data_dir = MakeDataDir()
1058
1059 def line_logger(msg):
1060 if (msg.find("kill") >= 0):
1061 server.stop = True
1062 print 'shutting down server'
1063 sys.exit(0)
1064
1065 # Instantiate a dummy authorizer for managing 'virtual' users
1066 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1067
1068 # Define a new user having full r/w permissions and a read-only
1069 # anonymous user
1070 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1071
1072 authorizer.add_anonymous(my_data_dir)
1073
1074 # Instantiate FTP handler class
1075 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1076 ftp_handler.authorizer = authorizer
1077 pyftpdlib.ftpserver.logline = line_logger
1078
1079 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001080 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1081 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001082
1083 # Instantiate FTP server class and listen to 127.0.0.1:port
1084 address = ('127.0.0.1', port)
1085 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
1086 print 'FTP server started on port %d...' % port
initial.commit94958cf2008-07-26 22:42:52 +00001087
1088 try:
1089 server.serve_forever()
1090 except KeyboardInterrupt:
1091 print 'shutting down server'
1092 server.stop = True
1093
1094if __name__ == '__main__':
1095 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001096 option_parser.add_option("-f", '--ftp', action='store_const',
1097 const=SERVER_FTP, default=SERVER_HTTP,
1098 dest='server_type',
1099 help='FTP or HTTP server default HTTP')
initial.commit94958cf2008-07-26 22:42:52 +00001100 option_parser.add_option('', '--port', default='8888', type='int',
1101 help='Port used by the server')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001102 option_parser.add_option('', '--data-dir', dest='data_dir',
initial.commit94958cf2008-07-26 22:42:52 +00001103 help='Directory from which to read the files')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001104 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001105 help='Specify that https should be used, specify '
1106 'the path to the cert containing the private key '
1107 'the server should use')
ben@chromium.org260c0172009-04-09 19:10:41 +00001108 option_parser.add_option('', '--file-root-url', default='/files/',
1109 help='Specify a root URL for files served.')
initial.commit94958cf2008-07-26 22:42:52 +00001110 options, args = option_parser.parse_args()
1111
1112 sys.exit(main(options, args))