blob: 407694a724be834bd698578e882648117334e53d [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
568 prefix='/files/'
569 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)
575
576 if not os.path.isfile(path):
577 print "File not found " + file + " full path:" + path
578 self.send_error(404)
579 return True
580
581 f = open(path, "rb")
582 data = f.read()
583 f.close()
584
585 # If file.mock-http-headers exists, it contains the headers we
586 # should send. Read them in and parse them.
587 headers_path = path + '.mock-http-headers'
588 if os.path.isfile(headers_path):
589 f = open(headers_path, "r")
590
591 # "HTTP/1.1 200 OK"
592 response = f.readline()
593 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
594 self.send_response(int(status_code))
595
596 for line in f:
597 # "name: value"
598 name, value = re.findall('(\S+):\s*(.*)', line)[0]
599 self.send_header(name, value)
600 f.close()
601 else:
602 # Could be more generic once we support mime-type sniffing, but for
603 # now we need to set it explicitly.
604 self.send_response(200)
605 self.send_header('Content-type', self.GetMIMETypeFromName(file))
606 self.send_header('Content-Length', len(data))
607 self.end_headers()
608
609 self.wfile.write(data)
610
611 return True
612
613 def RealFileWithCommonHeaderHandler(self):
614 """This handler sends the contents of the requested file without the pseudo
615 http head!"""
616
617 prefix='/realfiles/'
618 if not self.path.startswith(prefix):
619 return False
620
621 file = self.path[len(prefix):]
622 path = os.path.join(self.server.data_dir, file)
623
624 try:
625 f = open(path, "rb")
626 data = f.read()
627 f.close()
628
629 # just simply set the MIME as octal stream
630 self.send_response(200)
631 self.send_header('Content-type', 'application/octet-stream')
632 self.end_headers()
633
634 self.wfile.write(data)
635 except:
636 self.send_error(404)
637
638 return True
639
640 def RealBZ2FileWithCommonHeaderHandler(self):
641 """This handler sends the bzip2 contents of the requested file with
642 corresponding Content-Encoding field in http head!"""
643
644 prefix='/realbz2files/'
645 if not self.path.startswith(prefix):
646 return False
647
648 parts = self.path.split('?')
649 file = parts[0][len(prefix):]
650 path = os.path.join(self.server.data_dir, file) + '.bz2'
651
652 if len(parts) > 1:
653 options = parts[1]
654 else:
655 options = ''
656
657 try:
658 self.send_response(200)
659 accept_encoding = self.headers.get("Accept-Encoding")
660 if accept_encoding.find("bzip2") != -1:
661 f = open(path, "rb")
662 data = f.read()
663 f.close()
664 self.send_header('Content-Encoding', 'bzip2')
665 self.send_header('Content-type', 'application/x-bzip2')
666 self.end_headers()
667 if options == 'incremental-header':
668 self.wfile.write(data[:1])
669 self.wfile.flush()
670 time.sleep(1.0)
671 self.wfile.write(data[1:])
672 else:
673 self.wfile.write(data)
674 else:
675 """client do not support bzip2 format, send pseudo content
676 """
677 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
678 self.end_headers()
679 self.wfile.write("you do not support bzip2 encoding")
680 except:
681 self.send_error(404)
682
683 return True
684
685 def AuthBasicHandler(self):
686 """This handler tests 'Basic' authentication. It just sends a page with
687 title 'user/pass' if you succeed."""
688
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000689 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000690 return False
691
692 username = userpass = password = b64str = ""
693
694 auth = self.headers.getheader('authorization')
695 try:
696 if not auth:
697 raise Exception('no auth')
698 b64str = re.findall(r'Basic (\S+)', auth)[0]
699 userpass = base64.b64decode(b64str)
700 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
701 if password != 'secret':
702 raise Exception('wrong password')
703 except Exception, e:
704 # Authentication failed.
705 self.send_response(401)
706 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
707 self.send_header('Content-type', 'text/html')
708 self.end_headers()
709 self.wfile.write('<html><head>')
710 self.wfile.write('<title>Denied: %s</title>' % e)
711 self.wfile.write('</head><body>')
712 self.wfile.write('auth=%s<p>' % auth)
713 self.wfile.write('b64str=%s<p>' % b64str)
714 self.wfile.write('username: %s<p>' % username)
715 self.wfile.write('userpass: %s<p>' % userpass)
716 self.wfile.write('password: %s<p>' % password)
717 self.wfile.write('You sent:<br>%s<p>' % self.headers)
718 self.wfile.write('</body></html>')
719 return True
720
721 # Authentication successful. (Return a cachable response to allow for
722 # testing cached pages that require authentication.)
723 if_none_match = self.headers.getheader('if-none-match')
724 if if_none_match == "abc":
725 self.send_response(304)
726 self.end_headers()
727 else:
728 self.send_response(200)
729 self.send_header('Content-type', 'text/html')
730 self.send_header('Cache-control', 'max-age=60000')
731 self.send_header('Etag', 'abc')
732 self.end_headers()
733 self.wfile.write('<html><head>')
734 self.wfile.write('<title>%s/%s</title>' % (username, password))
735 self.wfile.write('</head><body>')
736 self.wfile.write('auth=%s<p>' % auth)
737 self.wfile.write('</body></html>')
738
739 return True
740
741 def AuthDigestHandler(self):
742 """This handler tests 'Digest' authentication. It just sends a page with
743 title 'user/pass' if you succeed."""
744
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000745 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000746 return False
747
748 # Periodically generate a new nonce. Technically we should incorporate
749 # the request URL into this, but we don't care for testing.
750 nonce_life = 10
751 stale = False
maruel@google.come250a9b2009-03-10 17:39:46 +0000752 if (not self.server.nonce or
753 (time.time() - self.server.nonce_time > nonce_life)):
initial.commit94958cf2008-07-26 22:42:52 +0000754 if self.server.nonce:
755 stale = True
756 self.server.nonce_time = time.time()
757 self.server.nonce = \
maruel@google.come250a9b2009-03-10 17:39:46 +0000758 _new_md5(time.ctime(self.server.nonce_time) +
759 'privatekey').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000760
761 nonce = self.server.nonce
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000762 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000763 password = 'secret'
764 realm = 'testrealm'
765
766 auth = self.headers.getheader('authorization')
767 pairs = {}
768 try:
769 if not auth:
770 raise Exception('no auth')
771 if not auth.startswith('Digest'):
772 raise Exception('not digest')
773 # Pull out all the name="value" pairs as a dictionary.
774 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
775
776 # Make sure it's all valid.
777 if pairs['nonce'] != nonce:
778 raise Exception('wrong nonce')
779 if pairs['opaque'] != opaque:
780 raise Exception('wrong opaque')
781
782 # Check the 'response' value and make sure it matches our magic hash.
783 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000784 hash_a1 = _new_md5(
785 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000786 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000787 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000788 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000789 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
790 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000791 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000792
793 if pairs['response'] != response:
794 raise Exception('wrong password')
795 except Exception, e:
796 # Authentication failed.
797 self.send_response(401)
798 hdr = ('Digest '
799 'realm="%s", '
800 'domain="/", '
801 'qop="auth", '
802 'algorithm=MD5, '
803 'nonce="%s", '
804 'opaque="%s"') % (realm, nonce, opaque)
805 if stale:
806 hdr += ', stale="TRUE"'
807 self.send_header('WWW-Authenticate', hdr)
808 self.send_header('Content-type', 'text/html')
809 self.end_headers()
810 self.wfile.write('<html><head>')
811 self.wfile.write('<title>Denied: %s</title>' % e)
812 self.wfile.write('</head><body>')
813 self.wfile.write('auth=%s<p>' % auth)
814 self.wfile.write('pairs=%s<p>' % pairs)
815 self.wfile.write('You sent:<br>%s<p>' % self.headers)
816 self.wfile.write('We are replying:<br>%s<p>' % hdr)
817 self.wfile.write('</body></html>')
818 return True
819
820 # Authentication successful.
821 self.send_response(200)
822 self.send_header('Content-type', 'text/html')
823 self.end_headers()
824 self.wfile.write('<html><head>')
825 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
826 self.wfile.write('</head><body>')
827 self.wfile.write('auth=%s<p>' % auth)
828 self.wfile.write('pairs=%s<p>' % pairs)
829 self.wfile.write('</body></html>')
830
831 return True
832
833 def SlowServerHandler(self):
834 """Wait for the user suggested time before responding. The syntax is
835 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000836 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000837 return False
838 query_char = self.path.find('?')
839 wait_sec = 1.0
840 if query_char >= 0:
841 try:
842 wait_sec = int(self.path[query_char + 1:])
843 except ValueError:
844 pass
845 time.sleep(wait_sec)
846 self.send_response(200)
847 self.send_header('Content-type', 'text/plain')
848 self.end_headers()
849 self.wfile.write("waited %d seconds" % wait_sec)
850 return True
851
852 def ContentTypeHandler(self):
853 """Returns a string of html with the given content type. E.g.,
854 /contenttype?text/css returns an html file with the Content-Type
855 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000856 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +0000857 return False
858 query_char = self.path.find('?')
859 content_type = self.path[query_char + 1:].strip()
860 if not content_type:
861 content_type = 'text/html'
862 self.send_response(200)
863 self.send_header('Content-Type', content_type)
864 self.end_headers()
865 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
866 return True
867
868 def ServerRedirectHandler(self):
869 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000870 '/server-redirect?http://foo.bar/asdf' to redirect to
871 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000872
873 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000874 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000875 return False
876
877 query_char = self.path.find('?')
878 if query_char < 0 or len(self.path) <= query_char + 1:
879 self.sendRedirectHelp(test_name)
880 return True
881 dest = self.path[query_char + 1:]
882
883 self.send_response(301) # moved permanently
884 self.send_header('Location', dest)
885 self.send_header('Content-type', 'text/html')
886 self.end_headers()
887 self.wfile.write('<html><head>')
888 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
889
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000890 return True
initial.commit94958cf2008-07-26 22:42:52 +0000891
892 def ClientRedirectHandler(self):
893 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000894 '/client-redirect?http://foo.bar/asdf' to redirect to
895 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000896
897 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000898 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000899 return False
900
901 query_char = self.path.find('?');
902 if query_char < 0 or len(self.path) <= query_char + 1:
903 self.sendRedirectHelp(test_name)
904 return True
905 dest = self.path[query_char + 1:]
906
907 self.send_response(200)
908 self.send_header('Content-type', 'text/html')
909 self.end_headers()
910 self.wfile.write('<html><head>')
911 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
912 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
913
914 return True
915
916 def DefaultResponseHandler(self):
917 """This is the catch-all response handler for requests that aren't handled
918 by one of the special handlers above.
919 Note that we specify the content-length as without it the https connection
920 is not closed properly (and the browser keeps expecting data)."""
921
922 contents = "Default response given for path: " + self.path
923 self.send_response(200)
924 self.send_header('Content-type', 'text/html')
925 self.send_header("Content-Length", len(contents))
926 self.end_headers()
927 self.wfile.write(contents)
928 return True
929
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000930 def RedirectConnectHandler(self):
931 """Sends a redirect to the CONNECT request for www.redirect.com. This
932 response is not specified by the RFC, so the browser should not follow
933 the redirect."""
934
935 if (self.path.find("www.redirect.com") < 0):
936 return False
937
938 dest = "http://www.destination.com/foo.js"
939
940 self.send_response(302) # moved temporarily
941 self.send_header('Location', dest)
942 self.send_header('Connection', 'close')
943 self.end_headers()
944 return True
945
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000946 def ServerAuthConnectHandler(self):
947 """Sends a 401 to the CONNECT request for www.server-auth.com. This
948 response doesn't make sense because the proxy server cannot request
949 server authentication."""
950
951 if (self.path.find("www.server-auth.com") < 0):
952 return False
953
954 challenge = 'Basic realm="WallyWorld"'
955
956 self.send_response(401) # unauthorized
957 self.send_header('WWW-Authenticate', challenge)
958 self.send_header('Connection', 'close')
959 self.end_headers()
960 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000961
962 def DefaultConnectResponseHandler(self):
963 """This is the catch-all response handler for CONNECT requests that aren't
964 handled by one of the special handlers above. Real Web servers respond
965 with 400 to CONNECT requests."""
966
967 contents = "Your client has issued a malformed or illegal request."
968 self.send_response(400) # bad request
969 self.send_header('Content-type', 'text/html')
970 self.send_header("Content-Length", len(contents))
971 self.end_headers()
972 self.wfile.write(contents)
973 return True
974
975 def do_CONNECT(self):
976 for handler in self._connect_handlers:
977 if handler():
978 return
979
initial.commit94958cf2008-07-26 22:42:52 +0000980 def do_GET(self):
981 for handler in self._get_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000982 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000983 return
984
985 def do_POST(self):
986 for handler in self._post_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000987 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000988 return
989
990 # called by the redirect handling function when there is no parameter
991 def sendRedirectHelp(self, redirect_name):
992 self.send_response(200)
993 self.send_header('Content-type', 'text/html')
994 self.end_headers()
995 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
996 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
997 self.wfile.write('</body></html>')
998
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000999def MakeDumpDir(data_dir):
1000 """Create directory named 'dump' where uploaded data via HTTP POST request
1001 will be stored. If the directory already exists all files and subdirectories
1002 will be deleted."""
1003 dump_dir = os.path.join(data_dir, 'dump');
1004 if os.path.isdir(dump_dir):
1005 shutil.rmtree(dump_dir)
1006 os.mkdir(dump_dir)
1007
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001008def MakeDataDir():
1009 if options.data_dir:
1010 if not os.path.isdir(options.data_dir):
1011 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1012 return None
1013 my_data_dir = options.data_dir
1014 else:
1015 # Create the default path to our data dir, relative to the exe dir.
1016 my_data_dir = os.path.dirname(sys.argv[0])
1017 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
1018 "test", "data")
1019
1020 #TODO(ibrar): Must use Find* funtion defined in google\tools
1021 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1022
1023 return my_data_dir
1024
initial.commit94958cf2008-07-26 22:42:52 +00001025def main(options, args):
1026 # redirect output to a log file so it doesn't spam the unit test output
1027 logfile = open('testserver.log', 'w')
1028 sys.stderr = sys.stdout = logfile
1029
1030 port = options.port
1031
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001032 if options.server_type == SERVER_HTTP:
1033 if options.cert:
1034 # let's make sure the cert file exists.
1035 if not os.path.isfile(options.cert):
1036 print 'specified cert file not found: ' + options.cert + ' exiting...'
1037 return
1038 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
1039 print 'HTTPS server started on port %d...' % port
1040 else:
1041 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
1042 print 'HTTP server started on port %d...' % port
erikkay@google.com70397b62008-12-30 21:49:21 +00001043
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001044 server.data_dir = MakeDataDir()
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001045 MakeDumpDir(server.data_dir)
maruel@chromium.org756cf982009-03-05 12:46:38 +00001046
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001047 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001048 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001049 my_data_dir = MakeDataDir()
1050
1051 def line_logger(msg):
1052 if (msg.find("kill") >= 0):
1053 server.stop = True
1054 print 'shutting down server'
1055 sys.exit(0)
1056
1057 # Instantiate a dummy authorizer for managing 'virtual' users
1058 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1059
1060 # Define a new user having full r/w permissions and a read-only
1061 # anonymous user
1062 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1063
1064 authorizer.add_anonymous(my_data_dir)
1065
1066 # Instantiate FTP handler class
1067 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1068 ftp_handler.authorizer = authorizer
1069 pyftpdlib.ftpserver.logline = line_logger
1070
1071 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001072 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1073 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001074
1075 # Instantiate FTP server class and listen to 127.0.0.1:port
1076 address = ('127.0.0.1', port)
1077 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
1078 print 'FTP server started on port %d...' % port
initial.commit94958cf2008-07-26 22:42:52 +00001079
1080 try:
1081 server.serve_forever()
1082 except KeyboardInterrupt:
1083 print 'shutting down server'
1084 server.stop = True
1085
1086if __name__ == '__main__':
1087 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001088 option_parser.add_option("-f", '--ftp', action='store_const',
1089 const=SERVER_FTP, default=SERVER_HTTP,
1090 dest='server_type',
1091 help='FTP or HTTP server default HTTP')
initial.commit94958cf2008-07-26 22:42:52 +00001092 option_parser.add_option('', '--port', default='8888', type='int',
1093 help='Port used by the server')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001094 option_parser.add_option('', '--data-dir', dest='data_dir',
initial.commit94958cf2008-07-26 22:42:52 +00001095 help='Directory from which to read the files')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001096 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001097 help='Specify that https should be used, specify '
1098 'the path to the cert containing the private key '
1099 'the server should use')
1100 options, args = option_parser.parse_args()
1101
1102 sys.exit(main(options, args))