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