blob: 2d897334b1db5345cf26f18cf81269f6ecede25a [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
123 self._mime_types = { 'gif': 'image/gif', 'jpeg' : 'image/jpeg', 'jpg' : 'image/jpeg' }
124 self._default_mime_type = 'text/html'
125
126 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, socket_server)
127
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000128 def _ShouldHandleRequest(self, handler_name):
129 """Determines if the path can be handled by the handler.
130
131 We consider a handler valid if the path begins with the
132 handler name. It can optionally be followed by "?*", "/*".
133 """
134
135 pattern = re.compile('%s($|\?|/).*' % handler_name)
136 return pattern.match(self.path)
137
initial.commit94958cf2008-07-26 22:42:52 +0000138 def GetMIMETypeFromName(self, file_name):
139 """Returns the mime type for the specified file_name. So far it only looks
140 at the file extension."""
141
142 (shortname, extension) = os.path.splitext(file_name)
143 if len(extension) == 0:
144 # no extension.
145 return self._default_mime_type
146
147 return self._mime_types.get(extension, self._default_mime_type)
148
149 def KillHandler(self):
150 """This request handler kills the server, for use when we're done"
151 with the a particular test."""
152
153 if (self.path.find("kill") < 0):
154 return False
155
156 self.send_response(200)
157 self.send_header('Content-type', 'text/html')
158 self.send_header('Cache-Control', 'max-age=0')
159 self.end_headers()
160 self.wfile.write("Time to die")
161 self.server.stop = True
162
163 return True
164
165 def NoCacheMaxAgeTimeHandler(self):
166 """This request handler yields a page with the title set to the current
167 system time, and no caching requested."""
168
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000169 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000170 return False
171
172 self.send_response(200)
173 self.send_header('Cache-Control', 'max-age=0')
174 self.send_header('Content-type', 'text/html')
175 self.end_headers()
176
177 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
178
179 return True
180
181 def NoCacheTimeHandler(self):
182 """This request handler yields a page with the title set to the current
183 system time, and no caching requested."""
184
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000185 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000186 return False
187
188 self.send_response(200)
189 self.send_header('Cache-Control', 'no-cache')
190 self.send_header('Content-type', 'text/html')
191 self.end_headers()
192
193 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
194
195 return True
196
197 def CacheTimeHandler(self):
198 """This request handler yields a page with the title set to the current
199 system time, and allows caching for one minute."""
200
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000201 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000202 return False
203
204 self.send_response(200)
205 self.send_header('Cache-Control', 'max-age=60')
206 self.send_header('Content-type', 'text/html')
207 self.end_headers()
208
209 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
210
211 return True
212
213 def CacheExpiresHandler(self):
214 """This request handler yields a page with the title set to the current
215 system time, and set the page to expire on 1 Jan 2099."""
216
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000217 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000218 return False
219
220 self.send_response(200)
221 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
222 self.send_header('Content-type', 'text/html')
223 self.end_headers()
224
225 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
226
227 return True
228
229 def CacheProxyRevalidateHandler(self):
230 """This request handler yields a page with the title set to the current
231 system time, and allows caching for 60 seconds"""
232
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000233 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000234 return False
235
236 self.send_response(200)
237 self.send_header('Content-type', 'text/html')
238 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
239 self.end_headers()
240
241 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
242
243 return True
244
245 def CachePrivateHandler(self):
246 """This request handler yields a page with the title set to the current
247 system time, and allows caching for 5 seconds."""
248
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000249 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000250 return False
251
252 self.send_response(200)
253 self.send_header('Content-type', 'text/html')
254 self.send_header('Cache-Control', 'max-age=5, private')
255 self.end_headers()
256
257 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
258
259 return True
260
261 def CachePublicHandler(self):
262 """This request handler yields a page with the title set to the current
263 system time, and allows caching for 5 seconds."""
264
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000265 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000266 return False
267
268 self.send_response(200)
269 self.send_header('Content-type', 'text/html')
270 self.send_header('Cache-Control', 'max-age=5, public')
271 self.end_headers()
272
273 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
274
275 return True
276
277 def CacheSMaxAgeHandler(self):
278 """This request handler yields a page with the title set to the current
279 system time, and does not allow for caching."""
280
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000281 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000282 return False
283
284 self.send_response(200)
285 self.send_header('Content-type', 'text/html')
286 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
287 self.end_headers()
288
289 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
290
291 return True
292
293 def CacheMustRevalidateHandler(self):
294 """This request handler yields a page with the title set to the current
295 system time, and does not allow caching."""
296
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000297 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000298 return False
299
300 self.send_response(200)
301 self.send_header('Content-type', 'text/html')
302 self.send_header('Cache-Control', 'must-revalidate')
303 self.end_headers()
304
305 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
306
307 return True
308
309 def CacheMustRevalidateMaxAgeHandler(self):
310 """This request handler yields a page with the title set to the current
311 system time, and does not allow caching event though max-age of 60
312 seconds is specified."""
313
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000314 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000315 return False
316
317 self.send_response(200)
318 self.send_header('Content-type', 'text/html')
319 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
320 self.end_headers()
321
322 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
323
324 return True
325
initial.commit94958cf2008-07-26 22:42:52 +0000326 def CacheNoStoreHandler(self):
327 """This request handler yields a page with the title set to the current
328 system time, and does not allow the page to be stored."""
329
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000330 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000331 return False
332
333 self.send_response(200)
334 self.send_header('Content-type', 'text/html')
335 self.send_header('Cache-Control', 'no-store')
336 self.end_headers()
337
338 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
339
340 return True
341
342 def CacheNoStoreMaxAgeHandler(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 even though max-age
345 of 60 seconds is specified."""
346
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000347 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000348 return False
349
350 self.send_response(200)
351 self.send_header('Content-type', 'text/html')
352 self.send_header('Cache-Control', 'max-age=60, no-store')
353 self.end_headers()
354
355 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
356
357 return True
358
359
360 def CacheNoTransformHandler(self):
361 """This request handler yields a page with the title set to the current
362 system time, and does not allow the content to transformed during
363 user-agent caching"""
364
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000365 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000366 return False
367
368 self.send_response(200)
369 self.send_header('Content-type', 'text/html')
370 self.send_header('Cache-Control', 'no-transform')
371 self.end_headers()
372
373 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
374
375 return True
376
377 def EchoHeader(self):
378 """This handler echoes back the value of a specific request header."""
379
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000380 if not self._ShouldHandleRequest("/echoheader"):
initial.commit94958cf2008-07-26 22:42:52 +0000381 return False
382
383 query_char = self.path.find('?')
384 if query_char != -1:
385 header_name = self.path[query_char+1:]
386
387 self.send_response(200)
388 self.send_header('Content-type', 'text/plain')
389 self.send_header('Cache-control', 'max-age=60000')
390 # insert a vary header to properly indicate that the cachability of this
391 # request is subject to value of the request header being echoed.
392 if len(header_name) > 0:
393 self.send_header('Vary', header_name)
394 self.end_headers()
395
396 if len(header_name) > 0:
397 self.wfile.write(self.headers.getheader(header_name))
398
399 return True
400
401 def EchoHandler(self):
402 """This handler just echoes back the payload of the request, for testing
403 form submission."""
404
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000405 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000406 return False
407
408 self.send_response(200)
409 self.send_header('Content-type', 'text/html')
410 self.end_headers()
411 length = int(self.headers.getheader('content-length'))
412 request = self.rfile.read(length)
413 self.wfile.write(request)
414 return True
415
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000416 def WriteFile(self):
417 """This is handler dumps the content of POST request to a disk file into
418 the data_dir/dump. Sub-directories are not supported."""
maruel@chromium.org756cf982009-03-05 12:46:38 +0000419
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000420 prefix='/writefile/'
421 if not self.path.startswith(prefix):
422 return False
maruel@chromium.org756cf982009-03-05 12:46:38 +0000423
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000424 file_name = self.path[len(prefix):]
425
426 # do not allow fancy chars in file name
427 re.sub('[^a-zA-Z0-9_.-]+', '', file_name)
428 if len(file_name) and file_name[0] != '.':
429 path = os.path.join(self.server.data_dir, 'dump', file_name);
430 length = int(self.headers.getheader('content-length'))
431 request = self.rfile.read(length)
432 f = open(path, "wb")
433 f.write(request);
434 f.close()
maruel@chromium.org756cf982009-03-05 12:46:38 +0000435
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000436 self.send_response(200)
437 self.send_header('Content-type', 'text/html')
438 self.end_headers()
439 self.wfile.write('<html>%s</html>' % file_name)
440 return True
maruel@chromium.org756cf982009-03-05 12:46:38 +0000441
initial.commit94958cf2008-07-26 22:42:52 +0000442 def EchoTitleHandler(self):
443 """This handler is like Echo, but sets the page title to the request."""
444
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000445 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000446 return False
447
448 self.send_response(200)
449 self.send_header('Content-type', 'text/html')
450 self.end_headers()
451 length = int(self.headers.getheader('content-length'))
452 request = self.rfile.read(length)
453 self.wfile.write('<html><head><title>')
454 self.wfile.write(request)
455 self.wfile.write('</title></head></html>')
456 return True
457
458 def EchoAllHandler(self):
459 """This handler yields a (more) human-readable page listing information
460 about the request header & contents."""
461
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000462 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000463 return False
464
465 self.send_response(200)
466 self.send_header('Content-type', 'text/html')
467 self.end_headers()
468 self.wfile.write('<html><head><style>'
469 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
470 '</style></head><body>'
471 '<div style="float: right">'
472 '<a href="http://localhost:8888/echo">back to referring page</a></div>'
473 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000474
ericroman@google.coma47622b2008-11-15 04:36:51 +0000475 if self.command == 'POST':
476 length = int(self.headers.getheader('content-length'))
477 qs = self.rfile.read(length)
478 params = cgi.parse_qs(qs, keep_blank_values=1)
479
480 for param in params:
481 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000482
483 self.wfile.write('</pre>')
484
485 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
486
487 self.wfile.write('</body></html>')
488 return True
489
490 def DownloadHandler(self):
491 """This handler sends a downloadable file with or without reporting
492 the size (6K)."""
493
494 if self.path.startswith("/download-unknown-size"):
495 send_length = False
496 elif self.path.startswith("/download-known-size"):
497 send_length = True
498 else:
499 return False
500
501 #
502 # The test which uses this functionality is attempting to send
503 # small chunks of data to the client. Use a fairly large buffer
504 # so that we'll fill chrome's IO buffer enough to force it to
505 # actually write the data.
506 # See also the comments in the client-side of this test in
507 # download_uitest.cc
508 #
509 size_chunk1 = 35*1024
510 size_chunk2 = 10*1024
511
512 self.send_response(200)
513 self.send_header('Content-type', 'application/octet-stream')
514 self.send_header('Cache-Control', 'max-age=0')
515 if send_length:
516 self.send_header('Content-Length', size_chunk1 + size_chunk2)
517 self.end_headers()
518
519 # First chunk of data:
520 self.wfile.write("*" * size_chunk1)
521 self.wfile.flush()
522
523 # handle requests until one of them clears this flag.
524 self.server.waitForDownload = True
525 while self.server.waitForDownload:
526 self.server.handle_request()
527
528 # Second chunk of data:
529 self.wfile.write("*" * size_chunk2)
530 return True
531
532 def DownloadFinishHandler(self):
533 """This handler just tells the server to finish the current download."""
534
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000535 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000536 return False
537
538 self.server.waitForDownload = False
539 self.send_response(200)
540 self.send_header('Content-type', 'text/html')
541 self.send_header('Cache-Control', 'max-age=0')
542 self.end_headers()
543 return True
544
545 def FileHandler(self):
546 """This handler sends the contents of the requested file. Wow, it's like
547 a real webserver!"""
548
549 prefix='/files/'
550 if not self.path.startswith(prefix):
551 return False
552
553 file = self.path[len(prefix):]
554 entries = file.split('/');
555 path = os.path.join(self.server.data_dir, *entries)
556
557 if not os.path.isfile(path):
558 print "File not found " + file + " full path:" + path
559 self.send_error(404)
560 return True
561
562 f = open(path, "rb")
563 data = f.read()
564 f.close()
565
566 # If file.mock-http-headers exists, it contains the headers we
567 # should send. Read them in and parse them.
568 headers_path = path + '.mock-http-headers'
569 if os.path.isfile(headers_path):
570 f = open(headers_path, "r")
571
572 # "HTTP/1.1 200 OK"
573 response = f.readline()
574 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
575 self.send_response(int(status_code))
576
577 for line in f:
578 # "name: value"
579 name, value = re.findall('(\S+):\s*(.*)', line)[0]
580 self.send_header(name, value)
581 f.close()
582 else:
583 # Could be more generic once we support mime-type sniffing, but for
584 # now we need to set it explicitly.
585 self.send_response(200)
586 self.send_header('Content-type', self.GetMIMETypeFromName(file))
587 self.send_header('Content-Length', len(data))
588 self.end_headers()
589
590 self.wfile.write(data)
591
592 return True
593
594 def RealFileWithCommonHeaderHandler(self):
595 """This handler sends the contents of the requested file without the pseudo
596 http head!"""
597
598 prefix='/realfiles/'
599 if not self.path.startswith(prefix):
600 return False
601
602 file = self.path[len(prefix):]
603 path = os.path.join(self.server.data_dir, file)
604
605 try:
606 f = open(path, "rb")
607 data = f.read()
608 f.close()
609
610 # just simply set the MIME as octal stream
611 self.send_response(200)
612 self.send_header('Content-type', 'application/octet-stream')
613 self.end_headers()
614
615 self.wfile.write(data)
616 except:
617 self.send_error(404)
618
619 return True
620
621 def RealBZ2FileWithCommonHeaderHandler(self):
622 """This handler sends the bzip2 contents of the requested file with
623 corresponding Content-Encoding field in http head!"""
624
625 prefix='/realbz2files/'
626 if not self.path.startswith(prefix):
627 return False
628
629 parts = self.path.split('?')
630 file = parts[0][len(prefix):]
631 path = os.path.join(self.server.data_dir, file) + '.bz2'
632
633 if len(parts) > 1:
634 options = parts[1]
635 else:
636 options = ''
637
638 try:
639 self.send_response(200)
640 accept_encoding = self.headers.get("Accept-Encoding")
641 if accept_encoding.find("bzip2") != -1:
642 f = open(path, "rb")
643 data = f.read()
644 f.close()
645 self.send_header('Content-Encoding', 'bzip2')
646 self.send_header('Content-type', 'application/x-bzip2')
647 self.end_headers()
648 if options == 'incremental-header':
649 self.wfile.write(data[:1])
650 self.wfile.flush()
651 time.sleep(1.0)
652 self.wfile.write(data[1:])
653 else:
654 self.wfile.write(data)
655 else:
656 """client do not support bzip2 format, send pseudo content
657 """
658 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
659 self.end_headers()
660 self.wfile.write("you do not support bzip2 encoding")
661 except:
662 self.send_error(404)
663
664 return True
665
666 def AuthBasicHandler(self):
667 """This handler tests 'Basic' authentication. It just sends a page with
668 title 'user/pass' if you succeed."""
669
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000670 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000671 return False
672
673 username = userpass = password = b64str = ""
674
675 auth = self.headers.getheader('authorization')
676 try:
677 if not auth:
678 raise Exception('no auth')
679 b64str = re.findall(r'Basic (\S+)', auth)[0]
680 userpass = base64.b64decode(b64str)
681 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
682 if password != 'secret':
683 raise Exception('wrong password')
684 except Exception, e:
685 # Authentication failed.
686 self.send_response(401)
687 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
688 self.send_header('Content-type', 'text/html')
689 self.end_headers()
690 self.wfile.write('<html><head>')
691 self.wfile.write('<title>Denied: %s</title>' % e)
692 self.wfile.write('</head><body>')
693 self.wfile.write('auth=%s<p>' % auth)
694 self.wfile.write('b64str=%s<p>' % b64str)
695 self.wfile.write('username: %s<p>' % username)
696 self.wfile.write('userpass: %s<p>' % userpass)
697 self.wfile.write('password: %s<p>' % password)
698 self.wfile.write('You sent:<br>%s<p>' % self.headers)
699 self.wfile.write('</body></html>')
700 return True
701
702 # Authentication successful. (Return a cachable response to allow for
703 # testing cached pages that require authentication.)
704 if_none_match = self.headers.getheader('if-none-match')
705 if if_none_match == "abc":
706 self.send_response(304)
707 self.end_headers()
708 else:
709 self.send_response(200)
710 self.send_header('Content-type', 'text/html')
711 self.send_header('Cache-control', 'max-age=60000')
712 self.send_header('Etag', 'abc')
713 self.end_headers()
714 self.wfile.write('<html><head>')
715 self.wfile.write('<title>%s/%s</title>' % (username, password))
716 self.wfile.write('</head><body>')
717 self.wfile.write('auth=%s<p>' % auth)
718 self.wfile.write('</body></html>')
719
720 return True
721
722 def AuthDigestHandler(self):
723 """This handler tests 'Digest' authentication. It just sends a page with
724 title 'user/pass' if you succeed."""
725
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000726 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000727 return False
728
729 # Periodically generate a new nonce. Technically we should incorporate
730 # the request URL into this, but we don't care for testing.
731 nonce_life = 10
732 stale = False
733 if not self.server.nonce or (time.time() - self.server.nonce_time > nonce_life):
734 if self.server.nonce:
735 stale = True
736 self.server.nonce_time = time.time()
737 self.server.nonce = \
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000738 _new_md5(time.ctime(self.server.nonce_time) + 'privatekey').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000739
740 nonce = self.server.nonce
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000741 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000742 password = 'secret'
743 realm = 'testrealm'
744
745 auth = self.headers.getheader('authorization')
746 pairs = {}
747 try:
748 if not auth:
749 raise Exception('no auth')
750 if not auth.startswith('Digest'):
751 raise Exception('not digest')
752 # Pull out all the name="value" pairs as a dictionary.
753 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
754
755 # Make sure it's all valid.
756 if pairs['nonce'] != nonce:
757 raise Exception('wrong nonce')
758 if pairs['opaque'] != opaque:
759 raise Exception('wrong opaque')
760
761 # Check the 'response' value and make sure it matches our magic hash.
762 # See http://www.ietf.org/rfc/rfc2617.txt
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000763 hash_a1 = _new_md5(':'.join([pairs['username'], realm, password])).hexdigest()
764 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000765 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000766 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000767 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
768 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000769 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000770
771 if pairs['response'] != response:
772 raise Exception('wrong password')
773 except Exception, e:
774 # Authentication failed.
775 self.send_response(401)
776 hdr = ('Digest '
777 'realm="%s", '
778 'domain="/", '
779 'qop="auth", '
780 'algorithm=MD5, '
781 'nonce="%s", '
782 'opaque="%s"') % (realm, nonce, opaque)
783 if stale:
784 hdr += ', stale="TRUE"'
785 self.send_header('WWW-Authenticate', hdr)
786 self.send_header('Content-type', 'text/html')
787 self.end_headers()
788 self.wfile.write('<html><head>')
789 self.wfile.write('<title>Denied: %s</title>' % e)
790 self.wfile.write('</head><body>')
791 self.wfile.write('auth=%s<p>' % auth)
792 self.wfile.write('pairs=%s<p>' % pairs)
793 self.wfile.write('You sent:<br>%s<p>' % self.headers)
794 self.wfile.write('We are replying:<br>%s<p>' % hdr)
795 self.wfile.write('</body></html>')
796 return True
797
798 # Authentication successful.
799 self.send_response(200)
800 self.send_header('Content-type', 'text/html')
801 self.end_headers()
802 self.wfile.write('<html><head>')
803 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
804 self.wfile.write('</head><body>')
805 self.wfile.write('auth=%s<p>' % auth)
806 self.wfile.write('pairs=%s<p>' % pairs)
807 self.wfile.write('</body></html>')
808
809 return True
810
811 def SlowServerHandler(self):
812 """Wait for the user suggested time before responding. The syntax is
813 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000814 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000815 return False
816 query_char = self.path.find('?')
817 wait_sec = 1.0
818 if query_char >= 0:
819 try:
820 wait_sec = int(self.path[query_char + 1:])
821 except ValueError:
822 pass
823 time.sleep(wait_sec)
824 self.send_response(200)
825 self.send_header('Content-type', 'text/plain')
826 self.end_headers()
827 self.wfile.write("waited %d seconds" % wait_sec)
828 return True
829
830 def ContentTypeHandler(self):
831 """Returns a string of html with the given content type. E.g.,
832 /contenttype?text/css returns an html file with the Content-Type
833 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000834 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +0000835 return False
836 query_char = self.path.find('?')
837 content_type = self.path[query_char + 1:].strip()
838 if not content_type:
839 content_type = 'text/html'
840 self.send_response(200)
841 self.send_header('Content-Type', content_type)
842 self.end_headers()
843 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
844 return True
845
846 def ServerRedirectHandler(self):
847 """Sends a server redirect to the given URL. The syntax is
848 '/server-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
849
850 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000851 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000852 return False
853
854 query_char = self.path.find('?')
855 if query_char < 0 or len(self.path) <= query_char + 1:
856 self.sendRedirectHelp(test_name)
857 return True
858 dest = self.path[query_char + 1:]
859
860 self.send_response(301) # moved permanently
861 self.send_header('Location', dest)
862 self.send_header('Content-type', 'text/html')
863 self.end_headers()
864 self.wfile.write('<html><head>')
865 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
866
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000867 return True
initial.commit94958cf2008-07-26 22:42:52 +0000868
869 def ClientRedirectHandler(self):
870 """Sends a client redirect to the given URL. The syntax is
871 '/client-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
872
873 test_name = "/client-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(200)
884 self.send_header('Content-type', 'text/html')
885 self.end_headers()
886 self.wfile.write('<html><head>')
887 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
888 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
889
890 return True
891
892 def DefaultResponseHandler(self):
893 """This is the catch-all response handler for requests that aren't handled
894 by one of the special handlers above.
895 Note that we specify the content-length as without it the https connection
896 is not closed properly (and the browser keeps expecting data)."""
897
898 contents = "Default response given for path: " + self.path
899 self.send_response(200)
900 self.send_header('Content-type', 'text/html')
901 self.send_header("Content-Length", len(contents))
902 self.end_headers()
903 self.wfile.write(contents)
904 return True
905
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000906 def RedirectConnectHandler(self):
907 """Sends a redirect to the CONNECT request for www.redirect.com. This
908 response is not specified by the RFC, so the browser should not follow
909 the redirect."""
910
911 if (self.path.find("www.redirect.com") < 0):
912 return False
913
914 dest = "http://www.destination.com/foo.js"
915
916 self.send_response(302) # moved temporarily
917 self.send_header('Location', dest)
918 self.send_header('Connection', 'close')
919 self.end_headers()
920 return True
921
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000922 def ServerAuthConnectHandler(self):
923 """Sends a 401 to the CONNECT request for www.server-auth.com. This
924 response doesn't make sense because the proxy server cannot request
925 server authentication."""
926
927 if (self.path.find("www.server-auth.com") < 0):
928 return False
929
930 challenge = 'Basic realm="WallyWorld"'
931
932 self.send_response(401) # unauthorized
933 self.send_header('WWW-Authenticate', challenge)
934 self.send_header('Connection', 'close')
935 self.end_headers()
936 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000937
938 def DefaultConnectResponseHandler(self):
939 """This is the catch-all response handler for CONNECT requests that aren't
940 handled by one of the special handlers above. Real Web servers respond
941 with 400 to CONNECT requests."""
942
943 contents = "Your client has issued a malformed or illegal request."
944 self.send_response(400) # bad request
945 self.send_header('Content-type', 'text/html')
946 self.send_header("Content-Length", len(contents))
947 self.end_headers()
948 self.wfile.write(contents)
949 return True
950
951 def do_CONNECT(self):
952 for handler in self._connect_handlers:
953 if handler():
954 return
955
initial.commit94958cf2008-07-26 22:42:52 +0000956 def do_GET(self):
957 for handler in self._get_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000958 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000959 return
960
961 def do_POST(self):
962 for handler in self._post_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000963 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000964 return
965
966 # called by the redirect handling function when there is no parameter
967 def sendRedirectHelp(self, redirect_name):
968 self.send_response(200)
969 self.send_header('Content-type', 'text/html')
970 self.end_headers()
971 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
972 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
973 self.wfile.write('</body></html>')
974
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000975def MakeDumpDir(data_dir):
976 """Create directory named 'dump' where uploaded data via HTTP POST request
977 will be stored. If the directory already exists all files and subdirectories
978 will be deleted."""
979 dump_dir = os.path.join(data_dir, 'dump');
980 if os.path.isdir(dump_dir):
981 shutil.rmtree(dump_dir)
982 os.mkdir(dump_dir)
983
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000984def MakeDataDir():
985 if options.data_dir:
986 if not os.path.isdir(options.data_dir):
987 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
988 return None
989 my_data_dir = options.data_dir
990 else:
991 # Create the default path to our data dir, relative to the exe dir.
992 my_data_dir = os.path.dirname(sys.argv[0])
993 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
994 "test", "data")
995
996 #TODO(ibrar): Must use Find* funtion defined in google\tools
997 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
998
999 return my_data_dir
1000
initial.commit94958cf2008-07-26 22:42:52 +00001001def main(options, args):
1002 # redirect output to a log file so it doesn't spam the unit test output
1003 logfile = open('testserver.log', 'w')
1004 sys.stderr = sys.stdout = logfile
1005
1006 port = options.port
1007
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001008 if options.server_type == SERVER_HTTP:
1009 if options.cert:
1010 # let's make sure the cert file exists.
1011 if not os.path.isfile(options.cert):
1012 print 'specified cert file not found: ' + options.cert + ' exiting...'
1013 return
1014 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
1015 print 'HTTPS server started on port %d...' % port
1016 else:
1017 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
1018 print 'HTTP server started on port %d...' % port
erikkay@google.com70397b62008-12-30 21:49:21 +00001019
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001020 server.data_dir = MakeDataDir()
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001021 MakeDumpDir(server.data_dir)
maruel@chromium.org756cf982009-03-05 12:46:38 +00001022
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001023 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001024 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001025 my_data_dir = MakeDataDir()
1026
1027 def line_logger(msg):
1028 if (msg.find("kill") >= 0):
1029 server.stop = True
1030 print 'shutting down server'
1031 sys.exit(0)
1032
1033 # Instantiate a dummy authorizer for managing 'virtual' users
1034 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1035
1036 # Define a new user having full r/w permissions and a read-only
1037 # anonymous user
1038 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1039
1040 authorizer.add_anonymous(my_data_dir)
1041
1042 # Instantiate FTP handler class
1043 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1044 ftp_handler.authorizer = authorizer
1045 pyftpdlib.ftpserver.logline = line_logger
1046
1047 # Define a customized banner (string returned when client connects)
1048 ftp_handler.banner = "pyftpdlib %s based ftpd ready." % pyftpdlib.ftpserver.__ver__
1049
1050 # Instantiate FTP server class and listen to 127.0.0.1:port
1051 address = ('127.0.0.1', port)
1052 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
1053 print 'FTP server started on port %d...' % port
initial.commit94958cf2008-07-26 22:42:52 +00001054
1055 try:
1056 server.serve_forever()
1057 except KeyboardInterrupt:
1058 print 'shutting down server'
1059 server.stop = True
1060
1061if __name__ == '__main__':
1062 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001063 option_parser.add_option("-f", '--ftp', action='store_const',
1064 const=SERVER_FTP, default=SERVER_HTTP,
1065 dest='server_type',
1066 help='FTP or HTTP server default HTTP')
initial.commit94958cf2008-07-26 22:42:52 +00001067 option_parser.add_option('', '--port', default='8888', type='int',
1068 help='Port used by the server')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001069 option_parser.add_option('', '--data-dir', dest='data_dir',
initial.commit94958cf2008-07-26 22:42:52 +00001070 help='Directory from which to read the files')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001071 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001072 help='Specify that https should be used, specify '
1073 'the path to the cert containing the private key '
1074 'the server should use')
1075 options, args = option_parser.parse_args()
1076
1077 sys.exit(main(options, args))
maruel@chromium.org756cf982009-03-05 12:46:38 +00001078