blob: 2d3049ef5bdca2ecf6b9b5c72e5e11dbb0be4ad1 [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
18import md5
19import optparse
20import os
21import re
22import SocketServer
23import sys
24import time
25import tlslite
26import tlslite.api
27
28debug_output = sys.stderr
29def debug(str):
30 debug_output.write(str + "\n")
31 debug_output.flush()
32
33class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
34 """This is a specialization of of BaseHTTPServer to allow it
35 to be exited cleanly (by setting its "stop" member to True)."""
36
37 def serve_forever(self):
38 self.stop = False
39 self.nonce = None
40 while not self.stop:
41 self.handle_request()
42 self.socket.close()
43
44class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
45 """This is a specialization of StoppableHTTPerver that add https support."""
46
47 def __init__(self, server_address, request_hander_class, cert_path):
48 s = open(cert_path).read()
49 x509 = tlslite.api.X509()
50 x509.parse(s)
51 self.cert_chain = tlslite.api.X509CertChain([x509])
52 s = open(cert_path).read()
53 self.private_key = tlslite.api.parsePEMKey(s, private=True)
54
55 self.session_cache = tlslite.api.SessionCache()
56 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
57
58 def handshake(self, tlsConnection):
59 """Creates the SSL connection."""
60 try:
61 tlsConnection.handshakeServer(certChain=self.cert_chain,
62 privateKey=self.private_key,
63 sessionCache=self.session_cache)
64 tlsConnection.ignoreAbruptClose = True
65 return True
66 except tlslite.api.TLSError, error:
67 print "Handshake failure:", str(error)
68 return False
69
70class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
71
72 def __init__(self, request, client_address, socket_server):
73 self._get_handlers = [
74 self.KillHandler,
75 self.NoCacheMaxAgeTimeHandler,
76 self.NoCacheTimeHandler,
77 self.CacheTimeHandler,
78 self.CacheExpiresHandler,
79 self.CacheProxyRevalidateHandler,
80 self.CachePrivateHandler,
81 self.CachePublicHandler,
82 self.CacheSMaxAgeHandler,
83 self.CacheMustRevalidateHandler,
84 self.CacheMustRevalidateMaxAgeHandler,
85 self.CacheNoStoreHandler,
86 self.CacheNoStoreMaxAgeHandler,
87 self.CacheNoTransformHandler,
88 self.DownloadHandler,
89 self.DownloadFinishHandler,
90 self.EchoHeader,
ericroman@google.coma47622b2008-11-15 04:36:51 +000091 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +000092 self.FileHandler,
93 self.RealFileWithCommonHeaderHandler,
94 self.RealBZ2FileWithCommonHeaderHandler,
95 self.AuthBasicHandler,
96 self.AuthDigestHandler,
97 self.SlowServerHandler,
98 self.ContentTypeHandler,
99 self.ServerRedirectHandler,
100 self.ClientRedirectHandler,
101 self.DefaultResponseHandler]
102 self._post_handlers = [
103 self.EchoTitleHandler,
104 self.EchoAllHandler,
105 self.EchoHandler] + self._get_handlers
106
107 self._mime_types = { 'gif': 'image/gif', 'jpeg' : 'image/jpeg', 'jpg' : 'image/jpeg' }
108 self._default_mime_type = 'text/html'
109
110 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, socket_server)
111
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000112 def _ShouldHandleRequest(self, handler_name):
113 """Determines if the path can be handled by the handler.
114
115 We consider a handler valid if the path begins with the
116 handler name. It can optionally be followed by "?*", "/*".
117 """
118
119 pattern = re.compile('%s($|\?|/).*' % handler_name)
120 return pattern.match(self.path)
121
initial.commit94958cf2008-07-26 22:42:52 +0000122 def GetMIMETypeFromName(self, file_name):
123 """Returns the mime type for the specified file_name. So far it only looks
124 at the file extension."""
125
126 (shortname, extension) = os.path.splitext(file_name)
127 if len(extension) == 0:
128 # no extension.
129 return self._default_mime_type
130
131 return self._mime_types.get(extension, self._default_mime_type)
132
133 def KillHandler(self):
134 """This request handler kills the server, for use when we're done"
135 with the a particular test."""
136
137 if (self.path.find("kill") < 0):
138 return False
139
140 self.send_response(200)
141 self.send_header('Content-type', 'text/html')
142 self.send_header('Cache-Control', 'max-age=0')
143 self.end_headers()
144 self.wfile.write("Time to die")
145 self.server.stop = True
146
147 return True
148
149 def NoCacheMaxAgeTimeHandler(self):
150 """This request handler yields a page with the title set to the current
151 system time, and no caching requested."""
152
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000153 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000154 return False
155
156 self.send_response(200)
157 self.send_header('Cache-Control', 'max-age=0')
158 self.send_header('Content-type', 'text/html')
159 self.end_headers()
160
161 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
162
163 return True
164
165 def NoCacheTimeHandler(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"):
initial.commit94958cf2008-07-26 22:42:52 +0000170 return False
171
172 self.send_response(200)
173 self.send_header('Cache-Control', 'no-cache')
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 CacheTimeHandler(self):
182 """This request handler yields a page with the title set to the current
183 system time, and allows caching for one minute."""
184
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000185 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000186 return False
187
188 self.send_response(200)
189 self.send_header('Cache-Control', 'max-age=60')
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 CacheExpiresHandler(self):
198 """This request handler yields a page with the title set to the current
199 system time, and set the page to expire on 1 Jan 2099."""
200
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000201 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000202 return False
203
204 self.send_response(200)
205 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
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 CacheProxyRevalidateHandler(self):
214 """This request handler yields a page with the title set to the current
215 system time, and allows caching for 60 seconds"""
216
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000217 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000218 return False
219
220 self.send_response(200)
221 self.send_header('Content-type', 'text/html')
222 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
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 CachePrivateHandler(self):
230 """This request handler yields a page with the title set to the current
231 system time, and allows caching for 5 seconds."""
232
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000233 if not self._ShouldHandleRequest("/cache/private"):
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=5, private')
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 CachePublicHandler(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/public"):
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, public')
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 CacheSMaxAgeHandler(self):
262 """This request handler yields a page with the title set to the current
263 system time, and does not allow for caching."""
264
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000265 if not self._ShouldHandleRequest("/cache/s-maxage"):
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', 'public, s-maxage = 60, max-age = 0')
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 CacheMustRevalidateHandler(self):
278 """This request handler yields a page with the title set to the current
279 system time, and does not allow caching."""
280
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000281 if not self._ShouldHandleRequest("/cache/must-revalidate"):
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', 'must-revalidate')
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 CacheMustRevalidateMaxAgeHandler(self):
294 """This request handler yields a page with the title set to the current
295 system time, and does not allow caching event though max-age of 60
296 seconds is specified."""
297
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000298 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000299 return False
300
301 self.send_response(200)
302 self.send_header('Content-type', 'text/html')
303 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
304 self.end_headers()
305
306 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
307
308 return True
309
initial.commit94958cf2008-07-26 22:42:52 +0000310 def CacheNoStoreHandler(self):
311 """This request handler yields a page with the title set to the current
312 system time, and does not allow the page to be stored."""
313
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000314 if not self._ShouldHandleRequest("/cache/no-store"):
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', 'no-store')
320 self.end_headers()
321
322 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
323
324 return True
325
326 def CacheNoStoreMaxAgeHandler(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 even though max-age
329 of 60 seconds is specified."""
330
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000331 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000332 return False
333
334 self.send_response(200)
335 self.send_header('Content-type', 'text/html')
336 self.send_header('Cache-Control', 'max-age=60, no-store')
337 self.end_headers()
338
339 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
340
341 return True
342
343
344 def CacheNoTransformHandler(self):
345 """This request handler yields a page with the title set to the current
346 system time, and does not allow the content to transformed during
347 user-agent caching"""
348
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000349 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000350 return False
351
352 self.send_response(200)
353 self.send_header('Content-type', 'text/html')
354 self.send_header('Cache-Control', 'no-transform')
355 self.end_headers()
356
357 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
358
359 return True
360
361 def EchoHeader(self):
362 """This handler echoes back the value of a specific request header."""
363
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000364 if not self._ShouldHandleRequest("/echoheader"):
initial.commit94958cf2008-07-26 22:42:52 +0000365 return False
366
367 query_char = self.path.find('?')
368 if query_char != -1:
369 header_name = self.path[query_char+1:]
370
371 self.send_response(200)
372 self.send_header('Content-type', 'text/plain')
373 self.send_header('Cache-control', 'max-age=60000')
374 # insert a vary header to properly indicate that the cachability of this
375 # request is subject to value of the request header being echoed.
376 if len(header_name) > 0:
377 self.send_header('Vary', header_name)
378 self.end_headers()
379
380 if len(header_name) > 0:
381 self.wfile.write(self.headers.getheader(header_name))
382
383 return True
384
385 def EchoHandler(self):
386 """This handler just echoes back the payload of the request, for testing
387 form submission."""
388
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000389 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000390 return False
391
392 self.send_response(200)
393 self.send_header('Content-type', 'text/html')
394 self.end_headers()
395 length = int(self.headers.getheader('content-length'))
396 request = self.rfile.read(length)
397 self.wfile.write(request)
398 return True
399
400 def EchoTitleHandler(self):
401 """This handler is like Echo, but sets the page title to the request."""
402
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000403 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000404 return False
405
406 self.send_response(200)
407 self.send_header('Content-type', 'text/html')
408 self.end_headers()
409 length = int(self.headers.getheader('content-length'))
410 request = self.rfile.read(length)
411 self.wfile.write('<html><head><title>')
412 self.wfile.write(request)
413 self.wfile.write('</title></head></html>')
414 return True
415
416 def EchoAllHandler(self):
417 """This handler yields a (more) human-readable page listing information
418 about the request header & contents."""
419
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000420 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000421 return False
422
423 self.send_response(200)
424 self.send_header('Content-type', 'text/html')
425 self.end_headers()
426 self.wfile.write('<html><head><style>'
427 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
428 '</style></head><body>'
429 '<div style="float: right">'
430 '<a href="http://localhost:8888/echo">back to referring page</a></div>'
431 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000432
ericroman@google.coma47622b2008-11-15 04:36:51 +0000433 if self.command == 'POST':
434 length = int(self.headers.getheader('content-length'))
435 qs = self.rfile.read(length)
436 params = cgi.parse_qs(qs, keep_blank_values=1)
437
438 for param in params:
439 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000440
441 self.wfile.write('</pre>')
442
443 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
444
445 self.wfile.write('</body></html>')
446 return True
447
448 def DownloadHandler(self):
449 """This handler sends a downloadable file with or without reporting
450 the size (6K)."""
451
452 if self.path.startswith("/download-unknown-size"):
453 send_length = False
454 elif self.path.startswith("/download-known-size"):
455 send_length = True
456 else:
457 return False
458
459 #
460 # The test which uses this functionality is attempting to send
461 # small chunks of data to the client. Use a fairly large buffer
462 # so that we'll fill chrome's IO buffer enough to force it to
463 # actually write the data.
464 # See also the comments in the client-side of this test in
465 # download_uitest.cc
466 #
467 size_chunk1 = 35*1024
468 size_chunk2 = 10*1024
469
470 self.send_response(200)
471 self.send_header('Content-type', 'application/octet-stream')
472 self.send_header('Cache-Control', 'max-age=0')
473 if send_length:
474 self.send_header('Content-Length', size_chunk1 + size_chunk2)
475 self.end_headers()
476
477 # First chunk of data:
478 self.wfile.write("*" * size_chunk1)
479 self.wfile.flush()
480
481 # handle requests until one of them clears this flag.
482 self.server.waitForDownload = True
483 while self.server.waitForDownload:
484 self.server.handle_request()
485
486 # Second chunk of data:
487 self.wfile.write("*" * size_chunk2)
488 return True
489
490 def DownloadFinishHandler(self):
491 """This handler just tells the server to finish the current download."""
492
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000493 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000494 return False
495
496 self.server.waitForDownload = False
497 self.send_response(200)
498 self.send_header('Content-type', 'text/html')
499 self.send_header('Cache-Control', 'max-age=0')
500 self.end_headers()
501 return True
502
503 def FileHandler(self):
504 """This handler sends the contents of the requested file. Wow, it's like
505 a real webserver!"""
506
507 prefix='/files/'
508 if not self.path.startswith(prefix):
509 return False
510
511 file = self.path[len(prefix):]
512 entries = file.split('/');
513 path = os.path.join(self.server.data_dir, *entries)
514
515 if not os.path.isfile(path):
516 print "File not found " + file + " full path:" + path
517 self.send_error(404)
518 return True
519
520 f = open(path, "rb")
521 data = f.read()
522 f.close()
523
524 # If file.mock-http-headers exists, it contains the headers we
525 # should send. Read them in and parse them.
526 headers_path = path + '.mock-http-headers'
527 if os.path.isfile(headers_path):
528 f = open(headers_path, "r")
529
530 # "HTTP/1.1 200 OK"
531 response = f.readline()
532 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
533 self.send_response(int(status_code))
534
535 for line in f:
536 # "name: value"
537 name, value = re.findall('(\S+):\s*(.*)', line)[0]
538 self.send_header(name, value)
539 f.close()
540 else:
541 # Could be more generic once we support mime-type sniffing, but for
542 # now we need to set it explicitly.
543 self.send_response(200)
544 self.send_header('Content-type', self.GetMIMETypeFromName(file))
545 self.send_header('Content-Length', len(data))
546 self.end_headers()
547
548 self.wfile.write(data)
549
550 return True
551
552 def RealFileWithCommonHeaderHandler(self):
553 """This handler sends the contents of the requested file without the pseudo
554 http head!"""
555
556 prefix='/realfiles/'
557 if not self.path.startswith(prefix):
558 return False
559
560 file = self.path[len(prefix):]
561 path = os.path.join(self.server.data_dir, file)
562
563 try:
564 f = open(path, "rb")
565 data = f.read()
566 f.close()
567
568 # just simply set the MIME as octal stream
569 self.send_response(200)
570 self.send_header('Content-type', 'application/octet-stream')
571 self.end_headers()
572
573 self.wfile.write(data)
574 except:
575 self.send_error(404)
576
577 return True
578
579 def RealBZ2FileWithCommonHeaderHandler(self):
580 """This handler sends the bzip2 contents of the requested file with
581 corresponding Content-Encoding field in http head!"""
582
583 prefix='/realbz2files/'
584 if not self.path.startswith(prefix):
585 return False
586
587 parts = self.path.split('?')
588 file = parts[0][len(prefix):]
589 path = os.path.join(self.server.data_dir, file) + '.bz2'
590
591 if len(parts) > 1:
592 options = parts[1]
593 else:
594 options = ''
595
596 try:
597 self.send_response(200)
598 accept_encoding = self.headers.get("Accept-Encoding")
599 if accept_encoding.find("bzip2") != -1:
600 f = open(path, "rb")
601 data = f.read()
602 f.close()
603 self.send_header('Content-Encoding', 'bzip2')
604 self.send_header('Content-type', 'application/x-bzip2')
605 self.end_headers()
606 if options == 'incremental-header':
607 self.wfile.write(data[:1])
608 self.wfile.flush()
609 time.sleep(1.0)
610 self.wfile.write(data[1:])
611 else:
612 self.wfile.write(data)
613 else:
614 """client do not support bzip2 format, send pseudo content
615 """
616 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
617 self.end_headers()
618 self.wfile.write("you do not support bzip2 encoding")
619 except:
620 self.send_error(404)
621
622 return True
623
624 def AuthBasicHandler(self):
625 """This handler tests 'Basic' authentication. It just sends a page with
626 title 'user/pass' if you succeed."""
627
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000628 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000629 return False
630
631 username = userpass = password = b64str = ""
632
633 auth = self.headers.getheader('authorization')
634 try:
635 if not auth:
636 raise Exception('no auth')
637 b64str = re.findall(r'Basic (\S+)', auth)[0]
638 userpass = base64.b64decode(b64str)
639 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
640 if password != 'secret':
641 raise Exception('wrong password')
642 except Exception, e:
643 # Authentication failed.
644 self.send_response(401)
645 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
646 self.send_header('Content-type', 'text/html')
647 self.end_headers()
648 self.wfile.write('<html><head>')
649 self.wfile.write('<title>Denied: %s</title>' % e)
650 self.wfile.write('</head><body>')
651 self.wfile.write('auth=%s<p>' % auth)
652 self.wfile.write('b64str=%s<p>' % b64str)
653 self.wfile.write('username: %s<p>' % username)
654 self.wfile.write('userpass: %s<p>' % userpass)
655 self.wfile.write('password: %s<p>' % password)
656 self.wfile.write('You sent:<br>%s<p>' % self.headers)
657 self.wfile.write('</body></html>')
658 return True
659
660 # Authentication successful. (Return a cachable response to allow for
661 # testing cached pages that require authentication.)
662 if_none_match = self.headers.getheader('if-none-match')
663 if if_none_match == "abc":
664 self.send_response(304)
665 self.end_headers()
666 else:
667 self.send_response(200)
668 self.send_header('Content-type', 'text/html')
669 self.send_header('Cache-control', 'max-age=60000')
670 self.send_header('Etag', 'abc')
671 self.end_headers()
672 self.wfile.write('<html><head>')
673 self.wfile.write('<title>%s/%s</title>' % (username, password))
674 self.wfile.write('</head><body>')
675 self.wfile.write('auth=%s<p>' % auth)
676 self.wfile.write('</body></html>')
677
678 return True
679
680 def AuthDigestHandler(self):
681 """This handler tests 'Digest' authentication. It just sends a page with
682 title 'user/pass' if you succeed."""
683
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000684 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000685 return False
686
687 # Periodically generate a new nonce. Technically we should incorporate
688 # the request URL into this, but we don't care for testing.
689 nonce_life = 10
690 stale = False
691 if not self.server.nonce or (time.time() - self.server.nonce_time > nonce_life):
692 if self.server.nonce:
693 stale = True
694 self.server.nonce_time = time.time()
695 self.server.nonce = \
696 md5.new(time.ctime(self.server.nonce_time) + 'privatekey').hexdigest()
697
698 nonce = self.server.nonce
699 opaque = md5.new('opaque').hexdigest()
700 password = 'secret'
701 realm = 'testrealm'
702
703 auth = self.headers.getheader('authorization')
704 pairs = {}
705 try:
706 if not auth:
707 raise Exception('no auth')
708 if not auth.startswith('Digest'):
709 raise Exception('not digest')
710 # Pull out all the name="value" pairs as a dictionary.
711 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
712
713 # Make sure it's all valid.
714 if pairs['nonce'] != nonce:
715 raise Exception('wrong nonce')
716 if pairs['opaque'] != opaque:
717 raise Exception('wrong opaque')
718
719 # Check the 'response' value and make sure it matches our magic hash.
720 # See http://www.ietf.org/rfc/rfc2617.txt
721 hash_a1 = md5.new(':'.join([pairs['username'], realm, password])).hexdigest()
722 hash_a2 = md5.new(':'.join([self.command, pairs['uri']])).hexdigest()
723 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
724 response = md5.new(':'.join([hash_a1, nonce, pairs['nc'],
725 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
726 else:
727 response = md5.new(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
728
729 if pairs['response'] != response:
730 raise Exception('wrong password')
731 except Exception, e:
732 # Authentication failed.
733 self.send_response(401)
734 hdr = ('Digest '
735 'realm="%s", '
736 'domain="/", '
737 'qop="auth", '
738 'algorithm=MD5, '
739 'nonce="%s", '
740 'opaque="%s"') % (realm, nonce, opaque)
741 if stale:
742 hdr += ', stale="TRUE"'
743 self.send_header('WWW-Authenticate', hdr)
744 self.send_header('Content-type', 'text/html')
745 self.end_headers()
746 self.wfile.write('<html><head>')
747 self.wfile.write('<title>Denied: %s</title>' % e)
748 self.wfile.write('</head><body>')
749 self.wfile.write('auth=%s<p>' % auth)
750 self.wfile.write('pairs=%s<p>' % pairs)
751 self.wfile.write('You sent:<br>%s<p>' % self.headers)
752 self.wfile.write('We are replying:<br>%s<p>' % hdr)
753 self.wfile.write('</body></html>')
754 return True
755
756 # Authentication successful.
757 self.send_response(200)
758 self.send_header('Content-type', 'text/html')
759 self.end_headers()
760 self.wfile.write('<html><head>')
761 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
762 self.wfile.write('</head><body>')
763 self.wfile.write('auth=%s<p>' % auth)
764 self.wfile.write('pairs=%s<p>' % pairs)
765 self.wfile.write('</body></html>')
766
767 return True
768
769 def SlowServerHandler(self):
770 """Wait for the user suggested time before responding. The syntax is
771 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000772 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000773 return False
774 query_char = self.path.find('?')
775 wait_sec = 1.0
776 if query_char >= 0:
777 try:
778 wait_sec = int(self.path[query_char + 1:])
779 except ValueError:
780 pass
781 time.sleep(wait_sec)
782 self.send_response(200)
783 self.send_header('Content-type', 'text/plain')
784 self.end_headers()
785 self.wfile.write("waited %d seconds" % wait_sec)
786 return True
787
788 def ContentTypeHandler(self):
789 """Returns a string of html with the given content type. E.g.,
790 /contenttype?text/css returns an html file with the Content-Type
791 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000792 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +0000793 return False
794 query_char = self.path.find('?')
795 content_type = self.path[query_char + 1:].strip()
796 if not content_type:
797 content_type = 'text/html'
798 self.send_response(200)
799 self.send_header('Content-Type', content_type)
800 self.end_headers()
801 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
802 return True
803
804 def ServerRedirectHandler(self):
805 """Sends a server redirect to the given URL. The syntax is
806 '/server-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
807
808 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000809 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000810 return False
811
812 query_char = self.path.find('?')
813 if query_char < 0 or len(self.path) <= query_char + 1:
814 self.sendRedirectHelp(test_name)
815 return True
816 dest = self.path[query_char + 1:]
817
818 self.send_response(301) # moved permanently
819 self.send_header('Location', dest)
820 self.send_header('Content-type', 'text/html')
821 self.end_headers()
822 self.wfile.write('<html><head>')
823 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
824
825 return True;
826
827 def ClientRedirectHandler(self):
828 """Sends a client redirect to the given URL. The syntax is
829 '/client-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
830
831 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000832 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000833 return False
834
835 query_char = self.path.find('?');
836 if query_char < 0 or len(self.path) <= query_char + 1:
837 self.sendRedirectHelp(test_name)
838 return True
839 dest = self.path[query_char + 1:]
840
841 self.send_response(200)
842 self.send_header('Content-type', 'text/html')
843 self.end_headers()
844 self.wfile.write('<html><head>')
845 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
846 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
847
848 return True
849
850 def DefaultResponseHandler(self):
851 """This is the catch-all response handler for requests that aren't handled
852 by one of the special handlers above.
853 Note that we specify the content-length as without it the https connection
854 is not closed properly (and the browser keeps expecting data)."""
855
856 contents = "Default response given for path: " + self.path
857 self.send_response(200)
858 self.send_header('Content-type', 'text/html')
859 self.send_header("Content-Length", len(contents))
860 self.end_headers()
861 self.wfile.write(contents)
862 return True
863
864 def do_GET(self):
865 for handler in self._get_handlers:
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000866 if (handler()):
initial.commit94958cf2008-07-26 22:42:52 +0000867 return
868
869 def do_POST(self):
870 for handler in self._post_handlers:
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000871 if(handler()):
initial.commit94958cf2008-07-26 22:42:52 +0000872 return
873
874 # called by the redirect handling function when there is no parameter
875 def sendRedirectHelp(self, redirect_name):
876 self.send_response(200)
877 self.send_header('Content-type', 'text/html')
878 self.end_headers()
879 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
880 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
881 self.wfile.write('</body></html>')
882
883def main(options, args):
884 # redirect output to a log file so it doesn't spam the unit test output
885 logfile = open('testserver.log', 'w')
886 sys.stderr = sys.stdout = logfile
887
888 port = options.port
889
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000890 if options.cert:
891 # let's make sure the cert file exists.
892 if not os.path.isfile(options.cert):
893 print 'specified cert file not found: ' + options.cert + ' exiting...'
894 return
895 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
896 print 'HTTPS server started on port %d...' % port
initial.commit94958cf2008-07-26 22:42:52 +0000897 else:
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000898 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
899 print 'HTTP server started on port %d...' % port
erikkay@google.com70397b62008-12-30 21:49:21 +0000900
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000901 if options.data_dir:
902 if not os.path.isdir(options.data_dir):
903 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
904 return
905 server.data_dir = options.data_dir
906 else:
907 # Create the default path to our data dir, relative to the exe dir.
908 server.data_dir = os.path.dirname(sys.argv[0])
909 server.data_dir = os.path.join(server.data_dir, "..", "..", "..", "..",
910 "test", "data")
initial.commit94958cf2008-07-26 22:42:52 +0000911
912 try:
913 server.serve_forever()
914 except KeyboardInterrupt:
915 print 'shutting down server'
916 server.stop = True
917
918if __name__ == '__main__':
919 option_parser = optparse.OptionParser()
920 option_parser.add_option('', '--port', default='8888', type='int',
921 help='Port used by the server')
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000922 option_parser.add_option('', '--data-dir', dest='data_dir',
initial.commit94958cf2008-07-26 22:42:52 +0000923 help='Directory from which to read the files')
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000924 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +0000925 help='Specify that https should be used, specify '
926 'the path to the cert containing the private key '
927 'the server should use')
928 options, args = option_parser.parse_args()
929
930 sys.exit(main(options, args))
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000931