blob: e7124f9ea22d9dbf16f9d1798ae112ef3905c317 [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
erikkay@google.comd5182ff2009-01-08 20:45:27 +000027import pyftpdlib.ftpserver
28
29SERVER_HTTP = 0
30SERVER_FTP = 1
initial.commit94958cf2008-07-26 22:42:52 +000031
32debug_output = sys.stderr
33def debug(str):
34 debug_output.write(str + "\n")
35 debug_output.flush()
36
37class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
38 """This is a specialization of of BaseHTTPServer to allow it
39 to be exited cleanly (by setting its "stop" member to True)."""
40
41 def serve_forever(self):
42 self.stop = False
43 self.nonce = None
44 while not self.stop:
45 self.handle_request()
46 self.socket.close()
47
48class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
49 """This is a specialization of StoppableHTTPerver that add https support."""
50
51 def __init__(self, server_address, request_hander_class, cert_path):
52 s = open(cert_path).read()
53 x509 = tlslite.api.X509()
54 x509.parse(s)
55 self.cert_chain = tlslite.api.X509CertChain([x509])
56 s = open(cert_path).read()
57 self.private_key = tlslite.api.parsePEMKey(s, private=True)
58
59 self.session_cache = tlslite.api.SessionCache()
60 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
61
62 def handshake(self, tlsConnection):
63 """Creates the SSL connection."""
64 try:
65 tlsConnection.handshakeServer(certChain=self.cert_chain,
66 privateKey=self.private_key,
67 sessionCache=self.session_cache)
68 tlsConnection.ignoreAbruptClose = True
69 return True
70 except tlslite.api.TLSError, error:
71 print "Handshake failure:", str(error)
72 return False
73
74class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
75
76 def __init__(self, request, client_address, socket_server):
77 self._get_handlers = [
78 self.KillHandler,
79 self.NoCacheMaxAgeTimeHandler,
80 self.NoCacheTimeHandler,
81 self.CacheTimeHandler,
82 self.CacheExpiresHandler,
83 self.CacheProxyRevalidateHandler,
84 self.CachePrivateHandler,
85 self.CachePublicHandler,
86 self.CacheSMaxAgeHandler,
87 self.CacheMustRevalidateHandler,
88 self.CacheMustRevalidateMaxAgeHandler,
89 self.CacheNoStoreHandler,
90 self.CacheNoStoreMaxAgeHandler,
91 self.CacheNoTransformHandler,
92 self.DownloadHandler,
93 self.DownloadFinishHandler,
94 self.EchoHeader,
ericroman@google.coma47622b2008-11-15 04:36:51 +000095 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +000096 self.FileHandler,
97 self.RealFileWithCommonHeaderHandler,
98 self.RealBZ2FileWithCommonHeaderHandler,
99 self.AuthBasicHandler,
100 self.AuthDigestHandler,
101 self.SlowServerHandler,
102 self.ContentTypeHandler,
103 self.ServerRedirectHandler,
104 self.ClientRedirectHandler,
105 self.DefaultResponseHandler]
106 self._post_handlers = [
107 self.EchoTitleHandler,
108 self.EchoAllHandler,
109 self.EchoHandler] + self._get_handlers
110
111 self._mime_types = { 'gif': 'image/gif', 'jpeg' : 'image/jpeg', 'jpg' : 'image/jpeg' }
112 self._default_mime_type = 'text/html'
113
114 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, socket_server)
115
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000116 def _ShouldHandleRequest(self, handler_name):
117 """Determines if the path can be handled by the handler.
118
119 We consider a handler valid if the path begins with the
120 handler name. It can optionally be followed by "?*", "/*".
121 """
122
123 pattern = re.compile('%s($|\?|/).*' % handler_name)
124 return pattern.match(self.path)
125
initial.commit94958cf2008-07-26 22:42:52 +0000126 def GetMIMETypeFromName(self, file_name):
127 """Returns the mime type for the specified file_name. So far it only looks
128 at the file extension."""
129
130 (shortname, extension) = os.path.splitext(file_name)
131 if len(extension) == 0:
132 # no extension.
133 return self._default_mime_type
134
135 return self._mime_types.get(extension, self._default_mime_type)
136
137 def KillHandler(self):
138 """This request handler kills the server, for use when we're done"
139 with the a particular test."""
140
141 if (self.path.find("kill") < 0):
142 return False
143
144 self.send_response(200)
145 self.send_header('Content-type', 'text/html')
146 self.send_header('Cache-Control', 'max-age=0')
147 self.end_headers()
148 self.wfile.write("Time to die")
149 self.server.stop = True
150
151 return True
152
153 def NoCacheMaxAgeTimeHandler(self):
154 """This request handler yields a page with the title set to the current
155 system time, and no caching requested."""
156
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000157 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000158 return False
159
160 self.send_response(200)
161 self.send_header('Cache-Control', 'max-age=0')
162 self.send_header('Content-type', 'text/html')
163 self.end_headers()
164
165 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
166
167 return True
168
169 def NoCacheTimeHandler(self):
170 """This request handler yields a page with the title set to the current
171 system time, and no caching requested."""
172
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000173 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000174 return False
175
176 self.send_response(200)
177 self.send_header('Cache-Control', 'no-cache')
178 self.send_header('Content-type', 'text/html')
179 self.end_headers()
180
181 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
182
183 return True
184
185 def CacheTimeHandler(self):
186 """This request handler yields a page with the title set to the current
187 system time, and allows caching for one minute."""
188
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000189 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000190 return False
191
192 self.send_response(200)
193 self.send_header('Cache-Control', 'max-age=60')
194 self.send_header('Content-type', 'text/html')
195 self.end_headers()
196
197 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
198
199 return True
200
201 def CacheExpiresHandler(self):
202 """This request handler yields a page with the title set to the current
203 system time, and set the page to expire on 1 Jan 2099."""
204
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000205 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000206 return False
207
208 self.send_response(200)
209 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
210 self.send_header('Content-type', 'text/html')
211 self.end_headers()
212
213 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
214
215 return True
216
217 def CacheProxyRevalidateHandler(self):
218 """This request handler yields a page with the title set to the current
219 system time, and allows caching for 60 seconds"""
220
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000221 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000222 return False
223
224 self.send_response(200)
225 self.send_header('Content-type', 'text/html')
226 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
227 self.end_headers()
228
229 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
230
231 return True
232
233 def CachePrivateHandler(self):
234 """This request handler yields a page with the title set to the current
235 system time, and allows caching for 5 seconds."""
236
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000237 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000238 return False
239
240 self.send_response(200)
241 self.send_header('Content-type', 'text/html')
242 self.send_header('Cache-Control', 'max-age=5, private')
243 self.end_headers()
244
245 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
246
247 return True
248
249 def CachePublicHandler(self):
250 """This request handler yields a page with the title set to the current
251 system time, and allows caching for 5 seconds."""
252
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000253 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000254 return False
255
256 self.send_response(200)
257 self.send_header('Content-type', 'text/html')
258 self.send_header('Cache-Control', 'max-age=5, public')
259 self.end_headers()
260
261 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
262
263 return True
264
265 def CacheSMaxAgeHandler(self):
266 """This request handler yields a page with the title set to the current
267 system time, and does not allow for caching."""
268
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000269 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000270 return False
271
272 self.send_response(200)
273 self.send_header('Content-type', 'text/html')
274 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
275 self.end_headers()
276
277 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
278
279 return True
280
281 def CacheMustRevalidateHandler(self):
282 """This request handler yields a page with the title set to the current
283 system time, and does not allow caching."""
284
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000285 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000286 return False
287
288 self.send_response(200)
289 self.send_header('Content-type', 'text/html')
290 self.send_header('Cache-Control', 'must-revalidate')
291 self.end_headers()
292
293 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
294
295 return True
296
297 def CacheMustRevalidateMaxAgeHandler(self):
298 """This request handler yields a page with the title set to the current
299 system time, and does not allow caching event though max-age of 60
300 seconds is specified."""
301
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000302 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000303 return False
304
305 self.send_response(200)
306 self.send_header('Content-type', 'text/html')
307 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
308 self.end_headers()
309
310 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
311
312 return True
313
initial.commit94958cf2008-07-26 22:42:52 +0000314 def CacheNoStoreHandler(self):
315 """This request handler yields a page with the title set to the current
316 system time, and does not allow the page to be stored."""
317
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000318 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000319 return False
320
321 self.send_response(200)
322 self.send_header('Content-type', 'text/html')
323 self.send_header('Cache-Control', 'no-store')
324 self.end_headers()
325
326 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
327
328 return True
329
330 def CacheNoStoreMaxAgeHandler(self):
331 """This request handler yields a page with the title set to the current
332 system time, and does not allow the page to be stored even though max-age
333 of 60 seconds is specified."""
334
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000335 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000336 return False
337
338 self.send_response(200)
339 self.send_header('Content-type', 'text/html')
340 self.send_header('Cache-Control', 'max-age=60, no-store')
341 self.end_headers()
342
343 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
344
345 return True
346
347
348 def CacheNoTransformHandler(self):
349 """This request handler yields a page with the title set to the current
350 system time, and does not allow the content to transformed during
351 user-agent caching"""
352
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000353 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000354 return False
355
356 self.send_response(200)
357 self.send_header('Content-type', 'text/html')
358 self.send_header('Cache-Control', 'no-transform')
359 self.end_headers()
360
361 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
362
363 return True
364
365 def EchoHeader(self):
366 """This handler echoes back the value of a specific request header."""
367
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000368 if not self._ShouldHandleRequest("/echoheader"):
initial.commit94958cf2008-07-26 22:42:52 +0000369 return False
370
371 query_char = self.path.find('?')
372 if query_char != -1:
373 header_name = self.path[query_char+1:]
374
375 self.send_response(200)
376 self.send_header('Content-type', 'text/plain')
377 self.send_header('Cache-control', 'max-age=60000')
378 # insert a vary header to properly indicate that the cachability of this
379 # request is subject to value of the request header being echoed.
380 if len(header_name) > 0:
381 self.send_header('Vary', header_name)
382 self.end_headers()
383
384 if len(header_name) > 0:
385 self.wfile.write(self.headers.getheader(header_name))
386
387 return True
388
389 def EchoHandler(self):
390 """This handler just echoes back the payload of the request, for testing
391 form submission."""
392
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000393 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000394 return False
395
396 self.send_response(200)
397 self.send_header('Content-type', 'text/html')
398 self.end_headers()
399 length = int(self.headers.getheader('content-length'))
400 request = self.rfile.read(length)
401 self.wfile.write(request)
402 return True
403
404 def EchoTitleHandler(self):
405 """This handler is like Echo, but sets the page title to the request."""
406
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000407 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000408 return False
409
410 self.send_response(200)
411 self.send_header('Content-type', 'text/html')
412 self.end_headers()
413 length = int(self.headers.getheader('content-length'))
414 request = self.rfile.read(length)
415 self.wfile.write('<html><head><title>')
416 self.wfile.write(request)
417 self.wfile.write('</title></head></html>')
418 return True
419
420 def EchoAllHandler(self):
421 """This handler yields a (more) human-readable page listing information
422 about the request header & contents."""
423
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000424 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000425 return False
426
427 self.send_response(200)
428 self.send_header('Content-type', 'text/html')
429 self.end_headers()
430 self.wfile.write('<html><head><style>'
431 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
432 '</style></head><body>'
433 '<div style="float: right">'
434 '<a href="http://localhost:8888/echo">back to referring page</a></div>'
435 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000436
ericroman@google.coma47622b2008-11-15 04:36:51 +0000437 if self.command == 'POST':
438 length = int(self.headers.getheader('content-length'))
439 qs = self.rfile.read(length)
440 params = cgi.parse_qs(qs, keep_blank_values=1)
441
442 for param in params:
443 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000444
445 self.wfile.write('</pre>')
446
447 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
448
449 self.wfile.write('</body></html>')
450 return True
451
452 def DownloadHandler(self):
453 """This handler sends a downloadable file with or without reporting
454 the size (6K)."""
455
456 if self.path.startswith("/download-unknown-size"):
457 send_length = False
458 elif self.path.startswith("/download-known-size"):
459 send_length = True
460 else:
461 return False
462
463 #
464 # The test which uses this functionality is attempting to send
465 # small chunks of data to the client. Use a fairly large buffer
466 # so that we'll fill chrome's IO buffer enough to force it to
467 # actually write the data.
468 # See also the comments in the client-side of this test in
469 # download_uitest.cc
470 #
471 size_chunk1 = 35*1024
472 size_chunk2 = 10*1024
473
474 self.send_response(200)
475 self.send_header('Content-type', 'application/octet-stream')
476 self.send_header('Cache-Control', 'max-age=0')
477 if send_length:
478 self.send_header('Content-Length', size_chunk1 + size_chunk2)
479 self.end_headers()
480
481 # First chunk of data:
482 self.wfile.write("*" * size_chunk1)
483 self.wfile.flush()
484
485 # handle requests until one of them clears this flag.
486 self.server.waitForDownload = True
487 while self.server.waitForDownload:
488 self.server.handle_request()
489
490 # Second chunk of data:
491 self.wfile.write("*" * size_chunk2)
492 return True
493
494 def DownloadFinishHandler(self):
495 """This handler just tells the server to finish the current download."""
496
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000497 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000498 return False
499
500 self.server.waitForDownload = False
501 self.send_response(200)
502 self.send_header('Content-type', 'text/html')
503 self.send_header('Cache-Control', 'max-age=0')
504 self.end_headers()
505 return True
506
507 def FileHandler(self):
508 """This handler sends the contents of the requested file. Wow, it's like
509 a real webserver!"""
510
511 prefix='/files/'
512 if not self.path.startswith(prefix):
513 return False
514
515 file = self.path[len(prefix):]
516 entries = file.split('/');
517 path = os.path.join(self.server.data_dir, *entries)
518
519 if not os.path.isfile(path):
520 print "File not found " + file + " full path:" + path
521 self.send_error(404)
522 return True
523
524 f = open(path, "rb")
525 data = f.read()
526 f.close()
527
528 # If file.mock-http-headers exists, it contains the headers we
529 # should send. Read them in and parse them.
530 headers_path = path + '.mock-http-headers'
531 if os.path.isfile(headers_path):
532 f = open(headers_path, "r")
533
534 # "HTTP/1.1 200 OK"
535 response = f.readline()
536 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
537 self.send_response(int(status_code))
538
539 for line in f:
540 # "name: value"
541 name, value = re.findall('(\S+):\s*(.*)', line)[0]
542 self.send_header(name, value)
543 f.close()
544 else:
545 # Could be more generic once we support mime-type sniffing, but for
546 # now we need to set it explicitly.
547 self.send_response(200)
548 self.send_header('Content-type', self.GetMIMETypeFromName(file))
549 self.send_header('Content-Length', len(data))
550 self.end_headers()
551
552 self.wfile.write(data)
553
554 return True
555
556 def RealFileWithCommonHeaderHandler(self):
557 """This handler sends the contents of the requested file without the pseudo
558 http head!"""
559
560 prefix='/realfiles/'
561 if not self.path.startswith(prefix):
562 return False
563
564 file = self.path[len(prefix):]
565 path = os.path.join(self.server.data_dir, file)
566
567 try:
568 f = open(path, "rb")
569 data = f.read()
570 f.close()
571
572 # just simply set the MIME as octal stream
573 self.send_response(200)
574 self.send_header('Content-type', 'application/octet-stream')
575 self.end_headers()
576
577 self.wfile.write(data)
578 except:
579 self.send_error(404)
580
581 return True
582
583 def RealBZ2FileWithCommonHeaderHandler(self):
584 """This handler sends the bzip2 contents of the requested file with
585 corresponding Content-Encoding field in http head!"""
586
587 prefix='/realbz2files/'
588 if not self.path.startswith(prefix):
589 return False
590
591 parts = self.path.split('?')
592 file = parts[0][len(prefix):]
593 path = os.path.join(self.server.data_dir, file) + '.bz2'
594
595 if len(parts) > 1:
596 options = parts[1]
597 else:
598 options = ''
599
600 try:
601 self.send_response(200)
602 accept_encoding = self.headers.get("Accept-Encoding")
603 if accept_encoding.find("bzip2") != -1:
604 f = open(path, "rb")
605 data = f.read()
606 f.close()
607 self.send_header('Content-Encoding', 'bzip2')
608 self.send_header('Content-type', 'application/x-bzip2')
609 self.end_headers()
610 if options == 'incremental-header':
611 self.wfile.write(data[:1])
612 self.wfile.flush()
613 time.sleep(1.0)
614 self.wfile.write(data[1:])
615 else:
616 self.wfile.write(data)
617 else:
618 """client do not support bzip2 format, send pseudo content
619 """
620 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
621 self.end_headers()
622 self.wfile.write("you do not support bzip2 encoding")
623 except:
624 self.send_error(404)
625
626 return True
627
628 def AuthBasicHandler(self):
629 """This handler tests 'Basic' authentication. It just sends a page with
630 title 'user/pass' if you succeed."""
631
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000632 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000633 return False
634
635 username = userpass = password = b64str = ""
636
637 auth = self.headers.getheader('authorization')
638 try:
639 if not auth:
640 raise Exception('no auth')
641 b64str = re.findall(r'Basic (\S+)', auth)[0]
642 userpass = base64.b64decode(b64str)
643 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
644 if password != 'secret':
645 raise Exception('wrong password')
646 except Exception, e:
647 # Authentication failed.
648 self.send_response(401)
649 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
650 self.send_header('Content-type', 'text/html')
651 self.end_headers()
652 self.wfile.write('<html><head>')
653 self.wfile.write('<title>Denied: %s</title>' % e)
654 self.wfile.write('</head><body>')
655 self.wfile.write('auth=%s<p>' % auth)
656 self.wfile.write('b64str=%s<p>' % b64str)
657 self.wfile.write('username: %s<p>' % username)
658 self.wfile.write('userpass: %s<p>' % userpass)
659 self.wfile.write('password: %s<p>' % password)
660 self.wfile.write('You sent:<br>%s<p>' % self.headers)
661 self.wfile.write('</body></html>')
662 return True
663
664 # Authentication successful. (Return a cachable response to allow for
665 # testing cached pages that require authentication.)
666 if_none_match = self.headers.getheader('if-none-match')
667 if if_none_match == "abc":
668 self.send_response(304)
669 self.end_headers()
670 else:
671 self.send_response(200)
672 self.send_header('Content-type', 'text/html')
673 self.send_header('Cache-control', 'max-age=60000')
674 self.send_header('Etag', 'abc')
675 self.end_headers()
676 self.wfile.write('<html><head>')
677 self.wfile.write('<title>%s/%s</title>' % (username, password))
678 self.wfile.write('</head><body>')
679 self.wfile.write('auth=%s<p>' % auth)
680 self.wfile.write('</body></html>')
681
682 return True
683
684 def AuthDigestHandler(self):
685 """This handler tests 'Digest' authentication. It just sends a page with
686 title 'user/pass' if you succeed."""
687
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000688 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000689 return False
690
691 # Periodically generate a new nonce. Technically we should incorporate
692 # the request URL into this, but we don't care for testing.
693 nonce_life = 10
694 stale = False
695 if not self.server.nonce or (time.time() - self.server.nonce_time > nonce_life):
696 if self.server.nonce:
697 stale = True
698 self.server.nonce_time = time.time()
699 self.server.nonce = \
700 md5.new(time.ctime(self.server.nonce_time) + 'privatekey').hexdigest()
701
702 nonce = self.server.nonce
703 opaque = md5.new('opaque').hexdigest()
704 password = 'secret'
705 realm = 'testrealm'
706
707 auth = self.headers.getheader('authorization')
708 pairs = {}
709 try:
710 if not auth:
711 raise Exception('no auth')
712 if not auth.startswith('Digest'):
713 raise Exception('not digest')
714 # Pull out all the name="value" pairs as a dictionary.
715 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
716
717 # Make sure it's all valid.
718 if pairs['nonce'] != nonce:
719 raise Exception('wrong nonce')
720 if pairs['opaque'] != opaque:
721 raise Exception('wrong opaque')
722
723 # Check the 'response' value and make sure it matches our magic hash.
724 # See http://www.ietf.org/rfc/rfc2617.txt
725 hash_a1 = md5.new(':'.join([pairs['username'], realm, password])).hexdigest()
726 hash_a2 = md5.new(':'.join([self.command, pairs['uri']])).hexdigest()
727 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
728 response = md5.new(':'.join([hash_a1, nonce, pairs['nc'],
729 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
730 else:
731 response = md5.new(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
732
733 if pairs['response'] != response:
734 raise Exception('wrong password')
735 except Exception, e:
736 # Authentication failed.
737 self.send_response(401)
738 hdr = ('Digest '
739 'realm="%s", '
740 'domain="/", '
741 'qop="auth", '
742 'algorithm=MD5, '
743 'nonce="%s", '
744 'opaque="%s"') % (realm, nonce, opaque)
745 if stale:
746 hdr += ', stale="TRUE"'
747 self.send_header('WWW-Authenticate', hdr)
748 self.send_header('Content-type', 'text/html')
749 self.end_headers()
750 self.wfile.write('<html><head>')
751 self.wfile.write('<title>Denied: %s</title>' % e)
752 self.wfile.write('</head><body>')
753 self.wfile.write('auth=%s<p>' % auth)
754 self.wfile.write('pairs=%s<p>' % pairs)
755 self.wfile.write('You sent:<br>%s<p>' % self.headers)
756 self.wfile.write('We are replying:<br>%s<p>' % hdr)
757 self.wfile.write('</body></html>')
758 return True
759
760 # Authentication successful.
761 self.send_response(200)
762 self.send_header('Content-type', 'text/html')
763 self.end_headers()
764 self.wfile.write('<html><head>')
765 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
766 self.wfile.write('</head><body>')
767 self.wfile.write('auth=%s<p>' % auth)
768 self.wfile.write('pairs=%s<p>' % pairs)
769 self.wfile.write('</body></html>')
770
771 return True
772
773 def SlowServerHandler(self):
774 """Wait for the user suggested time before responding. The syntax is
775 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000776 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000777 return False
778 query_char = self.path.find('?')
779 wait_sec = 1.0
780 if query_char >= 0:
781 try:
782 wait_sec = int(self.path[query_char + 1:])
783 except ValueError:
784 pass
785 time.sleep(wait_sec)
786 self.send_response(200)
787 self.send_header('Content-type', 'text/plain')
788 self.end_headers()
789 self.wfile.write("waited %d seconds" % wait_sec)
790 return True
791
792 def ContentTypeHandler(self):
793 """Returns a string of html with the given content type. E.g.,
794 /contenttype?text/css returns an html file with the Content-Type
795 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000796 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +0000797 return False
798 query_char = self.path.find('?')
799 content_type = self.path[query_char + 1:].strip()
800 if not content_type:
801 content_type = 'text/html'
802 self.send_response(200)
803 self.send_header('Content-Type', content_type)
804 self.end_headers()
805 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
806 return True
807
808 def ServerRedirectHandler(self):
809 """Sends a server redirect to the given URL. The syntax is
810 '/server-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
811
812 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000813 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000814 return False
815
816 query_char = self.path.find('?')
817 if query_char < 0 or len(self.path) <= query_char + 1:
818 self.sendRedirectHelp(test_name)
819 return True
820 dest = self.path[query_char + 1:]
821
822 self.send_response(301) # moved permanently
823 self.send_header('Location', dest)
824 self.send_header('Content-type', 'text/html')
825 self.end_headers()
826 self.wfile.write('<html><head>')
827 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
828
829 return True;
830
831 def ClientRedirectHandler(self):
832 """Sends a client redirect to the given URL. The syntax is
833 '/client-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
834
835 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000836 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000837 return False
838
839 query_char = self.path.find('?');
840 if query_char < 0 or len(self.path) <= query_char + 1:
841 self.sendRedirectHelp(test_name)
842 return True
843 dest = self.path[query_char + 1:]
844
845 self.send_response(200)
846 self.send_header('Content-type', 'text/html')
847 self.end_headers()
848 self.wfile.write('<html><head>')
849 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
850 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
851
852 return True
853
854 def DefaultResponseHandler(self):
855 """This is the catch-all response handler for requests that aren't handled
856 by one of the special handlers above.
857 Note that we specify the content-length as without it the https connection
858 is not closed properly (and the browser keeps expecting data)."""
859
860 contents = "Default response given for path: " + self.path
861 self.send_response(200)
862 self.send_header('Content-type', 'text/html')
863 self.send_header("Content-Length", len(contents))
864 self.end_headers()
865 self.wfile.write(contents)
866 return True
867
868 def do_GET(self):
869 for handler in self._get_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000870 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000871 return
872
873 def do_POST(self):
874 for handler in self._post_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000875 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000876 return
877
878 # called by the redirect handling function when there is no parameter
879 def sendRedirectHelp(self, redirect_name):
880 self.send_response(200)
881 self.send_header('Content-type', 'text/html')
882 self.end_headers()
883 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
884 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
885 self.wfile.write('</body></html>')
886
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000887def MakeDataDir():
888 if options.data_dir:
889 if not os.path.isdir(options.data_dir):
890 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
891 return None
892 my_data_dir = options.data_dir
893 else:
894 # Create the default path to our data dir, relative to the exe dir.
895 my_data_dir = os.path.dirname(sys.argv[0])
896 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
897 "test", "data")
898
899 #TODO(ibrar): Must use Find* funtion defined in google\tools
900 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
901
902 return my_data_dir
903
initial.commit94958cf2008-07-26 22:42:52 +0000904def main(options, args):
905 # redirect output to a log file so it doesn't spam the unit test output
906 logfile = open('testserver.log', 'w')
907 sys.stderr = sys.stdout = logfile
908
909 port = options.port
910
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000911 if options.server_type == SERVER_HTTP:
912 if options.cert:
913 # let's make sure the cert file exists.
914 if not os.path.isfile(options.cert):
915 print 'specified cert file not found: ' + options.cert + ' exiting...'
916 return
917 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
918 print 'HTTPS server started on port %d...' % port
919 else:
920 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
921 print 'HTTP server started on port %d...' % port
erikkay@google.com70397b62008-12-30 21:49:21 +0000922
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000923 server.data_dir = MakeDataDir()
924
925 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000926 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000927 my_data_dir = MakeDataDir()
928
929 def line_logger(msg):
930 if (msg.find("kill") >= 0):
931 server.stop = True
932 print 'shutting down server'
933 sys.exit(0)
934
935 # Instantiate a dummy authorizer for managing 'virtual' users
936 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
937
938 # Define a new user having full r/w permissions and a read-only
939 # anonymous user
940 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
941
942 authorizer.add_anonymous(my_data_dir)
943
944 # Instantiate FTP handler class
945 ftp_handler = pyftpdlib.ftpserver.FTPHandler
946 ftp_handler.authorizer = authorizer
947 pyftpdlib.ftpserver.logline = line_logger
948
949 # Define a customized banner (string returned when client connects)
950 ftp_handler.banner = "pyftpdlib %s based ftpd ready." % pyftpdlib.ftpserver.__ver__
951
952 # Instantiate FTP server class and listen to 127.0.0.1:port
953 address = ('127.0.0.1', port)
954 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
955 print 'FTP server started on port %d...' % port
initial.commit94958cf2008-07-26 22:42:52 +0000956
957 try:
958 server.serve_forever()
959 except KeyboardInterrupt:
960 print 'shutting down server'
961 server.stop = True
962
963if __name__ == '__main__':
964 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000965 option_parser.add_option("-f", '--ftp', action='store_const',
966 const=SERVER_FTP, default=SERVER_HTTP,
967 dest='server_type',
968 help='FTP or HTTP server default HTTP')
initial.commit94958cf2008-07-26 22:42:52 +0000969 option_parser.add_option('', '--port', default='8888', type='int',
970 help='Port used by the server')
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000971 option_parser.add_option('', '--data-dir', dest='data_dir',
initial.commit94958cf2008-07-26 22:42:52 +0000972 help='Directory from which to read the files')
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000973 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +0000974 help='Specify that https should be used, specify '
975 'the path to the cert containing the private key '
976 'the server should use')
977 options, args = option_parser.parse_args()
978
979 sys.exit(main(options, args))
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000980