blob: 57840599248e76346bb6d19df2a679db4c9321c8 [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
ericroman@google.com239b4d82009-03-27 04:00:22 +0000694 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
695
initial.commit94958cf2008-07-26 22:42:52 +0000696 auth = self.headers.getheader('authorization')
697 try:
698 if not auth:
699 raise Exception('no auth')
700 b64str = re.findall(r'Basic (\S+)', auth)[0]
701 userpass = base64.b64decode(b64str)
702 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
703 if password != 'secret':
704 raise Exception('wrong password')
705 except Exception, e:
706 # Authentication failed.
707 self.send_response(401)
708 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
709 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000710 if set_cookie_if_challenged:
711 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000712 self.end_headers()
713 self.wfile.write('<html><head>')
714 self.wfile.write('<title>Denied: %s</title>' % e)
715 self.wfile.write('</head><body>')
716 self.wfile.write('auth=%s<p>' % auth)
717 self.wfile.write('b64str=%s<p>' % b64str)
718 self.wfile.write('username: %s<p>' % username)
719 self.wfile.write('userpass: %s<p>' % userpass)
720 self.wfile.write('password: %s<p>' % password)
721 self.wfile.write('You sent:<br>%s<p>' % self.headers)
722 self.wfile.write('</body></html>')
723 return True
724
725 # Authentication successful. (Return a cachable response to allow for
726 # testing cached pages that require authentication.)
727 if_none_match = self.headers.getheader('if-none-match')
728 if if_none_match == "abc":
729 self.send_response(304)
730 self.end_headers()
731 else:
732 self.send_response(200)
733 self.send_header('Content-type', 'text/html')
734 self.send_header('Cache-control', 'max-age=60000')
735 self.send_header('Etag', 'abc')
736 self.end_headers()
737 self.wfile.write('<html><head>')
738 self.wfile.write('<title>%s/%s</title>' % (username, password))
739 self.wfile.write('</head><body>')
740 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000741 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000742 self.wfile.write('</body></html>')
743
744 return True
745
746 def AuthDigestHandler(self):
747 """This handler tests 'Digest' authentication. It just sends a page with
748 title 'user/pass' if you succeed."""
749
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000750 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000751 return False
752
753 # Periodically generate a new nonce. Technically we should incorporate
754 # the request URL into this, but we don't care for testing.
755 nonce_life = 10
756 stale = False
maruel@google.come250a9b2009-03-10 17:39:46 +0000757 if (not self.server.nonce or
758 (time.time() - self.server.nonce_time > nonce_life)):
initial.commit94958cf2008-07-26 22:42:52 +0000759 if self.server.nonce:
760 stale = True
761 self.server.nonce_time = time.time()
762 self.server.nonce = \
maruel@google.come250a9b2009-03-10 17:39:46 +0000763 _new_md5(time.ctime(self.server.nonce_time) +
764 'privatekey').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000765
766 nonce = self.server.nonce
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000767 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000768 password = 'secret'
769 realm = 'testrealm'
770
771 auth = self.headers.getheader('authorization')
772 pairs = {}
773 try:
774 if not auth:
775 raise Exception('no auth')
776 if not auth.startswith('Digest'):
777 raise Exception('not digest')
778 # Pull out all the name="value" pairs as a dictionary.
779 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
780
781 # Make sure it's all valid.
782 if pairs['nonce'] != nonce:
783 raise Exception('wrong nonce')
784 if pairs['opaque'] != opaque:
785 raise Exception('wrong opaque')
786
787 # Check the 'response' value and make sure it matches our magic hash.
788 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000789 hash_a1 = _new_md5(
790 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000791 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000792 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000793 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000794 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
795 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000796 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000797
798 if pairs['response'] != response:
799 raise Exception('wrong password')
800 except Exception, e:
801 # Authentication failed.
802 self.send_response(401)
803 hdr = ('Digest '
804 'realm="%s", '
805 'domain="/", '
806 'qop="auth", '
807 'algorithm=MD5, '
808 'nonce="%s", '
809 'opaque="%s"') % (realm, nonce, opaque)
810 if stale:
811 hdr += ', stale="TRUE"'
812 self.send_header('WWW-Authenticate', hdr)
813 self.send_header('Content-type', 'text/html')
814 self.end_headers()
815 self.wfile.write('<html><head>')
816 self.wfile.write('<title>Denied: %s</title>' % e)
817 self.wfile.write('</head><body>')
818 self.wfile.write('auth=%s<p>' % auth)
819 self.wfile.write('pairs=%s<p>' % pairs)
820 self.wfile.write('You sent:<br>%s<p>' % self.headers)
821 self.wfile.write('We are replying:<br>%s<p>' % hdr)
822 self.wfile.write('</body></html>')
823 return True
824
825 # Authentication successful.
826 self.send_response(200)
827 self.send_header('Content-type', 'text/html')
828 self.end_headers()
829 self.wfile.write('<html><head>')
830 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
831 self.wfile.write('</head><body>')
832 self.wfile.write('auth=%s<p>' % auth)
833 self.wfile.write('pairs=%s<p>' % pairs)
834 self.wfile.write('</body></html>')
835
836 return True
837
838 def SlowServerHandler(self):
839 """Wait for the user suggested time before responding. The syntax is
840 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000841 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000842 return False
843 query_char = self.path.find('?')
844 wait_sec = 1.0
845 if query_char >= 0:
846 try:
847 wait_sec = int(self.path[query_char + 1:])
848 except ValueError:
849 pass
850 time.sleep(wait_sec)
851 self.send_response(200)
852 self.send_header('Content-type', 'text/plain')
853 self.end_headers()
854 self.wfile.write("waited %d seconds" % wait_sec)
855 return True
856
857 def ContentTypeHandler(self):
858 """Returns a string of html with the given content type. E.g.,
859 /contenttype?text/css returns an html file with the Content-Type
860 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000861 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +0000862 return False
863 query_char = self.path.find('?')
864 content_type = self.path[query_char + 1:].strip()
865 if not content_type:
866 content_type = 'text/html'
867 self.send_response(200)
868 self.send_header('Content-Type', content_type)
869 self.end_headers()
870 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
871 return True
872
873 def ServerRedirectHandler(self):
874 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000875 '/server-redirect?http://foo.bar/asdf' to redirect to
876 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000877
878 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000879 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000880 return False
881
882 query_char = self.path.find('?')
883 if query_char < 0 or len(self.path) <= query_char + 1:
884 self.sendRedirectHelp(test_name)
885 return True
886 dest = self.path[query_char + 1:]
887
888 self.send_response(301) # moved permanently
889 self.send_header('Location', dest)
890 self.send_header('Content-type', 'text/html')
891 self.end_headers()
892 self.wfile.write('<html><head>')
893 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
894
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000895 return True
initial.commit94958cf2008-07-26 22:42:52 +0000896
897 def ClientRedirectHandler(self):
898 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000899 '/client-redirect?http://foo.bar/asdf' to redirect to
900 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000901
902 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000903 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000904 return False
905
906 query_char = self.path.find('?');
907 if query_char < 0 or len(self.path) <= query_char + 1:
908 self.sendRedirectHelp(test_name)
909 return True
910 dest = self.path[query_char + 1:]
911
912 self.send_response(200)
913 self.send_header('Content-type', 'text/html')
914 self.end_headers()
915 self.wfile.write('<html><head>')
916 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
917 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
918
919 return True
920
921 def DefaultResponseHandler(self):
922 """This is the catch-all response handler for requests that aren't handled
923 by one of the special handlers above.
924 Note that we specify the content-length as without it the https connection
925 is not closed properly (and the browser keeps expecting data)."""
926
927 contents = "Default response given for path: " + self.path
928 self.send_response(200)
929 self.send_header('Content-type', 'text/html')
930 self.send_header("Content-Length", len(contents))
931 self.end_headers()
932 self.wfile.write(contents)
933 return True
934
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000935 def RedirectConnectHandler(self):
936 """Sends a redirect to the CONNECT request for www.redirect.com. This
937 response is not specified by the RFC, so the browser should not follow
938 the redirect."""
939
940 if (self.path.find("www.redirect.com") < 0):
941 return False
942
943 dest = "http://www.destination.com/foo.js"
944
945 self.send_response(302) # moved temporarily
946 self.send_header('Location', dest)
947 self.send_header('Connection', 'close')
948 self.end_headers()
949 return True
950
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000951 def ServerAuthConnectHandler(self):
952 """Sends a 401 to the CONNECT request for www.server-auth.com. This
953 response doesn't make sense because the proxy server cannot request
954 server authentication."""
955
956 if (self.path.find("www.server-auth.com") < 0):
957 return False
958
959 challenge = 'Basic realm="WallyWorld"'
960
961 self.send_response(401) # unauthorized
962 self.send_header('WWW-Authenticate', challenge)
963 self.send_header('Connection', 'close')
964 self.end_headers()
965 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000966
967 def DefaultConnectResponseHandler(self):
968 """This is the catch-all response handler for CONNECT requests that aren't
969 handled by one of the special handlers above. Real Web servers respond
970 with 400 to CONNECT requests."""
971
972 contents = "Your client has issued a malformed or illegal request."
973 self.send_response(400) # bad request
974 self.send_header('Content-type', 'text/html')
975 self.send_header("Content-Length", len(contents))
976 self.end_headers()
977 self.wfile.write(contents)
978 return True
979
980 def do_CONNECT(self):
981 for handler in self._connect_handlers:
982 if handler():
983 return
984
initial.commit94958cf2008-07-26 22:42:52 +0000985 def do_GET(self):
986 for handler in self._get_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000987 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000988 return
989
990 def do_POST(self):
991 for handler in self._post_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000992 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000993 return
994
995 # called by the redirect handling function when there is no parameter
996 def sendRedirectHelp(self, redirect_name):
997 self.send_response(200)
998 self.send_header('Content-type', 'text/html')
999 self.end_headers()
1000 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1001 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1002 self.wfile.write('</body></html>')
1003
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001004def MakeDumpDir(data_dir):
1005 """Create directory named 'dump' where uploaded data via HTTP POST request
1006 will be stored. If the directory already exists all files and subdirectories
1007 will be deleted."""
1008 dump_dir = os.path.join(data_dir, 'dump');
1009 if os.path.isdir(dump_dir):
1010 shutil.rmtree(dump_dir)
1011 os.mkdir(dump_dir)
1012
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001013def MakeDataDir():
1014 if options.data_dir:
1015 if not os.path.isdir(options.data_dir):
1016 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1017 return None
1018 my_data_dir = options.data_dir
1019 else:
1020 # Create the default path to our data dir, relative to the exe dir.
1021 my_data_dir = os.path.dirname(sys.argv[0])
1022 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
1023 "test", "data")
1024
1025 #TODO(ibrar): Must use Find* funtion defined in google\tools
1026 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1027
1028 return my_data_dir
1029
initial.commit94958cf2008-07-26 22:42:52 +00001030def main(options, args):
1031 # redirect output to a log file so it doesn't spam the unit test output
1032 logfile = open('testserver.log', 'w')
1033 sys.stderr = sys.stdout = logfile
1034
1035 port = options.port
1036
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001037 if options.server_type == SERVER_HTTP:
1038 if options.cert:
1039 # let's make sure the cert file exists.
1040 if not os.path.isfile(options.cert):
1041 print 'specified cert file not found: ' + options.cert + ' exiting...'
1042 return
1043 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
1044 print 'HTTPS server started on port %d...' % port
1045 else:
1046 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
1047 print 'HTTP server started on port %d...' % port
erikkay@google.com70397b62008-12-30 21:49:21 +00001048
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001049 server.data_dir = MakeDataDir()
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001050 MakeDumpDir(server.data_dir)
maruel@chromium.org756cf982009-03-05 12:46:38 +00001051
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001052 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001053 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001054 my_data_dir = MakeDataDir()
1055
1056 def line_logger(msg):
1057 if (msg.find("kill") >= 0):
1058 server.stop = True
1059 print 'shutting down server'
1060 sys.exit(0)
1061
1062 # Instantiate a dummy authorizer for managing 'virtual' users
1063 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1064
1065 # Define a new user having full r/w permissions and a read-only
1066 # anonymous user
1067 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1068
1069 authorizer.add_anonymous(my_data_dir)
1070
1071 # Instantiate FTP handler class
1072 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1073 ftp_handler.authorizer = authorizer
1074 pyftpdlib.ftpserver.logline = line_logger
1075
1076 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001077 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1078 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001079
1080 # Instantiate FTP server class and listen to 127.0.0.1:port
1081 address = ('127.0.0.1', port)
1082 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
1083 print 'FTP server started on port %d...' % port
initial.commit94958cf2008-07-26 22:42:52 +00001084
1085 try:
1086 server.serve_forever()
1087 except KeyboardInterrupt:
1088 print 'shutting down server'
1089 server.stop = True
1090
1091if __name__ == '__main__':
1092 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001093 option_parser.add_option("-f", '--ftp', action='store_const',
1094 const=SERVER_FTP, default=SERVER_HTTP,
1095 dest='server_type',
1096 help='FTP or HTTP server default HTTP')
initial.commit94958cf2008-07-26 22:42:52 +00001097 option_parser.add_option('', '--port', default='8888', type='int',
1098 help='Port used by the server')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001099 option_parser.add_option('', '--data-dir', dest='data_dir',
initial.commit94958cf2008-07-26 22:42:52 +00001100 help='Directory from which to read the files')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001101 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001102 help='Specify that https should be used, specify '
1103 'the path to the cert containing the private key '
1104 'the server should use')
1105 options, args = option_parser.parse_args()
1106
1107 sys.exit(main(options, args))