blob: 381b6a81e9cab13e19790b929b9cb150e9fd93c1 [file] [log] [blame]
initial.commit94958cf2008-07-26 22:42:52 +00001#!/usr/bin/python2.4
2# Copyright 2008, Google Inc.
3# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are
7# met:
8#
9# * Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11# * Redistributions in binary form must reproduce the above
12# copyright notice, this list of conditions and the following disclaimer
13# in the documentation and/or other materials provided with the
14# distribution.
15# * Neither the name of Google Inc. nor the names of its
16# contributors may be used to endorse or promote products derived from
17# this software without specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31"""This is a simple HTTP server used for testing Chrome.
32
33It supports several test URLs, as specified by the handlers in TestPageHandler.
34It defaults to living on localhost:8888.
35It can use https if you specify the flag --https=CERT where CERT is the path
36to a pem file containing the certificate and private key that should be used.
37To shut it down properly, visit localhost:8888/kill.
38"""
39
40import base64
41import BaseHTTPServer
42import cgi
43import md5
44import optparse
45import os
46import re
47import SocketServer
48import sys
49import time
50import tlslite
51import tlslite.api
52
53debug_output = sys.stderr
54def debug(str):
55 debug_output.write(str + "\n")
56 debug_output.flush()
57
58class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
59 """This is a specialization of of BaseHTTPServer to allow it
60 to be exited cleanly (by setting its "stop" member to True)."""
61
62 def serve_forever(self):
63 self.stop = False
64 self.nonce = None
65 while not self.stop:
66 self.handle_request()
67 self.socket.close()
68
69class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
70 """This is a specialization of StoppableHTTPerver that add https support."""
71
72 def __init__(self, server_address, request_hander_class, cert_path):
73 s = open(cert_path).read()
74 x509 = tlslite.api.X509()
75 x509.parse(s)
76 self.cert_chain = tlslite.api.X509CertChain([x509])
77 s = open(cert_path).read()
78 self.private_key = tlslite.api.parsePEMKey(s, private=True)
79
80 self.session_cache = tlslite.api.SessionCache()
81 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
82
83 def handshake(self, tlsConnection):
84 """Creates the SSL connection."""
85 try:
86 tlsConnection.handshakeServer(certChain=self.cert_chain,
87 privateKey=self.private_key,
88 sessionCache=self.session_cache)
89 tlsConnection.ignoreAbruptClose = True
90 return True
91 except tlslite.api.TLSError, error:
92 print "Handshake failure:", str(error)
93 return False
94
95class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
96
97 def __init__(self, request, client_address, socket_server):
98 self._get_handlers = [
99 self.KillHandler,
100 self.NoCacheMaxAgeTimeHandler,
101 self.NoCacheTimeHandler,
102 self.CacheTimeHandler,
103 self.CacheExpiresHandler,
104 self.CacheProxyRevalidateHandler,
105 self.CachePrivateHandler,
106 self.CachePublicHandler,
107 self.CacheSMaxAgeHandler,
108 self.CacheMustRevalidateHandler,
109 self.CacheMustRevalidateMaxAgeHandler,
110 self.CacheNoStoreHandler,
111 self.CacheNoStoreMaxAgeHandler,
112 self.CacheNoTransformHandler,
113 self.DownloadHandler,
114 self.DownloadFinishHandler,
115 self.EchoHeader,
116 self.FileHandler,
117 self.RealFileWithCommonHeaderHandler,
118 self.RealBZ2FileWithCommonHeaderHandler,
119 self.AuthBasicHandler,
120 self.AuthDigestHandler,
121 self.SlowServerHandler,
122 self.ContentTypeHandler,
123 self.ServerRedirectHandler,
124 self.ClientRedirectHandler,
125 self.DefaultResponseHandler]
126 self._post_handlers = [
127 self.EchoTitleHandler,
128 self.EchoAllHandler,
129 self.EchoHandler] + self._get_handlers
130
131 self._mime_types = { 'gif': 'image/gif', 'jpeg' : 'image/jpeg', 'jpg' : 'image/jpeg' }
132 self._default_mime_type = 'text/html'
133
134 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, socket_server)
135
136 def GetMIMETypeFromName(self, file_name):
137 """Returns the mime type for the specified file_name. So far it only looks
138 at the file extension."""
139
140 (shortname, extension) = os.path.splitext(file_name)
141 if len(extension) == 0:
142 # no extension.
143 return self._default_mime_type
144
145 return self._mime_types.get(extension, self._default_mime_type)
146
147 def KillHandler(self):
148 """This request handler kills the server, for use when we're done"
149 with the a particular test."""
150
151 if (self.path.find("kill") < 0):
152 return False
153
154 self.send_response(200)
155 self.send_header('Content-type', 'text/html')
156 self.send_header('Cache-Control', 'max-age=0')
157 self.end_headers()
158 self.wfile.write("Time to die")
159 self.server.stop = True
160
161 return True
162
163 def NoCacheMaxAgeTimeHandler(self):
164 """This request handler yields a page with the title set to the current
165 system time, and no caching requested."""
166
167 if (self.path.find("/nocachetime/maxage") != 0):
168 return False
169
170 self.send_response(200)
171 self.send_header('Cache-Control', 'max-age=0')
172 self.send_header('Content-type', 'text/html')
173 self.end_headers()
174
175 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
176
177 return True
178
179 def NoCacheTimeHandler(self):
180 """This request handler yields a page with the title set to the current
181 system time, and no caching requested."""
182
183 if (self.path.find("/nocachetime") != 0):
184 return False
185
186 self.send_response(200)
187 self.send_header('Cache-Control', 'no-cache')
188 self.send_header('Content-type', 'text/html')
189 self.end_headers()
190
191 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
192
193 return True
194
195 def CacheTimeHandler(self):
196 """This request handler yields a page with the title set to the current
197 system time, and allows caching for one minute."""
198
199 if self.path.find("/cachetime") != 0:
200 return False
201
202 self.send_response(200)
203 self.send_header('Cache-Control', 'max-age=60')
204 self.send_header('Content-type', 'text/html')
205 self.end_headers()
206
207 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
208
209 return True
210
211 def CacheExpiresHandler(self):
212 """This request handler yields a page with the title set to the current
213 system time, and set the page to expire on 1 Jan 2099."""
214
215 if (self.path.find("/cache/expires") != 0):
216 return False
217
218 self.send_response(200)
219 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
220 self.send_header('Content-type', 'text/html')
221 self.end_headers()
222
223 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
224
225 return True
226
227 def CacheProxyRevalidateHandler(self):
228 """This request handler yields a page with the title set to the current
229 system time, and allows caching for 60 seconds"""
230
231 if (self.path.find("/cache/proxy-revalidate") != 0):
232 return False
233
234 self.send_response(200)
235 self.send_header('Content-type', 'text/html')
236 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
237 self.end_headers()
238
239 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
240
241 return True
242
243 def CachePrivateHandler(self):
244 """This request handler yields a page with the title set to the current
245 system time, and allows caching for 5 seconds."""
246
247 if (self.path.find("/cache/private") != 0):
248 return False
249
250 self.send_response(200)
251 self.send_header('Content-type', 'text/html')
252 self.send_header('Cache-Control', 'max-age=5, private')
253 self.end_headers()
254
255 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
256
257 return True
258
259 def CachePublicHandler(self):
260 """This request handler yields a page with the title set to the current
261 system time, and allows caching for 5 seconds."""
262
263 if (self.path.find("/cache/public") != 0):
264 return False
265
266 self.send_response(200)
267 self.send_header('Content-type', 'text/html')
268 self.send_header('Cache-Control', 'max-age=5, public')
269 self.end_headers()
270
271 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
272
273 return True
274
275 def CacheSMaxAgeHandler(self):
276 """This request handler yields a page with the title set to the current
277 system time, and does not allow for caching."""
278
279 if (self.path.find("/cache/s-maxage") != 0):
280 return False
281
282 self.send_response(200)
283 self.send_header('Content-type', 'text/html')
284 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
285 self.end_headers()
286
287 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
288
289 return True
290
291 def CacheMustRevalidateHandler(self):
292 """This request handler yields a page with the title set to the current
293 system time, and does not allow caching."""
294
295 if (self.path.find("/cache/must-revalidate") != 0):
296 return False
297
298 self.send_response(200)
299 self.send_header('Content-type', 'text/html')
300 self.send_header('Cache-Control', 'must-revalidate')
301 self.end_headers()
302
303 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
304
305 return True
306
307 def CacheMustRevalidateMaxAgeHandler(self):
308 """This request handler yields a page with the title set to the current
309 system time, and does not allow caching event though max-age of 60
310 seconds is specified."""
311
312 if (self.path.find("/cache/must-revalidate/max-age") != 0):
313 return False
314
315 self.send_response(200)
316 self.send_header('Content-type', 'text/html')
317 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
318 self.end_headers()
319
320 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
321
322 return True
323
324
325 def CacheNoStoreHandler(self):
326 """This request handler yields a page with the title set to the current
327 system time, and does not allow the page to be stored."""
328
329 if (self.path.find("/cache/no-store") != 0):
330 return False
331
332 self.send_response(200)
333 self.send_header('Content-type', 'text/html')
334 self.send_header('Cache-Control', 'no-store')
335 self.end_headers()
336
337 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
338
339 return True
340
341 def CacheNoStoreMaxAgeHandler(self):
342 """This request handler yields a page with the title set to the current
343 system time, and does not allow the page to be stored even though max-age
344 of 60 seconds is specified."""
345
346 if (self.path.find("/cache/no-store/max-age") != 0):
347 return False
348
349 self.send_response(200)
350 self.send_header('Content-type', 'text/html')
351 self.send_header('Cache-Control', 'max-age=60, no-store')
352 self.end_headers()
353
354 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
355
356 return True
357
358
359 def CacheNoTransformHandler(self):
360 """This request handler yields a page with the title set to the current
361 system time, and does not allow the content to transformed during
362 user-agent caching"""
363
364 if (self.path.find("/cache/no-transform") != 0):
365 return False
366
367 self.send_response(200)
368 self.send_header('Content-type', 'text/html')
369 self.send_header('Cache-Control', 'no-transform')
370 self.end_headers()
371
372 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
373
374 return True
375
376 def EchoHeader(self):
377 """This handler echoes back the value of a specific request header."""
378
379 if self.path.find("/echoheader") != 0:
380 return False
381
382 query_char = self.path.find('?')
383 if query_char != -1:
384 header_name = self.path[query_char+1:]
385
386 self.send_response(200)
387 self.send_header('Content-type', 'text/plain')
388 self.send_header('Cache-control', 'max-age=60000')
389 # insert a vary header to properly indicate that the cachability of this
390 # request is subject to value of the request header being echoed.
391 if len(header_name) > 0:
392 self.send_header('Vary', header_name)
393 self.end_headers()
394
395 if len(header_name) > 0:
396 self.wfile.write(self.headers.getheader(header_name))
397
398 return True
399
400 def EchoHandler(self):
401 """This handler just echoes back the payload of the request, for testing
402 form submission."""
403
404 if self.path.find("/echo") != 0:
405 return False
406
407 self.send_response(200)
408 self.send_header('Content-type', 'text/html')
409 self.end_headers()
410 length = int(self.headers.getheader('content-length'))
411 request = self.rfile.read(length)
412 self.wfile.write(request)
413 return True
414
415 def EchoTitleHandler(self):
416 """This handler is like Echo, but sets the page title to the request."""
417
418 if self.path.find("/echotitle") != 0:
419 return False
420
421 self.send_response(200)
422 self.send_header('Content-type', 'text/html')
423 self.end_headers()
424 length = int(self.headers.getheader('content-length'))
425 request = self.rfile.read(length)
426 self.wfile.write('<html><head><title>')
427 self.wfile.write(request)
428 self.wfile.write('</title></head></html>')
429 return True
430
431 def EchoAllHandler(self):
432 """This handler yields a (more) human-readable page listing information
433 about the request header & contents."""
434
435 if self.path.find("/echoall") != 0:
436 return False
437
438 self.send_response(200)
439 self.send_header('Content-type', 'text/html')
440 self.end_headers()
441 self.wfile.write('<html><head><style>'
442 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
443 '</style></head><body>'
444 '<div style="float: right">'
445 '<a href="http://localhost:8888/echo">back to referring page</a></div>'
446 '<h1>Request Body:</h1><pre>')
447 length = int(self.headers.getheader('content-length'))
448 qs = self.rfile.read(length)
449 params = cgi.parse_qs(qs, keep_blank_values=1)
450
451 for param in params:
452 self.wfile.write('%s=%s\n' % (param, params[param][0]))
453
454 self.wfile.write('</pre>')
455
456 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
457
458 self.wfile.write('</body></html>')
459 return True
460
461 def DownloadHandler(self):
462 """This handler sends a downloadable file with or without reporting
463 the size (6K)."""
464
465 if self.path.startswith("/download-unknown-size"):
466 send_length = False
467 elif self.path.startswith("/download-known-size"):
468 send_length = True
469 else:
470 return False
471
472 #
473 # The test which uses this functionality is attempting to send
474 # small chunks of data to the client. Use a fairly large buffer
475 # so that we'll fill chrome's IO buffer enough to force it to
476 # actually write the data.
477 # See also the comments in the client-side of this test in
478 # download_uitest.cc
479 #
480 size_chunk1 = 35*1024
481 size_chunk2 = 10*1024
482
483 self.send_response(200)
484 self.send_header('Content-type', 'application/octet-stream')
485 self.send_header('Cache-Control', 'max-age=0')
486 if send_length:
487 self.send_header('Content-Length', size_chunk1 + size_chunk2)
488 self.end_headers()
489
490 # First chunk of data:
491 self.wfile.write("*" * size_chunk1)
492 self.wfile.flush()
493
494 # handle requests until one of them clears this flag.
495 self.server.waitForDownload = True
496 while self.server.waitForDownload:
497 self.server.handle_request()
498
499 # Second chunk of data:
500 self.wfile.write("*" * size_chunk2)
501 return True
502
503 def DownloadFinishHandler(self):
504 """This handler just tells the server to finish the current download."""
505
506 if not self.path.startswith("/download-finish"):
507 return False
508
509 self.server.waitForDownload = False
510 self.send_response(200)
511 self.send_header('Content-type', 'text/html')
512 self.send_header('Cache-Control', 'max-age=0')
513 self.end_headers()
514 return True
515
516 def FileHandler(self):
517 """This handler sends the contents of the requested file. Wow, it's like
518 a real webserver!"""
519
520 prefix='/files/'
521 if not self.path.startswith(prefix):
522 return False
523
524 file = self.path[len(prefix):]
525 entries = file.split('/');
526 path = os.path.join(self.server.data_dir, *entries)
527
528 if not os.path.isfile(path):
529 print "File not found " + file + " full path:" + path
530 self.send_error(404)
531 return True
532
533 f = open(path, "rb")
534 data = f.read()
535 f.close()
536
537 # If file.mock-http-headers exists, it contains the headers we
538 # should send. Read them in and parse them.
539 headers_path = path + '.mock-http-headers'
540 if os.path.isfile(headers_path):
541 f = open(headers_path, "r")
542
543 # "HTTP/1.1 200 OK"
544 response = f.readline()
545 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
546 self.send_response(int(status_code))
547
548 for line in f:
549 # "name: value"
550 name, value = re.findall('(\S+):\s*(.*)', line)[0]
551 self.send_header(name, value)
552 f.close()
553 else:
554 # Could be more generic once we support mime-type sniffing, but for
555 # now we need to set it explicitly.
556 self.send_response(200)
557 self.send_header('Content-type', self.GetMIMETypeFromName(file))
558 self.send_header('Content-Length', len(data))
559 self.end_headers()
560
561 self.wfile.write(data)
562
563 return True
564
565 def RealFileWithCommonHeaderHandler(self):
566 """This handler sends the contents of the requested file without the pseudo
567 http head!"""
568
569 prefix='/realfiles/'
570 if not self.path.startswith(prefix):
571 return False
572
573 file = self.path[len(prefix):]
574 path = os.path.join(self.server.data_dir, file)
575
576 try:
577 f = open(path, "rb")
578 data = f.read()
579 f.close()
580
581 # just simply set the MIME as octal stream
582 self.send_response(200)
583 self.send_header('Content-type', 'application/octet-stream')
584 self.end_headers()
585
586 self.wfile.write(data)
587 except:
588 self.send_error(404)
589
590 return True
591
592 def RealBZ2FileWithCommonHeaderHandler(self):
593 """This handler sends the bzip2 contents of the requested file with
594 corresponding Content-Encoding field in http head!"""
595
596 prefix='/realbz2files/'
597 if not self.path.startswith(prefix):
598 return False
599
600 parts = self.path.split('?')
601 file = parts[0][len(prefix):]
602 path = os.path.join(self.server.data_dir, file) + '.bz2'
603
604 if len(parts) > 1:
605 options = parts[1]
606 else:
607 options = ''
608
609 try:
610 self.send_response(200)
611 accept_encoding = self.headers.get("Accept-Encoding")
612 if accept_encoding.find("bzip2") != -1:
613 f = open(path, "rb")
614 data = f.read()
615 f.close()
616 self.send_header('Content-Encoding', 'bzip2')
617 self.send_header('Content-type', 'application/x-bzip2')
618 self.end_headers()
619 if options == 'incremental-header':
620 self.wfile.write(data[:1])
621 self.wfile.flush()
622 time.sleep(1.0)
623 self.wfile.write(data[1:])
624 else:
625 self.wfile.write(data)
626 else:
627 """client do not support bzip2 format, send pseudo content
628 """
629 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
630 self.end_headers()
631 self.wfile.write("you do not support bzip2 encoding")
632 except:
633 self.send_error(404)
634
635 return True
636
637 def AuthBasicHandler(self):
638 """This handler tests 'Basic' authentication. It just sends a page with
639 title 'user/pass' if you succeed."""
640
641 if not self.path.startswith("/auth-basic"):
642 return False
643
644 username = userpass = password = b64str = ""
645
646 auth = self.headers.getheader('authorization')
647 try:
648 if not auth:
649 raise Exception('no auth')
650 b64str = re.findall(r'Basic (\S+)', auth)[0]
651 userpass = base64.b64decode(b64str)
652 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
653 if password != 'secret':
654 raise Exception('wrong password')
655 except Exception, e:
656 # Authentication failed.
657 self.send_response(401)
658 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
659 self.send_header('Content-type', 'text/html')
660 self.end_headers()
661 self.wfile.write('<html><head>')
662 self.wfile.write('<title>Denied: %s</title>' % e)
663 self.wfile.write('</head><body>')
664 self.wfile.write('auth=%s<p>' % auth)
665 self.wfile.write('b64str=%s<p>' % b64str)
666 self.wfile.write('username: %s<p>' % username)
667 self.wfile.write('userpass: %s<p>' % userpass)
668 self.wfile.write('password: %s<p>' % password)
669 self.wfile.write('You sent:<br>%s<p>' % self.headers)
670 self.wfile.write('</body></html>')
671 return True
672
673 # Authentication successful. (Return a cachable response to allow for
674 # testing cached pages that require authentication.)
675 if_none_match = self.headers.getheader('if-none-match')
676 if if_none_match == "abc":
677 self.send_response(304)
678 self.end_headers()
679 else:
680 self.send_response(200)
681 self.send_header('Content-type', 'text/html')
682 self.send_header('Cache-control', 'max-age=60000')
683 self.send_header('Etag', 'abc')
684 self.end_headers()
685 self.wfile.write('<html><head>')
686 self.wfile.write('<title>%s/%s</title>' % (username, password))
687 self.wfile.write('</head><body>')
688 self.wfile.write('auth=%s<p>' % auth)
689 self.wfile.write('</body></html>')
690
691 return True
692
693 def AuthDigestHandler(self):
694 """This handler tests 'Digest' authentication. It just sends a page with
695 title 'user/pass' if you succeed."""
696
697 if not self.path.startswith("/auth-digest"):
698 return False
699
700 # Periodically generate a new nonce. Technically we should incorporate
701 # the request URL into this, but we don't care for testing.
702 nonce_life = 10
703 stale = False
704 if not self.server.nonce or (time.time() - self.server.nonce_time > nonce_life):
705 if self.server.nonce:
706 stale = True
707 self.server.nonce_time = time.time()
708 self.server.nonce = \
709 md5.new(time.ctime(self.server.nonce_time) + 'privatekey').hexdigest()
710
711 nonce = self.server.nonce
712 opaque = md5.new('opaque').hexdigest()
713 password = 'secret'
714 realm = 'testrealm'
715
716 auth = self.headers.getheader('authorization')
717 pairs = {}
718 try:
719 if not auth:
720 raise Exception('no auth')
721 if not auth.startswith('Digest'):
722 raise Exception('not digest')
723 # Pull out all the name="value" pairs as a dictionary.
724 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
725
726 # Make sure it's all valid.
727 if pairs['nonce'] != nonce:
728 raise Exception('wrong nonce')
729 if pairs['opaque'] != opaque:
730 raise Exception('wrong opaque')
731
732 # Check the 'response' value and make sure it matches our magic hash.
733 # See http://www.ietf.org/rfc/rfc2617.txt
734 hash_a1 = md5.new(':'.join([pairs['username'], realm, password])).hexdigest()
735 hash_a2 = md5.new(':'.join([self.command, pairs['uri']])).hexdigest()
736 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
737 response = md5.new(':'.join([hash_a1, nonce, pairs['nc'],
738 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
739 else:
740 response = md5.new(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
741
742 if pairs['response'] != response:
743 raise Exception('wrong password')
744 except Exception, e:
745 # Authentication failed.
746 self.send_response(401)
747 hdr = ('Digest '
748 'realm="%s", '
749 'domain="/", '
750 'qop="auth", '
751 'algorithm=MD5, '
752 'nonce="%s", '
753 'opaque="%s"') % (realm, nonce, opaque)
754 if stale:
755 hdr += ', stale="TRUE"'
756 self.send_header('WWW-Authenticate', hdr)
757 self.send_header('Content-type', 'text/html')
758 self.end_headers()
759 self.wfile.write('<html><head>')
760 self.wfile.write('<title>Denied: %s</title>' % e)
761 self.wfile.write('</head><body>')
762 self.wfile.write('auth=%s<p>' % auth)
763 self.wfile.write('pairs=%s<p>' % pairs)
764 self.wfile.write('You sent:<br>%s<p>' % self.headers)
765 self.wfile.write('We are replying:<br>%s<p>' % hdr)
766 self.wfile.write('</body></html>')
767 return True
768
769 # Authentication successful.
770 self.send_response(200)
771 self.send_header('Content-type', 'text/html')
772 self.end_headers()
773 self.wfile.write('<html><head>')
774 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
775 self.wfile.write('</head><body>')
776 self.wfile.write('auth=%s<p>' % auth)
777 self.wfile.write('pairs=%s<p>' % pairs)
778 self.wfile.write('</body></html>')
779
780 return True
781
782 def SlowServerHandler(self):
783 """Wait for the user suggested time before responding. The syntax is
784 /slow?0.5 to wait for half a second."""
785 if not self.path.startswith("/slow"):
786 return False
787 query_char = self.path.find('?')
788 wait_sec = 1.0
789 if query_char >= 0:
790 try:
791 wait_sec = int(self.path[query_char + 1:])
792 except ValueError:
793 pass
794 time.sleep(wait_sec)
795 self.send_response(200)
796 self.send_header('Content-type', 'text/plain')
797 self.end_headers()
798 self.wfile.write("waited %d seconds" % wait_sec)
799 return True
800
801 def ContentTypeHandler(self):
802 """Returns a string of html with the given content type. E.g.,
803 /contenttype?text/css returns an html file with the Content-Type
804 header set to text/css."""
805 if not self.path.startswith('/contenttype'):
806 return False
807 query_char = self.path.find('?')
808 content_type = self.path[query_char + 1:].strip()
809 if not content_type:
810 content_type = 'text/html'
811 self.send_response(200)
812 self.send_header('Content-Type', content_type)
813 self.end_headers()
814 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
815 return True
816
817 def ServerRedirectHandler(self):
818 """Sends a server redirect to the given URL. The syntax is
819 '/server-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
820
821 test_name = "/server-redirect"
822 if not self.path.startswith(test_name):
823 return False
824
825 query_char = self.path.find('?')
826 if query_char < 0 or len(self.path) <= query_char + 1:
827 self.sendRedirectHelp(test_name)
828 return True
829 dest = self.path[query_char + 1:]
830
831 self.send_response(301) # moved permanently
832 self.send_header('Location', dest)
833 self.send_header('Content-type', 'text/html')
834 self.end_headers()
835 self.wfile.write('<html><head>')
836 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
837
838 return True;
839
840 def ClientRedirectHandler(self):
841 """Sends a client redirect to the given URL. The syntax is
842 '/client-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
843
844 test_name = "/client-redirect"
845 if not self.path.startswith(test_name):
846 return False
847
848 query_char = self.path.find('?');
849 if query_char < 0 or len(self.path) <= query_char + 1:
850 self.sendRedirectHelp(test_name)
851 return True
852 dest = self.path[query_char + 1:]
853
854 self.send_response(200)
855 self.send_header('Content-type', 'text/html')
856 self.end_headers()
857 self.wfile.write('<html><head>')
858 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
859 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
860
861 return True
862
863 def DefaultResponseHandler(self):
864 """This is the catch-all response handler for requests that aren't handled
865 by one of the special handlers above.
866 Note that we specify the content-length as without it the https connection
867 is not closed properly (and the browser keeps expecting data)."""
868
869 contents = "Default response given for path: " + self.path
870 self.send_response(200)
871 self.send_header('Content-type', 'text/html')
872 self.send_header("Content-Length", len(contents))
873 self.end_headers()
874 self.wfile.write(contents)
875 return True
876
877 def do_GET(self):
878 for handler in self._get_handlers:
879 if (handler()):
880 return
881
882 def do_POST(self):
883 for handler in self._post_handlers:
884 if(handler()):
885 return
886
887 # called by the redirect handling function when there is no parameter
888 def sendRedirectHelp(self, redirect_name):
889 self.send_response(200)
890 self.send_header('Content-type', 'text/html')
891 self.end_headers()
892 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
893 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
894 self.wfile.write('</body></html>')
895
896def main(options, args):
897 # redirect output to a log file so it doesn't spam the unit test output
898 logfile = open('testserver.log', 'w')
899 sys.stderr = sys.stdout = logfile
900
901 port = options.port
902
903 if options.cert:
904 # let's make sure the cert file exists.
905 if not os.path.isfile(options.cert):
906 print 'specified cert file not found: ' + options.cert + ' exiting...'
907 return
908 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
909 print 'HTTPS server started on port %d...' % port
910 else:
911 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
912 print 'HTTP server started on port %d...' % port
913
914 if options.data_dir:
915 if not os.path.isdir(options.data_dir):
916 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
917 return
918 server.data_dir = options.data_dir
919 else:
920 # Create the default path to our data dir, relative to the exe dir.
921 server.data_dir = os.path.dirname(sys.argv[0])
922 server.data_dir = os.path.join(server.data_dir, "..", "..", "..", "..",
923 "test", "data")
924
925 try:
926 server.serve_forever()
927 except KeyboardInterrupt:
928 print 'shutting down server'
929 server.stop = True
930
931if __name__ == '__main__':
932 option_parser = optparse.OptionParser()
933 option_parser.add_option('', '--port', default='8888', type='int',
934 help='Port used by the server')
935 option_parser.add_option('', '--data-dir', dest='data_dir',
936 help='Directory from which to read the files')
937 option_parser.add_option('', '--https', dest='cert',
938 help='Specify that https should be used, specify '
939 'the path to the cert containing the private key '
940 'the server should use')
941 options, args = option_parser.parse_args()
942
943 sys.exit(main(options, args))