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