blob: a27b534ff1ef3fbb7ce3c38006d8fdd4f5b7f24a [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
initial.commit94958cf2008-07-26 22:42:52 +000018import optparse
19import os
20import re
stoyan@chromium.org372692c2009-01-30 17:01:52 +000021import shutil
initial.commit94958cf2008-07-26 22:42:52 +000022import SocketServer
23import sys
24import time
25import tlslite
26import tlslite.api
erikkay@google.comd5182ff2009-01-08 20:45:27 +000027import pyftpdlib.ftpserver
28
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +000029try:
30 import hashlib
31 _new_md5 = hashlib.md5
32except ImportError:
33 import md5
34 _new_md5 = md5.new
35
maruel@chromium.org756cf982009-03-05 12:46:38 +000036SERVER_HTTP = 0
erikkay@google.comd5182ff2009-01-08 20:45:27 +000037SERVER_FTP = 1
initial.commit94958cf2008-07-26 22:42:52 +000038
39debug_output = sys.stderr
40def debug(str):
41 debug_output.write(str + "\n")
42 debug_output.flush()
43
44class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
45 """This is a specialization of of BaseHTTPServer to allow it
46 to be exited cleanly (by setting its "stop" member to True)."""
47
48 def serve_forever(self):
49 self.stop = False
50 self.nonce = None
51 while not self.stop:
52 self.handle_request()
53 self.socket.close()
54
55class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
56 """This is a specialization of StoppableHTTPerver that add https support."""
57
58 def __init__(self, server_address, request_hander_class, cert_path):
59 s = open(cert_path).read()
60 x509 = tlslite.api.X509()
61 x509.parse(s)
62 self.cert_chain = tlslite.api.X509CertChain([x509])
63 s = open(cert_path).read()
64 self.private_key = tlslite.api.parsePEMKey(s, private=True)
65
66 self.session_cache = tlslite.api.SessionCache()
67 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
68
69 def handshake(self, tlsConnection):
70 """Creates the SSL connection."""
71 try:
72 tlsConnection.handshakeServer(certChain=self.cert_chain,
73 privateKey=self.private_key,
74 sessionCache=self.session_cache)
75 tlsConnection.ignoreAbruptClose = True
76 return True
77 except tlslite.api.TLSError, error:
78 print "Handshake failure:", str(error)
79 return False
80
81class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
82
83 def __init__(self, request, client_address, socket_server):
wtc@chromium.org743d77b2009-02-11 02:48:15 +000084 self._connect_handlers = [
85 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +000086 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +000087 self.DefaultConnectResponseHandler]
initial.commit94958cf2008-07-26 22:42:52 +000088 self._get_handlers = [
89 self.KillHandler,
90 self.NoCacheMaxAgeTimeHandler,
91 self.NoCacheTimeHandler,
92 self.CacheTimeHandler,
93 self.CacheExpiresHandler,
94 self.CacheProxyRevalidateHandler,
95 self.CachePrivateHandler,
96 self.CachePublicHandler,
97 self.CacheSMaxAgeHandler,
98 self.CacheMustRevalidateHandler,
99 self.CacheMustRevalidateMaxAgeHandler,
100 self.CacheNoStoreHandler,
101 self.CacheNoStoreMaxAgeHandler,
102 self.CacheNoTransformHandler,
103 self.DownloadHandler,
104 self.DownloadFinishHandler,
105 self.EchoHeader,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000106 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000107 self.FileHandler,
108 self.RealFileWithCommonHeaderHandler,
109 self.RealBZ2FileWithCommonHeaderHandler,
110 self.AuthBasicHandler,
111 self.AuthDigestHandler,
112 self.SlowServerHandler,
113 self.ContentTypeHandler,
114 self.ServerRedirectHandler,
115 self.ClientRedirectHandler,
116 self.DefaultResponseHandler]
117 self._post_handlers = [
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000118 self.WriteFile,
initial.commit94958cf2008-07-26 22:42:52 +0000119 self.EchoTitleHandler,
120 self.EchoAllHandler,
121 self.EchoHandler] + self._get_handlers
122
maruel@google.come250a9b2009-03-10 17:39:46 +0000123 self._mime_types = {
124 'gif': 'image/gif',
125 'jpeg' : 'image/jpeg',
126 'jpg' : 'image/jpeg'
127 }
initial.commit94958cf2008-07-26 22:42:52 +0000128 self._default_mime_type = 'text/html'
129
maruel@google.come250a9b2009-03-10 17:39:46 +0000130 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
131 client_address,
132 socket_server)
initial.commit94958cf2008-07-26 22:42:52 +0000133
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000134 def _ShouldHandleRequest(self, handler_name):
135 """Determines if the path can be handled by the handler.
136
137 We consider a handler valid if the path begins with the
138 handler name. It can optionally be followed by "?*", "/*".
139 """
140
141 pattern = re.compile('%s($|\?|/).*' % handler_name)
142 return pattern.match(self.path)
143
initial.commit94958cf2008-07-26 22:42:52 +0000144 def GetMIMETypeFromName(self, file_name):
145 """Returns the mime type for the specified file_name. So far it only looks
146 at the file extension."""
147
148 (shortname, extension) = os.path.splitext(file_name)
149 if len(extension) == 0:
150 # no extension.
151 return self._default_mime_type
152
ericroman@google.comc17ca532009-05-07 03:51:05 +0000153 # extension starts with a dot, so we need to remove it
154 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000155
156 def KillHandler(self):
157 """This request handler kills the server, for use when we're done"
158 with the a particular test."""
159
160 if (self.path.find("kill") < 0):
161 return False
162
163 self.send_response(200)
164 self.send_header('Content-type', 'text/html')
165 self.send_header('Cache-Control', 'max-age=0')
166 self.end_headers()
167 self.wfile.write("Time to die")
168 self.server.stop = True
169
170 return True
171
172 def NoCacheMaxAgeTimeHandler(self):
173 """This request handler yields a page with the title set to the current
174 system time, and no caching requested."""
175
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000176 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000177 return False
178
179 self.send_response(200)
180 self.send_header('Cache-Control', 'max-age=0')
181 self.send_header('Content-type', 'text/html')
182 self.end_headers()
183
maruel@google.come250a9b2009-03-10 17:39:46 +0000184 self.wfile.write('<html><head><title>%s</title></head></html>' %
185 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000186
187 return True
188
189 def NoCacheTimeHandler(self):
190 """This request handler yields a page with the title set to the current
191 system time, and no caching requested."""
192
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000193 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000194 return False
195
196 self.send_response(200)
197 self.send_header('Cache-Control', 'no-cache')
198 self.send_header('Content-type', 'text/html')
199 self.end_headers()
200
maruel@google.come250a9b2009-03-10 17:39:46 +0000201 self.wfile.write('<html><head><title>%s</title></head></html>' %
202 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000203
204 return True
205
206 def CacheTimeHandler(self):
207 """This request handler yields a page with the title set to the current
208 system time, and allows caching for one minute."""
209
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000210 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000211 return False
212
213 self.send_response(200)
214 self.send_header('Cache-Control', 'max-age=60')
215 self.send_header('Content-type', 'text/html')
216 self.end_headers()
217
maruel@google.come250a9b2009-03-10 17:39:46 +0000218 self.wfile.write('<html><head><title>%s</title></head></html>' %
219 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000220
221 return True
222
223 def CacheExpiresHandler(self):
224 """This request handler yields a page with the title set to the current
225 system time, and set the page to expire on 1 Jan 2099."""
226
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000227 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000228 return False
229
230 self.send_response(200)
231 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
232 self.send_header('Content-type', 'text/html')
233 self.end_headers()
234
maruel@google.come250a9b2009-03-10 17:39:46 +0000235 self.wfile.write('<html><head><title>%s</title></head></html>' %
236 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000237
238 return True
239
240 def CacheProxyRevalidateHandler(self):
241 """This request handler yields a page with the title set to the current
242 system time, and allows caching for 60 seconds"""
243
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000244 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000245 return False
246
247 self.send_response(200)
248 self.send_header('Content-type', 'text/html')
249 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
250 self.end_headers()
251
maruel@google.come250a9b2009-03-10 17:39:46 +0000252 self.wfile.write('<html><head><title>%s</title></head></html>' %
253 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000254
255 return True
256
257 def CachePrivateHandler(self):
258 """This request handler yields a page with the title set to the current
259 system time, and allows caching for 5 seconds."""
260
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000261 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000262 return False
263
264 self.send_response(200)
265 self.send_header('Content-type', 'text/html')
266 self.send_header('Cache-Control', 'max-age=5, private')
267 self.end_headers()
268
maruel@google.come250a9b2009-03-10 17:39:46 +0000269 self.wfile.write('<html><head><title>%s</title></head></html>' %
270 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000271
272 return True
273
274 def CachePublicHandler(self):
275 """This request handler yields a page with the title set to the current
276 system time, and allows caching for 5 seconds."""
277
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000278 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000279 return False
280
281 self.send_response(200)
282 self.send_header('Content-type', 'text/html')
283 self.send_header('Cache-Control', 'max-age=5, public')
284 self.end_headers()
285
maruel@google.come250a9b2009-03-10 17:39:46 +0000286 self.wfile.write('<html><head><title>%s</title></head></html>' %
287 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000288
289 return True
290
291 def CacheSMaxAgeHandler(self):
292 """This request handler yields a page with the title set to the current
293 system time, and does not allow for caching."""
294
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000295 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000296 return False
297
298 self.send_response(200)
299 self.send_header('Content-type', 'text/html')
300 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
301 self.end_headers()
302
maruel@google.come250a9b2009-03-10 17:39:46 +0000303 self.wfile.write('<html><head><title>%s</title></head></html>' %
304 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000305
306 return True
307
308 def CacheMustRevalidateHandler(self):
309 """This request handler yields a page with the title set to the current
310 system time, and does not allow caching."""
311
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000312 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000313 return False
314
315 self.send_response(200)
316 self.send_header('Content-type', 'text/html')
317 self.send_header('Cache-Control', 'must-revalidate')
318 self.end_headers()
319
maruel@google.come250a9b2009-03-10 17:39:46 +0000320 self.wfile.write('<html><head><title>%s</title></head></html>' %
321 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000322
323 return True
324
325 def CacheMustRevalidateMaxAgeHandler(self):
326 """This request handler yields a page with the title set to the current
327 system time, and does not allow caching event though max-age of 60
328 seconds is specified."""
329
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000330 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000331 return False
332
333 self.send_response(200)
334 self.send_header('Content-type', 'text/html')
335 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
336 self.end_headers()
337
maruel@google.come250a9b2009-03-10 17:39:46 +0000338 self.wfile.write('<html><head><title>%s</title></head></html>' %
339 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000340
341 return True
342
initial.commit94958cf2008-07-26 22:42:52 +0000343 def CacheNoStoreHandler(self):
344 """This request handler yields a page with the title set to the current
345 system time, and does not allow the page to be stored."""
346
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000347 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000348 return False
349
350 self.send_response(200)
351 self.send_header('Content-type', 'text/html')
352 self.send_header('Cache-Control', 'no-store')
353 self.end_headers()
354
maruel@google.come250a9b2009-03-10 17:39:46 +0000355 self.wfile.write('<html><head><title>%s</title></head></html>' %
356 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000357
358 return True
359
360 def CacheNoStoreMaxAgeHandler(self):
361 """This request handler yields a page with the title set to the current
362 system time, and does not allow the page to be stored even though max-age
363 of 60 seconds is specified."""
364
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000365 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000366 return False
367
368 self.send_response(200)
369 self.send_header('Content-type', 'text/html')
370 self.send_header('Cache-Control', 'max-age=60, no-store')
371 self.end_headers()
372
maruel@google.come250a9b2009-03-10 17:39:46 +0000373 self.wfile.write('<html><head><title>%s</title></head></html>' %
374 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000375
376 return True
377
378
379 def CacheNoTransformHandler(self):
380 """This request handler yields a page with the title set to the current
381 system time, and does not allow the content to transformed during
382 user-agent caching"""
383
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000384 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000385 return False
386
387 self.send_response(200)
388 self.send_header('Content-type', 'text/html')
389 self.send_header('Cache-Control', 'no-transform')
390 self.end_headers()
391
maruel@google.come250a9b2009-03-10 17:39:46 +0000392 self.wfile.write('<html><head><title>%s</title></head></html>' %
393 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000394
395 return True
396
397 def EchoHeader(self):
398 """This handler echoes back the value of a specific request header."""
399
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000400 if not self._ShouldHandleRequest("/echoheader"):
initial.commit94958cf2008-07-26 22:42:52 +0000401 return False
402
403 query_char = self.path.find('?')
404 if query_char != -1:
405 header_name = self.path[query_char+1:]
406
407 self.send_response(200)
408 self.send_header('Content-type', 'text/plain')
409 self.send_header('Cache-control', 'max-age=60000')
410 # insert a vary header to properly indicate that the cachability of this
411 # request is subject to value of the request header being echoed.
412 if len(header_name) > 0:
413 self.send_header('Vary', header_name)
414 self.end_headers()
415
416 if len(header_name) > 0:
417 self.wfile.write(self.headers.getheader(header_name))
418
419 return True
420
421 def EchoHandler(self):
422 """This handler just echoes back the payload of the request, for testing
423 form submission."""
424
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000425 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000426 return False
427
428 self.send_response(200)
429 self.send_header('Content-type', 'text/html')
430 self.end_headers()
431 length = int(self.headers.getheader('content-length'))
432 request = self.rfile.read(length)
433 self.wfile.write(request)
434 return True
435
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000436 def WriteFile(self):
437 """This is handler dumps the content of POST request to a disk file into
438 the data_dir/dump. Sub-directories are not supported."""
maruel@chromium.org756cf982009-03-05 12:46:38 +0000439
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000440 prefix='/writefile/'
441 if not self.path.startswith(prefix):
442 return False
maruel@chromium.org756cf982009-03-05 12:46:38 +0000443
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000444 file_name = self.path[len(prefix):]
445
446 # do not allow fancy chars in file name
447 re.sub('[^a-zA-Z0-9_.-]+', '', file_name)
448 if len(file_name) and file_name[0] != '.':
449 path = os.path.join(self.server.data_dir, 'dump', file_name);
450 length = int(self.headers.getheader('content-length'))
451 request = self.rfile.read(length)
452 f = open(path, "wb")
453 f.write(request);
454 f.close()
maruel@chromium.org756cf982009-03-05 12:46:38 +0000455
stoyan@chromium.org372692c2009-01-30 17:01:52 +0000456 self.send_response(200)
457 self.send_header('Content-type', 'text/html')
458 self.end_headers()
459 self.wfile.write('<html>%s</html>' % file_name)
460 return True
maruel@chromium.org756cf982009-03-05 12:46:38 +0000461
initial.commit94958cf2008-07-26 22:42:52 +0000462 def EchoTitleHandler(self):
463 """This handler is like Echo, but sets the page title to the request."""
464
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000465 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000466 return False
467
468 self.send_response(200)
469 self.send_header('Content-type', 'text/html')
470 self.end_headers()
471 length = int(self.headers.getheader('content-length'))
472 request = self.rfile.read(length)
473 self.wfile.write('<html><head><title>')
474 self.wfile.write(request)
475 self.wfile.write('</title></head></html>')
476 return True
477
478 def EchoAllHandler(self):
479 """This handler yields a (more) human-readable page listing information
480 about the request header & contents."""
481
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000482 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000483 return False
484
485 self.send_response(200)
486 self.send_header('Content-type', 'text/html')
487 self.end_headers()
488 self.wfile.write('<html><head><style>'
489 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
490 '</style></head><body>'
491 '<div style="float: right">'
492 '<a href="http://localhost:8888/echo">back to referring page</a></div>'
493 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000494
ericroman@google.coma47622b2008-11-15 04:36:51 +0000495 if self.command == 'POST':
496 length = int(self.headers.getheader('content-length'))
497 qs = self.rfile.read(length)
498 params = cgi.parse_qs(qs, keep_blank_values=1)
499
500 for param in params:
501 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000502
503 self.wfile.write('</pre>')
504
505 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
506
507 self.wfile.write('</body></html>')
508 return True
509
510 def DownloadHandler(self):
511 """This handler sends a downloadable file with or without reporting
512 the size (6K)."""
513
514 if self.path.startswith("/download-unknown-size"):
515 send_length = False
516 elif self.path.startswith("/download-known-size"):
517 send_length = True
518 else:
519 return False
520
521 #
522 # The test which uses this functionality is attempting to send
523 # small chunks of data to the client. Use a fairly large buffer
524 # so that we'll fill chrome's IO buffer enough to force it to
525 # actually write the data.
526 # See also the comments in the client-side of this test in
527 # download_uitest.cc
528 #
529 size_chunk1 = 35*1024
530 size_chunk2 = 10*1024
531
532 self.send_response(200)
533 self.send_header('Content-type', 'application/octet-stream')
534 self.send_header('Cache-Control', 'max-age=0')
535 if send_length:
536 self.send_header('Content-Length', size_chunk1 + size_chunk2)
537 self.end_headers()
538
539 # First chunk of data:
540 self.wfile.write("*" * size_chunk1)
541 self.wfile.flush()
542
543 # handle requests until one of them clears this flag.
544 self.server.waitForDownload = True
545 while self.server.waitForDownload:
546 self.server.handle_request()
547
548 # Second chunk of data:
549 self.wfile.write("*" * size_chunk2)
550 return True
551
552 def DownloadFinishHandler(self):
553 """This handler just tells the server to finish the current download."""
554
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000555 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000556 return False
557
558 self.server.waitForDownload = False
559 self.send_response(200)
560 self.send_header('Content-type', 'text/html')
561 self.send_header('Cache-Control', 'max-age=0')
562 self.end_headers()
563 return True
564
565 def FileHandler(self):
566 """This handler sends the contents of the requested file. Wow, it's like
567 a real webserver!"""
568
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000569 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000570 if not self.path.startswith(prefix):
571 return False
572
573 file = self.path[len(prefix):]
574 entries = file.split('/');
575 path = os.path.join(self.server.data_dir, *entries)
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000576 if os.path.isdir(path):
577 path = os.path.join(path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000578
579 if not os.path.isfile(path):
580 print "File not found " + file + " full path:" + path
581 self.send_error(404)
582 return True
583
584 f = open(path, "rb")
585 data = f.read()
586 f.close()
587
588 # If file.mock-http-headers exists, it contains the headers we
589 # should send. Read them in and parse them.
590 headers_path = path + '.mock-http-headers'
591 if os.path.isfile(headers_path):
592 f = open(headers_path, "r")
593
594 # "HTTP/1.1 200 OK"
595 response = f.readline()
596 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
597 self.send_response(int(status_code))
598
599 for line in f:
600 # "name: value"
601 name, value = re.findall('(\S+):\s*(.*)', line)[0]
602 self.send_header(name, value)
603 f.close()
604 else:
605 # Could be more generic once we support mime-type sniffing, but for
606 # now we need to set it explicitly.
607 self.send_response(200)
608 self.send_header('Content-type', self.GetMIMETypeFromName(file))
609 self.send_header('Content-Length', len(data))
610 self.end_headers()
611
612 self.wfile.write(data)
613
614 return True
615
616 def RealFileWithCommonHeaderHandler(self):
617 """This handler sends the contents of the requested file without the pseudo
618 http head!"""
619
620 prefix='/realfiles/'
621 if not self.path.startswith(prefix):
622 return False
623
624 file = self.path[len(prefix):]
625 path = os.path.join(self.server.data_dir, file)
626
627 try:
628 f = open(path, "rb")
629 data = f.read()
630 f.close()
631
632 # just simply set the MIME as octal stream
633 self.send_response(200)
634 self.send_header('Content-type', 'application/octet-stream')
635 self.end_headers()
636
637 self.wfile.write(data)
638 except:
639 self.send_error(404)
640
641 return True
642
643 def RealBZ2FileWithCommonHeaderHandler(self):
644 """This handler sends the bzip2 contents of the requested file with
645 corresponding Content-Encoding field in http head!"""
646
647 prefix='/realbz2files/'
648 if not self.path.startswith(prefix):
649 return False
650
651 parts = self.path.split('?')
652 file = parts[0][len(prefix):]
653 path = os.path.join(self.server.data_dir, file) + '.bz2'
654
655 if len(parts) > 1:
656 options = parts[1]
657 else:
658 options = ''
659
660 try:
661 self.send_response(200)
662 accept_encoding = self.headers.get("Accept-Encoding")
663 if accept_encoding.find("bzip2") != -1:
664 f = open(path, "rb")
665 data = f.read()
666 f.close()
667 self.send_header('Content-Encoding', 'bzip2')
668 self.send_header('Content-type', 'application/x-bzip2')
669 self.end_headers()
670 if options == 'incremental-header':
671 self.wfile.write(data[:1])
672 self.wfile.flush()
673 time.sleep(1.0)
674 self.wfile.write(data[1:])
675 else:
676 self.wfile.write(data)
677 else:
678 """client do not support bzip2 format, send pseudo content
679 """
680 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
681 self.end_headers()
682 self.wfile.write("you do not support bzip2 encoding")
683 except:
684 self.send_error(404)
685
686 return True
687
688 def AuthBasicHandler(self):
689 """This handler tests 'Basic' authentication. It just sends a page with
690 title 'user/pass' if you succeed."""
691
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000692 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000693 return False
694
695 username = userpass = password = b64str = ""
696
ericroman@google.com239b4d82009-03-27 04:00:22 +0000697 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
698
initial.commit94958cf2008-07-26 22:42:52 +0000699 auth = self.headers.getheader('authorization')
700 try:
701 if not auth:
702 raise Exception('no auth')
703 b64str = re.findall(r'Basic (\S+)', auth)[0]
704 userpass = base64.b64decode(b64str)
705 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
706 if password != 'secret':
707 raise Exception('wrong password')
708 except Exception, e:
709 # Authentication failed.
710 self.send_response(401)
711 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
712 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000713 if set_cookie_if_challenged:
714 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000715 self.end_headers()
716 self.wfile.write('<html><head>')
717 self.wfile.write('<title>Denied: %s</title>' % e)
718 self.wfile.write('</head><body>')
719 self.wfile.write('auth=%s<p>' % auth)
720 self.wfile.write('b64str=%s<p>' % b64str)
721 self.wfile.write('username: %s<p>' % username)
722 self.wfile.write('userpass: %s<p>' % userpass)
723 self.wfile.write('password: %s<p>' % password)
724 self.wfile.write('You sent:<br>%s<p>' % self.headers)
725 self.wfile.write('</body></html>')
726 return True
727
728 # Authentication successful. (Return a cachable response to allow for
729 # testing cached pages that require authentication.)
730 if_none_match = self.headers.getheader('if-none-match')
731 if if_none_match == "abc":
732 self.send_response(304)
733 self.end_headers()
734 else:
735 self.send_response(200)
736 self.send_header('Content-type', 'text/html')
737 self.send_header('Cache-control', 'max-age=60000')
738 self.send_header('Etag', 'abc')
739 self.end_headers()
740 self.wfile.write('<html><head>')
741 self.wfile.write('<title>%s/%s</title>' % (username, password))
742 self.wfile.write('</head><body>')
743 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000744 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000745 self.wfile.write('</body></html>')
746
747 return True
748
749 def AuthDigestHandler(self):
750 """This handler tests 'Digest' authentication. It just sends a page with
751 title 'user/pass' if you succeed."""
752
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000753 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000754 return False
755
756 # Periodically generate a new nonce. Technically we should incorporate
757 # the request URL into this, but we don't care for testing.
758 nonce_life = 10
759 stale = False
maruel@google.come250a9b2009-03-10 17:39:46 +0000760 if (not self.server.nonce or
761 (time.time() - self.server.nonce_time > nonce_life)):
initial.commit94958cf2008-07-26 22:42:52 +0000762 if self.server.nonce:
763 stale = True
764 self.server.nonce_time = time.time()
765 self.server.nonce = \
maruel@google.come250a9b2009-03-10 17:39:46 +0000766 _new_md5(time.ctime(self.server.nonce_time) +
767 'privatekey').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000768
769 nonce = self.server.nonce
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000770 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000771 password = 'secret'
772 realm = 'testrealm'
773
774 auth = self.headers.getheader('authorization')
775 pairs = {}
776 try:
777 if not auth:
778 raise Exception('no auth')
779 if not auth.startswith('Digest'):
780 raise Exception('not digest')
781 # Pull out all the name="value" pairs as a dictionary.
782 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
783
784 # Make sure it's all valid.
785 if pairs['nonce'] != nonce:
786 raise Exception('wrong nonce')
787 if pairs['opaque'] != opaque:
788 raise Exception('wrong opaque')
789
790 # Check the 'response' value and make sure it matches our magic hash.
791 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000792 hash_a1 = _new_md5(
793 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000794 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000795 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000796 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000797 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
798 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000799 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000800
801 if pairs['response'] != response:
802 raise Exception('wrong password')
803 except Exception, e:
804 # Authentication failed.
805 self.send_response(401)
806 hdr = ('Digest '
807 'realm="%s", '
808 'domain="/", '
809 'qop="auth", '
810 'algorithm=MD5, '
811 'nonce="%s", '
812 'opaque="%s"') % (realm, nonce, opaque)
813 if stale:
814 hdr += ', stale="TRUE"'
815 self.send_header('WWW-Authenticate', hdr)
816 self.send_header('Content-type', 'text/html')
817 self.end_headers()
818 self.wfile.write('<html><head>')
819 self.wfile.write('<title>Denied: %s</title>' % e)
820 self.wfile.write('</head><body>')
821 self.wfile.write('auth=%s<p>' % auth)
822 self.wfile.write('pairs=%s<p>' % pairs)
823 self.wfile.write('You sent:<br>%s<p>' % self.headers)
824 self.wfile.write('We are replying:<br>%s<p>' % hdr)
825 self.wfile.write('</body></html>')
826 return True
827
828 # Authentication successful.
829 self.send_response(200)
830 self.send_header('Content-type', 'text/html')
831 self.end_headers()
832 self.wfile.write('<html><head>')
833 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
834 self.wfile.write('</head><body>')
835 self.wfile.write('auth=%s<p>' % auth)
836 self.wfile.write('pairs=%s<p>' % pairs)
837 self.wfile.write('</body></html>')
838
839 return True
840
841 def SlowServerHandler(self):
842 """Wait for the user suggested time before responding. The syntax is
843 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000844 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000845 return False
846 query_char = self.path.find('?')
847 wait_sec = 1.0
848 if query_char >= 0:
849 try:
850 wait_sec = int(self.path[query_char + 1:])
851 except ValueError:
852 pass
853 time.sleep(wait_sec)
854 self.send_response(200)
855 self.send_header('Content-type', 'text/plain')
856 self.end_headers()
857 self.wfile.write("waited %d seconds" % wait_sec)
858 return True
859
860 def ContentTypeHandler(self):
861 """Returns a string of html with the given content type. E.g.,
862 /contenttype?text/css returns an html file with the Content-Type
863 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000864 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +0000865 return False
866 query_char = self.path.find('?')
867 content_type = self.path[query_char + 1:].strip()
868 if not content_type:
869 content_type = 'text/html'
870 self.send_response(200)
871 self.send_header('Content-Type', content_type)
872 self.end_headers()
873 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
874 return True
875
876 def ServerRedirectHandler(self):
877 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000878 '/server-redirect?http://foo.bar/asdf' to redirect to
879 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000880
881 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000882 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000883 return False
884
885 query_char = self.path.find('?')
886 if query_char < 0 or len(self.path) <= query_char + 1:
887 self.sendRedirectHelp(test_name)
888 return True
889 dest = self.path[query_char + 1:]
890
891 self.send_response(301) # moved permanently
892 self.send_header('Location', dest)
893 self.send_header('Content-type', 'text/html')
894 self.end_headers()
895 self.wfile.write('<html><head>')
896 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
897
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000898 return True
initial.commit94958cf2008-07-26 22:42:52 +0000899
900 def ClientRedirectHandler(self):
901 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +0000902 '/client-redirect?http://foo.bar/asdf' to redirect to
903 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +0000904
905 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000906 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +0000907 return False
908
909 query_char = self.path.find('?');
910 if query_char < 0 or len(self.path) <= query_char + 1:
911 self.sendRedirectHelp(test_name)
912 return True
913 dest = self.path[query_char + 1:]
914
915 self.send_response(200)
916 self.send_header('Content-type', 'text/html')
917 self.end_headers()
918 self.wfile.write('<html><head>')
919 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
920 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
921
922 return True
923
924 def DefaultResponseHandler(self):
925 """This is the catch-all response handler for requests that aren't handled
926 by one of the special handlers above.
927 Note that we specify the content-length as without it the https connection
928 is not closed properly (and the browser keeps expecting data)."""
929
930 contents = "Default response given for path: " + self.path
931 self.send_response(200)
932 self.send_header('Content-type', 'text/html')
933 self.send_header("Content-Length", len(contents))
934 self.end_headers()
935 self.wfile.write(contents)
936 return True
937
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000938 def RedirectConnectHandler(self):
939 """Sends a redirect to the CONNECT request for www.redirect.com. This
940 response is not specified by the RFC, so the browser should not follow
941 the redirect."""
942
943 if (self.path.find("www.redirect.com") < 0):
944 return False
945
946 dest = "http://www.destination.com/foo.js"
947
948 self.send_response(302) # moved temporarily
949 self.send_header('Location', dest)
950 self.send_header('Connection', 'close')
951 self.end_headers()
952 return True
953
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000954 def ServerAuthConnectHandler(self):
955 """Sends a 401 to the CONNECT request for www.server-auth.com. This
956 response doesn't make sense because the proxy server cannot request
957 server authentication."""
958
959 if (self.path.find("www.server-auth.com") < 0):
960 return False
961
962 challenge = 'Basic realm="WallyWorld"'
963
964 self.send_response(401) # unauthorized
965 self.send_header('WWW-Authenticate', challenge)
966 self.send_header('Connection', 'close')
967 self.end_headers()
968 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000969
970 def DefaultConnectResponseHandler(self):
971 """This is the catch-all response handler for CONNECT requests that aren't
972 handled by one of the special handlers above. Real Web servers respond
973 with 400 to CONNECT requests."""
974
975 contents = "Your client has issued a malformed or illegal request."
976 self.send_response(400) # bad request
977 self.send_header('Content-type', 'text/html')
978 self.send_header("Content-Length", len(contents))
979 self.end_headers()
980 self.wfile.write(contents)
981 return True
982
983 def do_CONNECT(self):
984 for handler in self._connect_handlers:
985 if handler():
986 return
987
initial.commit94958cf2008-07-26 22:42:52 +0000988 def do_GET(self):
989 for handler in self._get_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000990 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000991 return
992
993 def do_POST(self):
994 for handler in self._post_handlers:
erikkay@google.comd5182ff2009-01-08 20:45:27 +0000995 if handler():
initial.commit94958cf2008-07-26 22:42:52 +0000996 return
997
998 # called by the redirect handling function when there is no parameter
999 def sendRedirectHelp(self, redirect_name):
1000 self.send_response(200)
1001 self.send_header('Content-type', 'text/html')
1002 self.end_headers()
1003 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1004 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1005 self.wfile.write('</body></html>')
1006
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001007def MakeDumpDir(data_dir):
1008 """Create directory named 'dump' where uploaded data via HTTP POST request
1009 will be stored. If the directory already exists all files and subdirectories
1010 will be deleted."""
1011 dump_dir = os.path.join(data_dir, 'dump');
1012 if os.path.isdir(dump_dir):
1013 shutil.rmtree(dump_dir)
1014 os.mkdir(dump_dir)
1015
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001016def MakeDataDir():
1017 if options.data_dir:
1018 if not os.path.isdir(options.data_dir):
1019 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1020 return None
1021 my_data_dir = options.data_dir
1022 else:
1023 # Create the default path to our data dir, relative to the exe dir.
1024 my_data_dir = os.path.dirname(sys.argv[0])
1025 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
1026 "test", "data")
1027
1028 #TODO(ibrar): Must use Find* funtion defined in google\tools
1029 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1030
1031 return my_data_dir
1032
initial.commit94958cf2008-07-26 22:42:52 +00001033def main(options, args):
1034 # redirect output to a log file so it doesn't spam the unit test output
1035 logfile = open('testserver.log', 'w')
1036 sys.stderr = sys.stdout = logfile
1037
1038 port = options.port
1039
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001040 if options.server_type == SERVER_HTTP:
1041 if options.cert:
1042 # let's make sure the cert file exists.
1043 if not os.path.isfile(options.cert):
1044 print 'specified cert file not found: ' + options.cert + ' exiting...'
1045 return
1046 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert)
1047 print 'HTTPS server started on port %d...' % port
1048 else:
1049 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
1050 print 'HTTP server started on port %d...' % port
erikkay@google.com70397b62008-12-30 21:49:21 +00001051
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001052 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001053 server.file_root_url = options.file_root_url
stoyan@chromium.org372692c2009-01-30 17:01:52 +00001054 MakeDumpDir(server.data_dir)
maruel@chromium.org756cf982009-03-05 12:46:38 +00001055
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001056 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001057 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001058 my_data_dir = MakeDataDir()
1059
1060 def line_logger(msg):
1061 if (msg.find("kill") >= 0):
1062 server.stop = True
1063 print 'shutting down server'
1064 sys.exit(0)
1065
1066 # Instantiate a dummy authorizer for managing 'virtual' users
1067 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1068
1069 # Define a new user having full r/w permissions and a read-only
1070 # anonymous user
1071 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1072
1073 authorizer.add_anonymous(my_data_dir)
1074
1075 # Instantiate FTP handler class
1076 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1077 ftp_handler.authorizer = authorizer
1078 pyftpdlib.ftpserver.logline = line_logger
1079
1080 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001081 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1082 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001083
1084 # Instantiate FTP server class and listen to 127.0.0.1:port
1085 address = ('127.0.0.1', port)
1086 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
1087 print 'FTP server started on port %d...' % port
initial.commit94958cf2008-07-26 22:42:52 +00001088
1089 try:
1090 server.serve_forever()
1091 except KeyboardInterrupt:
1092 print 'shutting down server'
1093 server.stop = True
1094
1095if __name__ == '__main__':
1096 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001097 option_parser.add_option("-f", '--ftp', action='store_const',
1098 const=SERVER_FTP, default=SERVER_HTTP,
1099 dest='server_type',
1100 help='FTP or HTTP server default HTTP')
initial.commit94958cf2008-07-26 22:42:52 +00001101 option_parser.add_option('', '--port', default='8888', type='int',
1102 help='Port used by the server')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001103 option_parser.add_option('', '--data-dir', dest='data_dir',
initial.commit94958cf2008-07-26 22:42:52 +00001104 help='Directory from which to read the files')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001105 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001106 help='Specify that https should be used, specify '
1107 'the path to the cert containing the private key '
1108 'the server should use')
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001109 option_parser.add_option('', '--file-root-url', default='/files/',
1110 help='Specify a root URL for files served.')
initial.commit94958cf2008-07-26 22:42:52 +00001111 options, args = option_parser.parse_args()
1112
1113 sys.exit(main(options, args))