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