blob: acbfd6774e7edc268962a66c6e83f90433915ad7 [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
stoyan@chromium.org372692c2009-01-30 17:01:52 +000022import shutil
initial.commit94958cf2008-07-26 22:42:52 +000023import SocketServer
24import sys
25import time
26import tlslite
27import tlslite.api
erikkay@google.comd5182ff2009-01-08 20:45:27 +000028import pyftpdlib.ftpserver
29
30SERVER_HTTP = 0
31SERVER_FTP = 1
initial.commit94958cf2008-07-26 22:42:52 +000032
33debug_output = sys.stderr
34def debug(str):
35 debug_output.write(str + "\n")
36 debug_output.flush()
37
38class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
39 """This is a specialization of of BaseHTTPServer to allow it
40 to be exited cleanly (by setting its "stop" member to True)."""
41
42 def serve_forever(self):
43 self.stop = False
44 self.nonce = None
45 while not self.stop:
46 self.handle_request()
47 self.socket.close()
48
49class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
50 """This is a specialization of StoppableHTTPerver that add https support."""
51
52 def __init__(self, server_address, request_hander_class, cert_path):
53 s = open(cert_path).read()
54 x509 = tlslite.api.X509()
55 x509.parse(s)
56 self.cert_chain = tlslite.api.X509CertChain([x509])
57 s = open(cert_path).read()
58 self.private_key = tlslite.api.parsePEMKey(s, private=True)
59
60 self.session_cache = tlslite.api.SessionCache()
61 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
62
63 def handshake(self, tlsConnection):
64 """Creates the SSL connection."""
65 try:
66 tlsConnection.handshakeServer(certChain=self.cert_chain,
67 privateKey=self.private_key,
68 sessionCache=self.session_cache)
69 tlsConnection.ignoreAbruptClose = True
70 return True
71 except tlslite.api.TLSError, error:
72 print "Handshake failure:", str(error)
73 return False
74
75class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
76
77 def __init__(self, request, client_address, socket_server):
wtc@chromium.org743d77b2009-02-11 02:48:15 +000078 self._connect_handlers = [
79 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +000080 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +000081 self.DefaultConnectResponseHandler]
initial.commit94958cf2008-07-26 22:42:52 +000082 self._get_handlers = [
83 self.KillHandler,
84 self.NoCacheMaxAgeTimeHandler,
85 self.NoCacheTimeHandler,
86 self.CacheTimeHandler,
87 self.CacheExpiresHandler,
88 self.CacheProxyRevalidateHandler,
89 self.CachePrivateHandler,
90 self.CachePublicHandler,
91 self.CacheSMaxAgeHandler,
92 self.CacheMustRevalidateHandler,
93 self.CacheMustRevalidateMaxAgeHandler,
94 self.CacheNoStoreHandler,
95 self.CacheNoStoreMaxAgeHandler,
96 self.CacheNoTransformHandler,
97 self.DownloadHandler,
98 self.DownloadFinishHandler,
99 self.EchoHeader,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000100 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000101 self.FileHandler,
102 self.RealFileWithCommonHeaderHandler,
103 self.RealBZ2FileWithCommonHeaderHandler,
104 self.AuthBasicHandler,
105 self.AuthDigestHandler,
106 self.SlowServerHandler,
107 self.ContentTypeHandler,
108 self.ServerRedirectHandler,
109 self.ClientRedirectHandler,
110 self.DefaultResponseHandler]
111 self._post_handlers = [
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000112 self.WriteFile,
initial.commit94958cf2008-07-26 22:42:52 +0000113 self.EchoTitleHandler,
114 self.EchoAllHandler,
115 self.EchoHandler] + self._get_handlers
116
117 self._mime_types = { 'gif': 'image/gif', 'jpeg' : 'image/jpeg', 'jpg' : 'image/jpeg' }
118 self._default_mime_type = 'text/html'
119
120 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, socket_server)
121
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000122 def _ShouldHandleRequest(self, handler_name):
123 """Determines if the path can be handled by the handler.
124
125 We consider a handler valid if the path begins with the
126 handler name. It can optionally be followed by "?*", "/*".
127 """
128
129 pattern = re.compile('%s($|\?|/).*' % handler_name)
130 return pattern.match(self.path)
131
initial.commit94958cf2008-07-26 22:42:52 +0000132 def GetMIMETypeFromName(self, file_name):
133 """Returns the mime type for the specified file_name. So far it only looks
134 at the file extension."""
135
136 (shortname, extension) = os.path.splitext(file_name)
137 if len(extension) == 0:
138 # no extension.
139 return self._default_mime_type
140
141 return self._mime_types.get(extension, self._default_mime_type)
142
143 def KillHandler(self):
144 """This request handler kills the server, for use when we're done"
145 with the a particular test."""
146
147 if (self.path.find("kill") < 0):
148 return False
149
150 self.send_response(200)
151 self.send_header('Content-type', 'text/html')
152 self.send_header('Cache-Control', 'max-age=0')
153 self.end_headers()
154 self.wfile.write("Time to die")
155 self.server.stop = True
156
157 return True
158
159 def NoCacheMaxAgeTimeHandler(self):
160 """This request handler yields a page with the title set to the current
161 system time, and no caching requested."""
162
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000163 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000164 return False
165
166 self.send_response(200)
167 self.send_header('Cache-Control', 'max-age=0')
168 self.send_header('Content-type', 'text/html')
169 self.end_headers()
170
171 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
172
173 return True
174
175 def NoCacheTimeHandler(self):
176 """This request handler yields a page with the title set to the current
177 system time, and no caching requested."""
178
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000179 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000180 return False
181
182 self.send_response(200)
183 self.send_header('Cache-Control', 'no-cache')
184 self.send_header('Content-type', 'text/html')
185 self.end_headers()
186
187 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
188
189 return True
190
191 def CacheTimeHandler(self):
192 """This request handler yields a page with the title set to the current
193 system time, and allows caching for one minute."""
194
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000195 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000196 return False
197
198 self.send_response(200)
199 self.send_header('Cache-Control', 'max-age=60')
200 self.send_header('Content-type', 'text/html')
201 self.end_headers()
202
203 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
204
205 return True
206
207 def CacheExpiresHandler(self):
208 """This request handler yields a page with the title set to the current
209 system time, and set the page to expire on 1 Jan 2099."""
210
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000211 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000212 return False
213
214 self.send_response(200)
215 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
216 self.send_header('Content-type', 'text/html')
217 self.end_headers()
218
219 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
220
221 return True
222
223 def CacheProxyRevalidateHandler(self):
224 """This request handler yields a page with the title set to the current
225 system time, and allows caching for 60 seconds"""
226
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000227 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000228 return False
229
230 self.send_response(200)
231 self.send_header('Content-type', 'text/html')
232 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
233 self.end_headers()
234
235 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
236
237 return True
238
239 def CachePrivateHandler(self):
240 """This request handler yields a page with the title set to the current
241 system time, and allows caching for 5 seconds."""
242
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000243 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000244 return False
245
246 self.send_response(200)
247 self.send_header('Content-type', 'text/html')
248 self.send_header('Cache-Control', 'max-age=5, private')
249 self.end_headers()
250
251 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
252
253 return True
254
255 def CachePublicHandler(self):
256 """This request handler yields a page with the title set to the current
257 system time, and allows caching for 5 seconds."""
258
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000259 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000260 return False
261
262 self.send_response(200)
263 self.send_header('Content-type', 'text/html')
264 self.send_header('Cache-Control', 'max-age=5, public')
265 self.end_headers()
266
267 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
268
269 return True
270
271 def CacheSMaxAgeHandler(self):
272 """This request handler yields a page with the title set to the current
273 system time, and does not allow for caching."""
274
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000275 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000276 return False
277
278 self.send_response(200)
279 self.send_header('Content-type', 'text/html')
280 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
281 self.end_headers()
282
283 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
284
285 return True
286
287 def CacheMustRevalidateHandler(self):
288 """This request handler yields a page with the title set to the current
289 system time, and does not allow caching."""
290
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000291 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000292 return False
293
294 self.send_response(200)
295 self.send_header('Content-type', 'text/html')
296 self.send_header('Cache-Control', 'must-revalidate')
297 self.end_headers()
298
299 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
300
301 return True
302
303 def CacheMustRevalidateMaxAgeHandler(self):
304 """This request handler yields a page with the title set to the current
305 system time, and does not allow caching event though max-age of 60
306 seconds is specified."""
307
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000308 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000309 return False
310
311 self.send_response(200)
312 self.send_header('Content-type', 'text/html')
313 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
314 self.end_headers()
315
316 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
317
318 return True
319
initial.commit94958cf2008-07-26 22:42:52 +0000320 def CacheNoStoreHandler(self):
321 """This request handler yields a page with the title set to the current
322 system time, and does not allow the page to be stored."""
323
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000324 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000325 return False
326
327 self.send_response(200)
328 self.send_header('Content-type', 'text/html')
329 self.send_header('Cache-Control', 'no-store')
330 self.end_headers()
331
332 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
333
334 return True
335
336 def CacheNoStoreMaxAgeHandler(self):
337 """This request handler yields a page with the title set to the current
338 system time, and does not allow the page to be stored even though max-age
339 of 60 seconds is specified."""
340
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000341 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000342 return False
343
344 self.send_response(200)
345 self.send_header('Content-type', 'text/html')
346 self.send_header('Cache-Control', 'max-age=60, no-store')
347 self.end_headers()
348
349 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
350
351 return True
352
353
354 def CacheNoTransformHandler(self):
355 """This request handler yields a page with the title set to the current
356 system time, and does not allow the content to transformed during
357 user-agent caching"""
358
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000359 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000360 return False
361
362 self.send_response(200)
363 self.send_header('Content-type', 'text/html')
364 self.send_header('Cache-Control', 'no-transform')
365 self.end_headers()
366
367 self.wfile.write('<html><head><title>%s</title></head></html>' % time.time())
368
369 return True
370
371 def EchoHeader(self):
372 """This handler echoes back the value of a specific request header."""
373
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000374 if not self._ShouldHandleRequest("/echoheader"):
initial.commit94958cf2008-07-26 22:42:52 +0000375 return False
376
377 query_char = self.path.find('?')
378 if query_char != -1:
379 header_name = self.path[query_char+1:]
380
381 self.send_response(200)
382 self.send_header('Content-type', 'text/plain')
383 self.send_header('Cache-control', 'max-age=60000')
384 # insert a vary header to properly indicate that the cachability of this
385 # request is subject to value of the request header being echoed.
386 if len(header_name) > 0:
387 self.send_header('Vary', header_name)
388 self.end_headers()
389
390 if len(header_name) > 0:
391 self.wfile.write(self.headers.getheader(header_name))
392
393 return True
394
395 def EchoHandler(self):
396 """This handler just echoes back the payload of the request, for testing
397 form submission."""
398
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000399 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000400 return False
401
402 self.send_response(200)
403 self.send_header('Content-type', 'text/html')
404 self.end_headers()
405 length = int(self.headers.getheader('content-length'))
406 request = self.rfile.read(length)
407 self.wfile.write(request)
408 return True
409
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000410 def WriteFile(self):
411 """This is handler dumps the content of POST request to a disk file into
412 the data_dir/dump. Sub-directories are not supported."""
413
414 prefix='/writefile/'
415 if not self.path.startswith(prefix):
416 return False
417
418 file_name = self.path[len(prefix):]
419
420 # do not allow fancy chars in file name
421 re.sub('[^a-zA-Z0-9_.-]+', '', file_name)
422 if len(file_name) and file_name[0] != '.':
423 path = os.path.join(self.server.data_dir, 'dump', file_name);
424 length = int(self.headers.getheader('content-length'))
425 request = self.rfile.read(length)
426 f = open(path, "wb")
427 f.write(request);
428 f.close()
429
430 self.send_response(200)
431 self.send_header('Content-type', 'text/html')
432 self.end_headers()
433 self.wfile.write('<html>%s</html>' % file_name)
434 return True
435
initial.commit94958cf2008-07-26 22:42:52 +0000436 def EchoTitleHandler(self):
437 """This handler is like Echo, but sets the page title to the request."""
438
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000439 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000440 return False
441
442 self.send_response(200)
443 self.send_header('Content-type', 'text/html')
444 self.end_headers()
445 length = int(self.headers.getheader('content-length'))
446 request = self.rfile.read(length)
447 self.wfile.write('<html><head><title>')
448 self.wfile.write(request)
449 self.wfile.write('</title></head></html>')
450 return True
451
452 def EchoAllHandler(self):
453 """This handler yields a (more) human-readable page listing information
454 about the request header & contents."""
455
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000456 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000457 return False
458
459 self.send_response(200)
460 self.send_header('Content-type', 'text/html')
461 self.end_headers()
462 self.wfile.write('<html><head><style>'
463 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
464 '</style></head><body>'
465 '<div style="float: right">'
466 '<a href="http://localhost:8888/echo">back to referring page</a></div>'
467 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000468
ericroman@google.coma47622b2008-11-15 04:36:51 +0000469 if self.command == 'POST':
470 length = int(self.headers.getheader('content-length'))
471 qs = self.rfile.read(length)
472 params = cgi.parse_qs(qs, keep_blank_values=1)
473
474 for param in params:
475 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000476
477 self.wfile.write('</pre>')
478
479 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
480
481 self.wfile.write('</body></html>')
482 return True
483
484 def DownloadHandler(self):
485 """This handler sends a downloadable file with or without reporting
486 the size (6K)."""
487
488 if self.path.startswith("/download-unknown-size"):
489 send_length = False
490 elif self.path.startswith("/download-known-size"):
491 send_length = True
492 else:
493 return False
494
495 #
496 # The test which uses this functionality is attempting to send
497 # small chunks of data to the client. Use a fairly large buffer
498 # so that we'll fill chrome's IO buffer enough to force it to
499 # actually write the data.
500 # See also the comments in the client-side of this test in
501 # download_uitest.cc
502 #
503 size_chunk1 = 35*1024
504 size_chunk2 = 10*1024
505
506 self.send_response(200)
507 self.send_header('Content-type', 'application/octet-stream')
508 self.send_header('Cache-Control', 'max-age=0')
509 if send_length:
510 self.send_header('Content-Length', size_chunk1 + size_chunk2)
511 self.end_headers()
512
513 # First chunk of data:
514 self.wfile.write("*" * size_chunk1)
515 self.wfile.flush()
516
517 # handle requests until one of them clears this flag.
518 self.server.waitForDownload = True
519 while self.server.waitForDownload:
520 self.server.handle_request()
521
522 # Second chunk of data:
523 self.wfile.write("*" * size_chunk2)
524 return True
525
526 def DownloadFinishHandler(self):
527 """This handler just tells the server to finish the current download."""
528
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000529 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000530 return False
531
532 self.server.waitForDownload = False
533 self.send_response(200)
534 self.send_header('Content-type', 'text/html')
535 self.send_header('Cache-Control', 'max-age=0')
536 self.end_headers()
537 return True
538
539 def FileHandler(self):
540 """This handler sends the contents of the requested file. Wow, it's like
541 a real webserver!"""
542
543 prefix='/files/'
544 if not self.path.startswith(prefix):
545 return False
546
547 file = self.path[len(prefix):]
548 entries = file.split('/');
549 path = os.path.join(self.server.data_dir, *entries)
550
551 if not os.path.isfile(path):
552 print "File not found " + file + " full path:" + path
553 self.send_error(404)
554 return True
555
556 f = open(path, "rb")
557 data = f.read()
558 f.close()
559
560 # If file.mock-http-headers exists, it contains the headers we
561 # should send. Read them in and parse them.
562 headers_path = path + '.mock-http-headers'
563 if os.path.isfile(headers_path):
564 f = open(headers_path, "r")
565
566 # "HTTP/1.1 200 OK"
567 response = f.readline()
568 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
569 self.send_response(int(status_code))
570
571 for line in f:
572 # "name: value"
573 name, value = re.findall('(\S+):\s*(.*)', line)[0]
574 self.send_header(name, value)
575 f.close()
576 else:
577 # Could be more generic once we support mime-type sniffing, but for
578 # now we need to set it explicitly.
579 self.send_response(200)
580 self.send_header('Content-type', self.GetMIMETypeFromName(file))
581 self.send_header('Content-Length', len(data))
582 self.end_headers()
583
584 self.wfile.write(data)
585
586 return True
587
588 def RealFileWithCommonHeaderHandler(self):
589 """This handler sends the contents of the requested file without the pseudo
590 http head!"""
591
592 prefix='/realfiles/'
593 if not self.path.startswith(prefix):
594 return False
595
596 file = self.path[len(prefix):]
597 path = os.path.join(self.server.data_dir, file)
598
599 try:
600 f = open(path, "rb")
601 data = f.read()
602 f.close()
603
604 # just simply set the MIME as octal stream
605 self.send_response(200)
606 self.send_header('Content-type', 'application/octet-stream')
607 self.end_headers()
608
609 self.wfile.write(data)
610 except:
611 self.send_error(404)
612
613 return True
614
615 def RealBZ2FileWithCommonHeaderHandler(self):
616 """This handler sends the bzip2 contents of the requested file with
617 corresponding Content-Encoding field in http head!"""
618
619 prefix='/realbz2files/'
620 if not self.path.startswith(prefix):
621 return False
622
623 parts = self.path.split('?')
624 file = parts[0][len(prefix):]
625 path = os.path.join(self.server.data_dir, file) + '.bz2'
626
627 if len(parts) > 1:
628 options = parts[1]
629 else:
630 options = ''
631
632 try:
633 self.send_response(200)
634 accept_encoding = self.headers.get("Accept-Encoding")
635 if accept_encoding.find("bzip2") != -1:
636 f = open(path, "rb")
637 data = f.read()
638 f.close()
639 self.send_header('Content-Encoding', 'bzip2')
640 self.send_header('Content-type', 'application/x-bzip2')
641 self.end_headers()
642 if options == 'incremental-header':
643 self.wfile.write(data[:1])
644 self.wfile.flush()
645 time.sleep(1.0)
646 self.wfile.write(data[1:])
647 else:
648 self.wfile.write(data)
649 else:
650 """client do not support bzip2 format, send pseudo content
651 """
652 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
653 self.end_headers()
654 self.wfile.write("you do not support bzip2 encoding")
655 except:
656 self.send_error(404)
657
658 return True
659
660 def AuthBasicHandler(self):
661 """This handler tests 'Basic' authentication. It just sends a page with
662 title 'user/pass' if you succeed."""
663
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000664 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000665 return False
666
667 username = userpass = password = b64str = ""
668
669 auth = self.headers.getheader('authorization')
670 try:
671 if not auth:
672 raise Exception('no auth')
673 b64str = re.findall(r'Basic (\S+)', auth)[0]
674 userpass = base64.b64decode(b64str)
675 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
676 if password != 'secret':
677 raise Exception('wrong password')
678 except Exception, e:
679 # Authentication failed.
680 self.send_response(401)
681 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
682 self.send_header('Content-type', 'text/html')
683 self.end_headers()
684 self.wfile.write('<html><head>')
685 self.wfile.write('<title>Denied: %s</title>' % e)
686 self.wfile.write('</head><body>')
687 self.wfile.write('auth=%s<p>' % auth)
688 self.wfile.write('b64str=%s<p>' % b64str)
689 self.wfile.write('username: %s<p>' % username)
690 self.wfile.write('userpass: %s<p>' % userpass)
691 self.wfile.write('password: %s<p>' % password)
692 self.wfile.write('You sent:<br>%s<p>' % self.headers)
693 self.wfile.write('</body></html>')
694 return True
695
696 # Authentication successful. (Return a cachable response to allow for
697 # testing cached pages that require authentication.)
698 if_none_match = self.headers.getheader('if-none-match')
699 if if_none_match == "abc":
700 self.send_response(304)
701 self.end_headers()
702 else:
703 self.send_response(200)
704 self.send_header('Content-type', 'text/html')
705 self.send_header('Cache-control', 'max-age=60000')
706 self.send_header('Etag', 'abc')
707 self.end_headers()
708 self.wfile.write('<html><head>')
709 self.wfile.write('<title>%s/%s</title>' % (username, password))
710 self.wfile.write('</head><body>')
711 self.wfile.write('auth=%s<p>' % auth)
712 self.wfile.write('</body></html>')
713
714 return True
715
716 def AuthDigestHandler(self):
717 """This handler tests 'Digest' authentication. It just sends a page with
718 title 'user/pass' if you succeed."""
719
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000720 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000721 return False
722
723 # Periodically generate a new nonce. Technically we should incorporate
724 # the request URL into this, but we don't care for testing.
725 nonce_life = 10
726 stale = False
727 if not self.server.nonce or (time.time() - self.server.nonce_time > nonce_life):
728 if self.server.nonce:
729 stale = True
730 self.server.nonce_time = time.time()
731 self.server.nonce = \
732 md5.new(time.ctime(self.server.nonce_time) + 'privatekey').hexdigest()
733
734 nonce = self.server.nonce
735 opaque = md5.new('opaque').hexdigest()
736 password = 'secret'
737 realm = 'testrealm'
738
739 auth = self.headers.getheader('authorization')
740 pairs = {}
741 try:
742 if not auth:
743 raise Exception('no auth')
744 if not auth.startswith('Digest'):
745 raise Exception('not digest')
746 # Pull out all the name="value" pairs as a dictionary.
747 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
748
749 # Make sure it's all valid.
750 if pairs['nonce'] != nonce:
751 raise Exception('wrong nonce')
752 if pairs['opaque'] != opaque:
753 raise Exception('wrong opaque')
754
755 # Check the 'response' value and make sure it matches our magic hash.
756 # See http://www.ietf.org/rfc/rfc2617.txt
757 hash_a1 = md5.new(':'.join([pairs['username'], realm, password])).hexdigest()
758 hash_a2 = md5.new(':'.join([self.command, pairs['uri']])).hexdigest()
759 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
760 response = md5.new(':'.join([hash_a1, nonce, pairs['nc'],
761 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
762 else:
763 response = md5.new(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
764
765 if pairs['response'] != response:
766 raise Exception('wrong password')
767 except Exception, e:
768 # Authentication failed.
769 self.send_response(401)
770 hdr = ('Digest '
771 'realm="%s", '
772 'domain="/", '
773 'qop="auth", '
774 'algorithm=MD5, '
775 'nonce="%s", '
776 'opaque="%s"') % (realm, nonce, opaque)
777 if stale:
778 hdr += ', stale="TRUE"'
779 self.send_header('WWW-Authenticate', hdr)
780 self.send_header('Content-type', 'text/html')
781 self.end_headers()
782 self.wfile.write('<html><head>')
783 self.wfile.write('<title>Denied: %s</title>' % e)
784 self.wfile.write('</head><body>')
785 self.wfile.write('auth=%s<p>' % auth)
786 self.wfile.write('pairs=%s<p>' % pairs)
787 self.wfile.write('You sent:<br>%s<p>' % self.headers)
788 self.wfile.write('We are replying:<br>%s<p>' % hdr)
789 self.wfile.write('</body></html>')
790 return True
791
792 # Authentication successful.
793 self.send_response(200)
794 self.send_header('Content-type', 'text/html')
795 self.end_headers()
796 self.wfile.write('<html><head>')
797 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
798 self.wfile.write('</head><body>')
799 self.wfile.write('auth=%s<p>' % auth)
800 self.wfile.write('pairs=%s<p>' % pairs)
801 self.wfile.write('</body></html>')
802
803 return True
804
805 def SlowServerHandler(self):
806 """Wait for the user suggested time before responding. The syntax is
807 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000808 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000809 return False
810 query_char = self.path.find('?')
811 wait_sec = 1.0
812 if query_char >= 0:
813 try:
814 wait_sec = int(self.path[query_char + 1:])
815 except ValueError:
816 pass
817 time.sleep(wait_sec)
818 self.send_response(200)
819 self.send_header('Content-type', 'text/plain')
820 self.end_headers()
821 self.wfile.write("waited %d seconds" % wait_sec)
822 return True
823
824 def ContentTypeHandler(self):
825 """Returns a string of html with the given content type. E.g.,
826 /contenttype?text/css returns an html file with the Content-Type
827 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000828 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +0000829 return False
830 query_char = self.path.find('?')
831 content_type = self.path[query_char + 1:].strip()
832 if not content_type:
833 content_type = 'text/html'
834 self.send_response(200)
835 self.send_header('Content-Type', content_type)
836 self.end_headers()
837 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
838 return True
839
840 def ServerRedirectHandler(self):
841 """Sends a server redirect to the given URL. The syntax is
842 '/server-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
843
844 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000845 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000846 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(301) # moved permanently
855 self.send_header('Location', dest)
856 self.send_header('Content-type', 'text/html')
857 self.end_headers()
858 self.wfile.write('<html><head>')
859 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
860
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000861 return True
initial.commit94958cf2008-07-26 22:42:52 +0000862
863 def ClientRedirectHandler(self):
864 """Sends a client redirect to the given URL. The syntax is
865 '/client-redirect?http://foo.bar/asdf' to redirect to 'http://foo.bar/asdf'"""
866
867 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000868 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000869 return False
870
871 query_char = self.path.find('?');
872 if query_char < 0 or len(self.path) <= query_char + 1:
873 self.sendRedirectHelp(test_name)
874 return True
875 dest = self.path[query_char + 1:]
876
877 self.send_response(200)
878 self.send_header('Content-type', 'text/html')
879 self.end_headers()
880 self.wfile.write('<html><head>')
881 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
882 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
883
884 return True
885
886 def DefaultResponseHandler(self):
887 """This is the catch-all response handler for requests that aren't handled
888 by one of the special handlers above.
889 Note that we specify the content-length as without it the https connection
890 is not closed properly (and the browser keeps expecting data)."""
891
892 contents = "Default response given for path: " + self.path
893 self.send_response(200)
894 self.send_header('Content-type', 'text/html')
895 self.send_header("Content-Length", len(contents))
896 self.end_headers()
897 self.wfile.write(contents)
898 return True
899
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000900 def RedirectConnectHandler(self):
901 """Sends a redirect to the CONNECT request for www.redirect.com. This
902 response is not specified by the RFC, so the browser should not follow
903 the redirect."""
904
905 if (self.path.find("www.redirect.com") < 0):
906 return False
907
908 dest = "http://www.destination.com/foo.js"
909
910 self.send_response(302) # moved temporarily
911 self.send_header('Location', dest)
912 self.send_header('Connection', 'close')
913 self.end_headers()
914 return True
915
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000916 def ServerAuthConnectHandler(self):
917 """Sends a 401 to the CONNECT request for www.server-auth.com. This
918 response doesn't make sense because the proxy server cannot request
919 server authentication."""
920
921 if (self.path.find("www.server-auth.com") < 0):
922 return False
923
924 challenge = 'Basic realm="WallyWorld"'
925
926 self.send_response(401) # unauthorized
927 self.send_header('WWW-Authenticate', challenge)
928 self.send_header('Connection', 'close')
929 self.end_headers()
930 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000931
932 def DefaultConnectResponseHandler(self):
933 """This is the catch-all response handler for CONNECT requests that aren't
934 handled by one of the special handlers above. Real Web servers respond
935 with 400 to CONNECT requests."""
936
937 contents = "Your client has issued a malformed or illegal request."
938 self.send_response(400) # bad request
939 self.send_header('Content-type', 'text/html')
940 self.send_header("Content-Length", len(contents))
941 self.end_headers()
942 self.wfile.write(contents)
943 return True
944
945 def do_CONNECT(self):
946 for handler in self._connect_handlers:
947 if handler():
948 return
949
initial.commit94958cf2008-07-26 22:42:52 +0000950 def do_GET(self):
951 for handler in self._get_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000952 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000953 return
954
955 def do_POST(self):
956 for handler in self._post_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000957 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000958 return
959
960 # called by the redirect handling function when there is no parameter
961 def sendRedirectHelp(self, redirect_name):
962 self.send_response(200)
963 self.send_header('Content-type', 'text/html')
964 self.end_headers()
965 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
966 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
967 self.wfile.write('</body></html>')
968
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000969def MakeDumpDir(data_dir):
970 """Create directory named 'dump' where uploaded data via HTTP POST request
971 will be stored. If the directory already exists all files and subdirectories
972 will be deleted."""
973 dump_dir = os.path.join(data_dir, 'dump');
974 if os.path.isdir(dump_dir):
975 shutil.rmtree(dump_dir)
976 os.mkdir(dump_dir)
977
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000978def MakeDataDir():
979 if options.data_dir:
980 if not os.path.isdir(options.data_dir):
981 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
982 return None
983 my_data_dir = options.data_dir
984 else:
985 # Create the default path to our data dir, relative to the exe dir.
986 my_data_dir = os.path.dirname(sys.argv[0])
987 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
988 "test", "data")
989
990 #TODO(ibrar): Must use Find* funtion defined in google\tools
991 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
992
993 return my_data_dir
994
initial.commit94958cf2008-07-26 22:42:52 +0000995def main(options, args):
996 # redirect output to a log file so it doesn't spam the unit test output
997 logfile = open('testserver.log', 'w')
998 sys.stderr = sys.stdout = logfile
999
1000 port = options.port
1001
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001002 if options.server_type == SERVER_HTTP:
1003 if options.cert:
1004 # let's make sure the cert file exists.
1005 if not os.path.isfile(options.cert):
1006 print 'specified cert file not found: ' + options.cert + ' exiting...'
1007 return
1008 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
1009 print 'HTTPS server started on port %d...' % port
1010 else:
1011 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
1012 print 'HTTP server started on port %d...' % port
erikkay@google.com70397b62008-12-30 21:49:21 +00001013
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001014 server.data_dir = MakeDataDir()
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001015 MakeDumpDir(server.data_dir)
1016
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001017 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001018 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001019 my_data_dir = MakeDataDir()
1020
1021 def line_logger(msg):
1022 if (msg.find("kill") >= 0):
1023 server.stop = True
1024 print 'shutting down server'
1025 sys.exit(0)
1026
1027 # Instantiate a dummy authorizer for managing 'virtual' users
1028 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1029
1030 # Define a new user having full r/w permissions and a read-only
1031 # anonymous user
1032 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1033
1034 authorizer.add_anonymous(my_data_dir)
1035
1036 # Instantiate FTP handler class
1037 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1038 ftp_handler.authorizer = authorizer
1039 pyftpdlib.ftpserver.logline = line_logger
1040
1041 # Define a customized banner (string returned when client connects)
1042 ftp_handler.banner = "pyftpdlib %s based ftpd ready." % pyftpdlib.ftpserver.__ver__
1043
1044 # Instantiate FTP server class and listen to 127.0.0.1:port
1045 address = ('127.0.0.1', port)
1046 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
1047 print 'FTP server started on port %d...' % port
initial.commit94958cf2008-07-26 22:42:52 +00001048
1049 try:
1050 server.serve_forever()
1051 except KeyboardInterrupt:
1052 print 'shutting down server'
1053 server.stop = True
1054
1055if __name__ == '__main__':
1056 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001057 option_parser.add_option("-f", '--ftp', action='store_const',
1058 const=SERVER_FTP, default=SERVER_HTTP,
1059 dest='server_type',
1060 help='FTP or HTTP server default HTTP')
initial.commit94958cf2008-07-26 22:42:52 +00001061 option_parser.add_option('', '--port', default='8888', type='int',
1062 help='Port used by the server')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001063 option_parser.add_option('', '--data-dir', dest='data_dir',
initial.commit94958cf2008-07-26 22:42:52 +00001064 help='Directory from which to read the files')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001065 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001066 help='Specify that https should be used, specify '
1067 'the path to the cert containing the private key '
1068 'the server should use')
1069 options, args = option_parser.parse_args()
1070
1071 sys.exit(main(options, args))
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001072