blob: e3e1176cc9a0e03b5c5dd3165df784bb4fcd2fa4 [file] [log] [blame]
initial.commit94958cf2008-07-26 22:42:52 +00001#!/usr/bin/python2.4
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00002# Copyright (c) 2006-2010 The Chromium Authors. All rights reserved.
license.botf3378c22008-08-24 00:55:55 +00003# 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.
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00009By default, it listens on an ephemeral port and sends the port number back to
10the originating process over a pipe. The originating process can specify an
11explicit port if necessary.
initial.commit94958cf2008-07-26 22:42:52 +000012It can use https if you specify the flag --https=CERT where CERT is the path
13to a pem file containing the certificate and private key that should be used.
initial.commit94958cf2008-07-26 22:42:52 +000014"""
15
16import base64
17import BaseHTTPServer
18import cgi
initial.commit94958cf2008-07-26 22:42:52 +000019import optparse
20import os
21import re
stoyan@chromium.org372692c2009-01-30 17:01:52 +000022import shutil
akalin@chromium.org18e34882010-11-26 07:10:41 +000023import simplejson
initial.commit94958cf2008-07-26 22:42:52 +000024import SocketServer
25import sys
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +000026import struct
initial.commit94958cf2008-07-26 22:42:52 +000027import time
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +000028import urlparse
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +000029import warnings
30
31# Ignore deprecation warnings, they make our output more cluttered.
32warnings.filterwarnings("ignore", category=DeprecationWarning)
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000033
34import pyftpdlib.ftpserver
initial.commit94958cf2008-07-26 22:42:52 +000035import tlslite
36import tlslite.api
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000037
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +000038try:
39 import hashlib
40 _new_md5 = hashlib.md5
41except ImportError:
42 import md5
43 _new_md5 = md5.new
44
davidben@chromium.org06fcf202010-09-22 18:15:23 +000045if sys.platform == 'win32':
46 import msvcrt
47
maruel@chromium.org756cf982009-03-05 12:46:38 +000048SERVER_HTTP = 0
erikkay@google.comd5182ff2009-01-08 20:45:27 +000049SERVER_FTP = 1
akalin@chromium.org154bb132010-11-12 02:20:27 +000050SERVER_SYNC = 2
initial.commit94958cf2008-07-26 22:42:52 +000051
52debug_output = sys.stderr
53def debug(str):
54 debug_output.write(str + "\n")
55 debug_output.flush()
56
57class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
58 """This is a specialization of of BaseHTTPServer to allow it
59 to be exited cleanly (by setting its "stop" member to True)."""
60
61 def serve_forever(self):
62 self.stop = False
tonyg@chromium.org75054202010-03-31 22:06:10 +000063 self.nonce_time = None
initial.commit94958cf2008-07-26 22:42:52 +000064 while not self.stop:
65 self.handle_request()
66 self.socket.close()
67
68class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
69 """This is a specialization of StoppableHTTPerver that add https support."""
70
davidben@chromium.org31282a12010-08-07 01:10:02 +000071 def __init__(self, server_address, request_hander_class, cert_path,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000072 ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers):
initial.commit94958cf2008-07-26 22:42:52 +000073 s = open(cert_path).read()
74 x509 = tlslite.api.X509()
75 x509.parse(s)
76 self.cert_chain = tlslite.api.X509CertChain([x509])
77 s = open(cert_path).read()
78 self.private_key = tlslite.api.parsePEMKey(s, private=True)
davidben@chromium.org31282a12010-08-07 01:10:02 +000079 self.ssl_client_auth = ssl_client_auth
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +000080 self.ssl_client_cas = []
81 for ca_file in ssl_client_cas:
82 s = open(ca_file).read()
83 x509 = tlslite.api.X509()
84 x509.parse(s)
85 self.ssl_client_cas.append(x509.subject)
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000086 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
87 if ssl_bulk_ciphers is not None:
88 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
initial.commit94958cf2008-07-26 22:42:52 +000089
90 self.session_cache = tlslite.api.SessionCache()
91 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
92
93 def handshake(self, tlsConnection):
94 """Creates the SSL connection."""
95 try:
96 tlsConnection.handshakeServer(certChain=self.cert_chain,
97 privateKey=self.private_key,
davidben@chromium.org31282a12010-08-07 01:10:02 +000098 sessionCache=self.session_cache,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +000099 reqCert=self.ssl_client_auth,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +0000100 settings=self.ssl_handshake_settings,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000101 reqCAs=self.ssl_client_cas)
initial.commit94958cf2008-07-26 22:42:52 +0000102 tlsConnection.ignoreAbruptClose = True
103 return True
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +0000104 except tlslite.api.TLSAbruptCloseError:
105 # Ignore abrupt close.
106 return True
initial.commit94958cf2008-07-26 22:42:52 +0000107 except tlslite.api.TLSError, error:
108 print "Handshake failure:", str(error)
109 return False
110
akalin@chromium.org154bb132010-11-12 02:20:27 +0000111
112class SyncHTTPServer(StoppableHTTPServer):
113 """An HTTP server that handles sync commands."""
114
115 def __init__(self, server_address, request_handler_class):
116 # We import here to avoid pulling in chromiumsync's dependencies
117 # unless strictly necessary.
118 import chromiumsync
119 self._sync_handler = chromiumsync.TestServer()
120 StoppableHTTPServer.__init__(self, server_address, request_handler_class)
121
122 def HandleCommand(self, query, raw_request):
123 return self._sync_handler.HandleCommand(query, raw_request)
124
125
126class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
127
128 def __init__(self, request, client_address, socket_server,
129 connect_handlers, get_handlers, post_handlers, put_handlers):
130 self._connect_handlers = connect_handlers
131 self._get_handlers = get_handlers
132 self._post_handlers = post_handlers
133 self._put_handlers = put_handlers
134 BaseHTTPServer.BaseHTTPRequestHandler.__init__(
135 self, request, client_address, socket_server)
136
137 def log_request(self, *args, **kwargs):
138 # Disable request logging to declutter test log output.
139 pass
140
141 def _ShouldHandleRequest(self, handler_name):
142 """Determines if the path can be handled by the handler.
143
144 We consider a handler valid if the path begins with the
145 handler name. It can optionally be followed by "?*", "/*".
146 """
147
148 pattern = re.compile('%s($|\?|/).*' % handler_name)
149 return pattern.match(self.path)
150
151 def do_CONNECT(self):
152 for handler in self._connect_handlers:
153 if handler():
154 return
155
156 def do_GET(self):
157 for handler in self._get_handlers:
158 if handler():
159 return
160
161 def do_POST(self):
162 for handler in self._post_handlers:
163 if handler():
164 return
165
166 def do_PUT(self):
167 for handler in self._put_handlers:
168 if handler():
169 return
170
171
172class TestPageHandler(BasePageHandler):
initial.commit94958cf2008-07-26 22:42:52 +0000173
174 def __init__(self, request, client_address, socket_server):
akalin@chromium.org154bb132010-11-12 02:20:27 +0000175 connect_handlers = [
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000176 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000177 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000178 self.DefaultConnectResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000179 get_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000180 self.NoCacheMaxAgeTimeHandler,
181 self.NoCacheTimeHandler,
182 self.CacheTimeHandler,
183 self.CacheExpiresHandler,
184 self.CacheProxyRevalidateHandler,
185 self.CachePrivateHandler,
186 self.CachePublicHandler,
187 self.CacheSMaxAgeHandler,
188 self.CacheMustRevalidateHandler,
189 self.CacheMustRevalidateMaxAgeHandler,
190 self.CacheNoStoreHandler,
191 self.CacheNoStoreMaxAgeHandler,
192 self.CacheNoTransformHandler,
193 self.DownloadHandler,
194 self.DownloadFinishHandler,
195 self.EchoHeader,
ananta@chromium.org219b2062009-10-23 16:09:41 +0000196 self.EchoHeaderOverride,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000197 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000198 self.FileHandler,
199 self.RealFileWithCommonHeaderHandler,
200 self.RealBZ2FileWithCommonHeaderHandler,
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000201 self.SetCookieHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000202 self.AuthBasicHandler,
203 self.AuthDigestHandler,
204 self.SlowServerHandler,
205 self.ContentTypeHandler,
206 self.ServerRedirectHandler,
207 self.ClientRedirectHandler,
tony@chromium.org03266982010-03-05 03:18:42 +0000208 self.MultipartHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000209 self.DefaultResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000210 post_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000211 self.EchoTitleHandler,
212 self.EchoAllHandler,
mnissler@chromium.org7c939802010-11-11 08:47:14 +0000213 self.EchoHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000214 self.DeviceManagementHandler] + get_handlers
215 put_handlers = [
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000216 self.EchoTitleHandler,
217 self.EchoAllHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000218 self.EchoHandler] + get_handlers
initial.commit94958cf2008-07-26 22:42:52 +0000219
maruel@google.come250a9b2009-03-10 17:39:46 +0000220 self._mime_types = {
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000221 'crx' : 'application/x-chrome-extension',
maruel@google.come250a9b2009-03-10 17:39:46 +0000222 'gif': 'image/gif',
223 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000224 'jpg' : 'image/jpeg',
jam@chromium.org41550782010-11-17 23:47:50 +0000225 'xml' : 'text/xml',
226 'pdf' : 'application/pdf'
maruel@google.come250a9b2009-03-10 17:39:46 +0000227 }
initial.commit94958cf2008-07-26 22:42:52 +0000228 self._default_mime_type = 'text/html'
229
akalin@chromium.org154bb132010-11-12 02:20:27 +0000230 BasePageHandler.__init__(self, request, client_address, socket_server,
231 connect_handlers, get_handlers, post_handlers,
232 put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000233
initial.commit94958cf2008-07-26 22:42:52 +0000234 def GetMIMETypeFromName(self, file_name):
235 """Returns the mime type for the specified file_name. So far it only looks
236 at the file extension."""
237
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000238 (shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000239 if len(extension) == 0:
240 # no extension.
241 return self._default_mime_type
242
ericroman@google.comc17ca532009-05-07 03:51:05 +0000243 # extension starts with a dot, so we need to remove it
244 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000245
initial.commit94958cf2008-07-26 22:42:52 +0000246 def NoCacheMaxAgeTimeHandler(self):
247 """This request handler yields a page with the title set to the current
248 system time, and no caching requested."""
249
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000250 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000251 return False
252
253 self.send_response(200)
254 self.send_header('Cache-Control', 'max-age=0')
255 self.send_header('Content-type', 'text/html')
256 self.end_headers()
257
maruel@google.come250a9b2009-03-10 17:39:46 +0000258 self.wfile.write('<html><head><title>%s</title></head></html>' %
259 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000260
261 return True
262
263 def NoCacheTimeHandler(self):
264 """This request handler yields a page with the title set to the current
265 system time, and no caching requested."""
266
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000267 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000268 return False
269
270 self.send_response(200)
271 self.send_header('Cache-Control', 'no-cache')
272 self.send_header('Content-type', 'text/html')
273 self.end_headers()
274
maruel@google.come250a9b2009-03-10 17:39:46 +0000275 self.wfile.write('<html><head><title>%s</title></head></html>' %
276 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000277
278 return True
279
280 def CacheTimeHandler(self):
281 """This request handler yields a page with the title set to the current
282 system time, and allows caching for one minute."""
283
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000284 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000285 return False
286
287 self.send_response(200)
288 self.send_header('Cache-Control', 'max-age=60')
289 self.send_header('Content-type', 'text/html')
290 self.end_headers()
291
maruel@google.come250a9b2009-03-10 17:39:46 +0000292 self.wfile.write('<html><head><title>%s</title></head></html>' %
293 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000294
295 return True
296
297 def CacheExpiresHandler(self):
298 """This request handler yields a page with the title set to the current
299 system time, and set the page to expire on 1 Jan 2099."""
300
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000301 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000302 return False
303
304 self.send_response(200)
305 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
306 self.send_header('Content-type', 'text/html')
307 self.end_headers()
308
maruel@google.come250a9b2009-03-10 17:39:46 +0000309 self.wfile.write('<html><head><title>%s</title></head></html>' %
310 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000311
312 return True
313
314 def CacheProxyRevalidateHandler(self):
315 """This request handler yields a page with the title set to the current
316 system time, and allows caching for 60 seconds"""
317
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000318 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000319 return False
320
321 self.send_response(200)
322 self.send_header('Content-type', 'text/html')
323 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
324 self.end_headers()
325
maruel@google.come250a9b2009-03-10 17:39:46 +0000326 self.wfile.write('<html><head><title>%s</title></head></html>' %
327 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000328
329 return True
330
331 def CachePrivateHandler(self):
332 """This request handler yields a page with the title set to the current
333 system time, and allows caching for 5 seconds."""
334
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000335 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000336 return False
337
338 self.send_response(200)
339 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000340 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000341 self.end_headers()
342
maruel@google.come250a9b2009-03-10 17:39:46 +0000343 self.wfile.write('<html><head><title>%s</title></head></html>' %
344 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000345
346 return True
347
348 def CachePublicHandler(self):
349 """This request handler yields a page with the title set to the current
350 system time, and allows caching for 5 seconds."""
351
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000352 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000353 return False
354
355 self.send_response(200)
356 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000357 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000358 self.end_headers()
359
maruel@google.come250a9b2009-03-10 17:39:46 +0000360 self.wfile.write('<html><head><title>%s</title></head></html>' %
361 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000362
363 return True
364
365 def CacheSMaxAgeHandler(self):
366 """This request handler yields a page with the title set to the current
367 system time, and does not allow for caching."""
368
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000369 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000370 return False
371
372 self.send_response(200)
373 self.send_header('Content-type', 'text/html')
374 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
375 self.end_headers()
376
maruel@google.come250a9b2009-03-10 17:39:46 +0000377 self.wfile.write('<html><head><title>%s</title></head></html>' %
378 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000379
380 return True
381
382 def CacheMustRevalidateHandler(self):
383 """This request handler yields a page with the title set to the current
384 system time, and does not allow caching."""
385
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000386 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000387 return False
388
389 self.send_response(200)
390 self.send_header('Content-type', 'text/html')
391 self.send_header('Cache-Control', 'must-revalidate')
392 self.end_headers()
393
maruel@google.come250a9b2009-03-10 17:39:46 +0000394 self.wfile.write('<html><head><title>%s</title></head></html>' %
395 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000396
397 return True
398
399 def CacheMustRevalidateMaxAgeHandler(self):
400 """This request handler yields a page with the title set to the current
401 system time, and does not allow caching event though max-age of 60
402 seconds is specified."""
403
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000404 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000405 return False
406
407 self.send_response(200)
408 self.send_header('Content-type', 'text/html')
409 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
410 self.end_headers()
411
maruel@google.come250a9b2009-03-10 17:39:46 +0000412 self.wfile.write('<html><head><title>%s</title></head></html>' %
413 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000414
415 return True
416
initial.commit94958cf2008-07-26 22:42:52 +0000417 def CacheNoStoreHandler(self):
418 """This request handler yields a page with the title set to the current
419 system time, and does not allow the page to be stored."""
420
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000421 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000422 return False
423
424 self.send_response(200)
425 self.send_header('Content-type', 'text/html')
426 self.send_header('Cache-Control', 'no-store')
427 self.end_headers()
428
maruel@google.come250a9b2009-03-10 17:39:46 +0000429 self.wfile.write('<html><head><title>%s</title></head></html>' %
430 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000431
432 return True
433
434 def CacheNoStoreMaxAgeHandler(self):
435 """This request handler yields a page with the title set to the current
436 system time, and does not allow the page to be stored even though max-age
437 of 60 seconds is specified."""
438
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000439 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000440 return False
441
442 self.send_response(200)
443 self.send_header('Content-type', 'text/html')
444 self.send_header('Cache-Control', 'max-age=60, no-store')
445 self.end_headers()
446
maruel@google.come250a9b2009-03-10 17:39:46 +0000447 self.wfile.write('<html><head><title>%s</title></head></html>' %
448 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000449
450 return True
451
452
453 def CacheNoTransformHandler(self):
454 """This request handler yields a page with the title set to the current
455 system time, and does not allow the content to transformed during
456 user-agent caching"""
457
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000458 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000459 return False
460
461 self.send_response(200)
462 self.send_header('Content-type', 'text/html')
463 self.send_header('Cache-Control', 'no-transform')
464 self.end_headers()
465
maruel@google.come250a9b2009-03-10 17:39:46 +0000466 self.wfile.write('<html><head><title>%s</title></head></html>' %
467 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000468
469 return True
470
471 def EchoHeader(self):
472 """This handler echoes back the value of a specific request header."""
ananta@chromium.org219b2062009-10-23 16:09:41 +0000473 """The only difference between this function and the EchoHeaderOverride"""
474 """function is in the parameter being passed to the helper function"""
475 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000476
ananta@chromium.org219b2062009-10-23 16:09:41 +0000477 def EchoHeaderOverride(self):
478 """This handler echoes back the value of a specific request header."""
479 """The UrlRequest unit tests also execute for ChromeFrame which uses"""
480 """IE to issue HTTP requests using the host network stack."""
481 """The Accept and Charset tests which expect the server to echo back"""
482 """the corresponding headers fail here as IE returns cached responses"""
483 """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
484 """treats this request as a new request and does not cache it."""
485 return self.EchoHeaderHelper("/echoheaderoverride")
486
487 def EchoHeaderHelper(self, echo_header):
488 """This function echoes back the value of the request header passed in."""
489 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000490 return False
491
492 query_char = self.path.find('?')
493 if query_char != -1:
494 header_name = self.path[query_char+1:]
495
496 self.send_response(200)
497 self.send_header('Content-type', 'text/plain')
498 self.send_header('Cache-control', 'max-age=60000')
499 # insert a vary header to properly indicate that the cachability of this
500 # request is subject to value of the request header being echoed.
501 if len(header_name) > 0:
502 self.send_header('Vary', header_name)
503 self.end_headers()
504
505 if len(header_name) > 0:
506 self.wfile.write(self.headers.getheader(header_name))
507
508 return True
509
510 def EchoHandler(self):
511 """This handler just echoes back the payload of the request, for testing
512 form submission."""
513
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000514 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000515 return False
516
517 self.send_response(200)
518 self.send_header('Content-type', 'text/html')
519 self.end_headers()
520 length = int(self.headers.getheader('content-length'))
521 request = self.rfile.read(length)
522 self.wfile.write(request)
523 return True
524
525 def EchoTitleHandler(self):
526 """This handler is like Echo, but sets the page title to the request."""
527
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000528 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000529 return False
530
531 self.send_response(200)
532 self.send_header('Content-type', 'text/html')
533 self.end_headers()
534 length = int(self.headers.getheader('content-length'))
535 request = self.rfile.read(length)
536 self.wfile.write('<html><head><title>')
537 self.wfile.write(request)
538 self.wfile.write('</title></head></html>')
539 return True
540
541 def EchoAllHandler(self):
542 """This handler yields a (more) human-readable page listing information
543 about the request header & contents."""
544
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000545 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000546 return False
547
548 self.send_response(200)
549 self.send_header('Content-type', 'text/html')
550 self.end_headers()
551 self.wfile.write('<html><head><style>'
552 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
553 '</style></head><body>'
554 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000555 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000556 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000557
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000558 if self.command == 'POST' or self.command == 'PUT':
ericroman@google.coma47622b2008-11-15 04:36:51 +0000559 length = int(self.headers.getheader('content-length'))
560 qs = self.rfile.read(length)
561 params = cgi.parse_qs(qs, keep_blank_values=1)
562
563 for param in params:
564 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000565
566 self.wfile.write('</pre>')
567
568 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
569
570 self.wfile.write('</body></html>')
571 return True
572
573 def DownloadHandler(self):
574 """This handler sends a downloadable file with or without reporting
575 the size (6K)."""
576
577 if self.path.startswith("/download-unknown-size"):
578 send_length = False
579 elif self.path.startswith("/download-known-size"):
580 send_length = True
581 else:
582 return False
583
584 #
585 # The test which uses this functionality is attempting to send
586 # small chunks of data to the client. Use a fairly large buffer
587 # so that we'll fill chrome's IO buffer enough to force it to
588 # actually write the data.
589 # See also the comments in the client-side of this test in
590 # download_uitest.cc
591 #
592 size_chunk1 = 35*1024
593 size_chunk2 = 10*1024
594
595 self.send_response(200)
596 self.send_header('Content-type', 'application/octet-stream')
597 self.send_header('Cache-Control', 'max-age=0')
598 if send_length:
599 self.send_header('Content-Length', size_chunk1 + size_chunk2)
600 self.end_headers()
601
602 # First chunk of data:
603 self.wfile.write("*" * size_chunk1)
604 self.wfile.flush()
605
606 # handle requests until one of them clears this flag.
607 self.server.waitForDownload = True
608 while self.server.waitForDownload:
609 self.server.handle_request()
610
611 # Second chunk of data:
612 self.wfile.write("*" * size_chunk2)
613 return True
614
615 def DownloadFinishHandler(self):
616 """This handler just tells the server to finish the current download."""
617
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000618 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000619 return False
620
621 self.server.waitForDownload = False
622 self.send_response(200)
623 self.send_header('Content-type', 'text/html')
624 self.send_header('Cache-Control', 'max-age=0')
625 self.end_headers()
626 return True
627
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000628 def _ReplaceFileData(self, data, query_parameters):
629 """Replaces matching substrings in a file.
630
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000631 If the 'replace_text' URL query parameter is present, it is expected to be
632 of the form old_text:new_text, which indicates that any old_text strings in
633 the file are replaced with new_text. Multiple 'replace_text' parameters may
634 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000635
636 If the parameters are not present, |data| is returned.
637 """
638 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000639 replace_text_values = query_dict.get('replace_text', [])
640 for replace_text_value in replace_text_values:
641 replace_text_args = replace_text_value.split(':')
642 if len(replace_text_args) != 2:
643 raise ValueError(
644 'replace_text must be of form old_text:new_text. Actual value: %s' %
645 replace_text_value)
646 old_text_b64, new_text_b64 = replace_text_args
647 old_text = base64.urlsafe_b64decode(old_text_b64)
648 new_text = base64.urlsafe_b64decode(new_text_b64)
649 data = data.replace(old_text, new_text)
650 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000651
initial.commit94958cf2008-07-26 22:42:52 +0000652 def FileHandler(self):
653 """This handler sends the contents of the requested file. Wow, it's like
654 a real webserver!"""
655
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000656 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000657 if not self.path.startswith(prefix):
658 return False
659
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000660 # Consume a request body if present.
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000661 if self.command == 'POST' or self.command == 'PUT' :
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000662 self.rfile.read(int(self.headers.getheader('content-length')))
663
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000664 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
665 sub_path = url_path[len(prefix):]
666 entries = sub_path.split('/')
667 file_path = os.path.join(self.server.data_dir, *entries)
668 if os.path.isdir(file_path):
669 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000670
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000671 if not os.path.isfile(file_path):
672 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000673 self.send_error(404)
674 return True
675
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000676 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000677 data = f.read()
678 f.close()
679
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000680 data = self._ReplaceFileData(data, query)
681
initial.commit94958cf2008-07-26 22:42:52 +0000682 # If file.mock-http-headers exists, it contains the headers we
683 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000684 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000685 if os.path.isfile(headers_path):
686 f = open(headers_path, "r")
687
688 # "HTTP/1.1 200 OK"
689 response = f.readline()
690 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
691 self.send_response(int(status_code))
692
693 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000694 header_values = re.findall('(\S+):\s*(.*)', line)
695 if len(header_values) > 0:
696 # "name: value"
697 name, value = header_values[0]
698 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000699 f.close()
700 else:
701 # Could be more generic once we support mime-type sniffing, but for
702 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000703
704 range = self.headers.get('Range')
705 if range and range.startswith('bytes='):
706 # Note this doesn't handle all valid byte range values (i.e. open ended
707 # ones), just enough for what we needed so far.
708 range = range[6:].split('-')
709 start = int(range[0])
710 end = int(range[1])
711
712 self.send_response(206)
713 content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
714 str(len(data))
715 self.send_header('Content-Range', content_range)
716 data = data[start: end + 1]
717 else:
718 self.send_response(200)
719
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000720 self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000721 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000722 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000723 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000724 self.end_headers()
725
726 self.wfile.write(data)
727
728 return True
729
730 def RealFileWithCommonHeaderHandler(self):
731 """This handler sends the contents of the requested file without the pseudo
732 http head!"""
733
734 prefix='/realfiles/'
735 if not self.path.startswith(prefix):
736 return False
737
738 file = self.path[len(prefix):]
739 path = os.path.join(self.server.data_dir, file)
740
741 try:
742 f = open(path, "rb")
743 data = f.read()
744 f.close()
745
746 # just simply set the MIME as octal stream
747 self.send_response(200)
748 self.send_header('Content-type', 'application/octet-stream')
749 self.end_headers()
750
751 self.wfile.write(data)
752 except:
753 self.send_error(404)
754
755 return True
756
757 def RealBZ2FileWithCommonHeaderHandler(self):
758 """This handler sends the bzip2 contents of the requested file with
759 corresponding Content-Encoding field in http head!"""
760
761 prefix='/realbz2files/'
762 if not self.path.startswith(prefix):
763 return False
764
765 parts = self.path.split('?')
766 file = parts[0][len(prefix):]
767 path = os.path.join(self.server.data_dir, file) + '.bz2'
768
769 if len(parts) > 1:
770 options = parts[1]
771 else:
772 options = ''
773
774 try:
775 self.send_response(200)
776 accept_encoding = self.headers.get("Accept-Encoding")
777 if accept_encoding.find("bzip2") != -1:
778 f = open(path, "rb")
779 data = f.read()
780 f.close()
781 self.send_header('Content-Encoding', 'bzip2')
782 self.send_header('Content-type', 'application/x-bzip2')
783 self.end_headers()
784 if options == 'incremental-header':
785 self.wfile.write(data[:1])
786 self.wfile.flush()
787 time.sleep(1.0)
788 self.wfile.write(data[1:])
789 else:
790 self.wfile.write(data)
791 else:
792 """client do not support bzip2 format, send pseudo content
793 """
794 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
795 self.end_headers()
796 self.wfile.write("you do not support bzip2 encoding")
797 except:
798 self.send_error(404)
799
800 return True
801
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000802 def SetCookieHandler(self):
803 """This handler just sets a cookie, for testing cookie handling."""
804
805 if not self._ShouldHandleRequest("/set-cookie"):
806 return False
807
808 query_char = self.path.find('?')
809 if query_char != -1:
810 cookie_values = self.path[query_char + 1:].split('&')
811 else:
812 cookie_values = ("",)
813 self.send_response(200)
814 self.send_header('Content-type', 'text/html')
815 for cookie_value in cookie_values:
816 self.send_header('Set-Cookie', '%s' % cookie_value)
817 self.end_headers()
818 for cookie_value in cookie_values:
819 self.wfile.write('%s' % cookie_value)
820 return True
821
initial.commit94958cf2008-07-26 22:42:52 +0000822 def AuthBasicHandler(self):
823 """This handler tests 'Basic' authentication. It just sends a page with
824 title 'user/pass' if you succeed."""
825
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000826 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000827 return False
828
829 username = userpass = password = b64str = ""
830
ericroman@google.com239b4d82009-03-27 04:00:22 +0000831 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
832
initial.commit94958cf2008-07-26 22:42:52 +0000833 auth = self.headers.getheader('authorization')
834 try:
835 if not auth:
836 raise Exception('no auth')
837 b64str = re.findall(r'Basic (\S+)', auth)[0]
838 userpass = base64.b64decode(b64str)
839 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
840 if password != 'secret':
841 raise Exception('wrong password')
842 except Exception, e:
843 # Authentication failed.
844 self.send_response(401)
845 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
846 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000847 if set_cookie_if_challenged:
848 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000849 self.end_headers()
850 self.wfile.write('<html><head>')
851 self.wfile.write('<title>Denied: %s</title>' % e)
852 self.wfile.write('</head><body>')
853 self.wfile.write('auth=%s<p>' % auth)
854 self.wfile.write('b64str=%s<p>' % b64str)
855 self.wfile.write('username: %s<p>' % username)
856 self.wfile.write('userpass: %s<p>' % userpass)
857 self.wfile.write('password: %s<p>' % password)
858 self.wfile.write('You sent:<br>%s<p>' % self.headers)
859 self.wfile.write('</body></html>')
860 return True
861
862 # Authentication successful. (Return a cachable response to allow for
863 # testing cached pages that require authentication.)
864 if_none_match = self.headers.getheader('if-none-match')
865 if if_none_match == "abc":
866 self.send_response(304)
867 self.end_headers()
868 else:
869 self.send_response(200)
870 self.send_header('Content-type', 'text/html')
871 self.send_header('Cache-control', 'max-age=60000')
872 self.send_header('Etag', 'abc')
873 self.end_headers()
874 self.wfile.write('<html><head>')
875 self.wfile.write('<title>%s/%s</title>' % (username, password))
876 self.wfile.write('</head><body>')
877 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000878 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000879 self.wfile.write('</body></html>')
880
881 return True
882
tonyg@chromium.org75054202010-03-31 22:06:10 +0000883 def GetNonce(self, force_reset=False):
884 """Returns a nonce that's stable per request path for the server's lifetime.
initial.commit94958cf2008-07-26 22:42:52 +0000885
tonyg@chromium.org75054202010-03-31 22:06:10 +0000886 This is a fake implementation. A real implementation would only use a given
887 nonce a single time (hence the name n-once). However, for the purposes of
888 unittesting, we don't care about the security of the nonce.
889
890 Args:
891 force_reset: Iff set, the nonce will be changed. Useful for testing the
892 "stale" response.
893 """
894 if force_reset or not self.server.nonce_time:
895 self.server.nonce_time = time.time()
896 return _new_md5('privatekey%s%d' %
897 (self.path, self.server.nonce_time)).hexdigest()
898
899 def AuthDigestHandler(self):
900 """This handler tests 'Digest' authentication.
901
902 It just sends a page with title 'user/pass' if you succeed.
903
904 A stale response is sent iff "stale" is present in the request path.
905 """
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000906 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000907 return False
908
tonyg@chromium.org75054202010-03-31 22:06:10 +0000909 stale = 'stale' in self.path
910 nonce = self.GetNonce(force_reset=stale)
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000911 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000912 password = 'secret'
913 realm = 'testrealm'
914
915 auth = self.headers.getheader('authorization')
916 pairs = {}
917 try:
918 if not auth:
919 raise Exception('no auth')
920 if not auth.startswith('Digest'):
921 raise Exception('not digest')
922 # Pull out all the name="value" pairs as a dictionary.
923 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
924
925 # Make sure it's all valid.
926 if pairs['nonce'] != nonce:
927 raise Exception('wrong nonce')
928 if pairs['opaque'] != opaque:
929 raise Exception('wrong opaque')
930
931 # Check the 'response' value and make sure it matches our magic hash.
932 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000933 hash_a1 = _new_md5(
934 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000935 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000936 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000937 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000938 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
939 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000940 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000941
942 if pairs['response'] != response:
943 raise Exception('wrong password')
944 except Exception, e:
945 # Authentication failed.
946 self.send_response(401)
947 hdr = ('Digest '
948 'realm="%s", '
949 'domain="/", '
950 'qop="auth", '
951 'algorithm=MD5, '
952 'nonce="%s", '
953 'opaque="%s"') % (realm, nonce, opaque)
954 if stale:
955 hdr += ', stale="TRUE"'
956 self.send_header('WWW-Authenticate', hdr)
957 self.send_header('Content-type', 'text/html')
958 self.end_headers()
959 self.wfile.write('<html><head>')
960 self.wfile.write('<title>Denied: %s</title>' % e)
961 self.wfile.write('</head><body>')
962 self.wfile.write('auth=%s<p>' % auth)
963 self.wfile.write('pairs=%s<p>' % pairs)
964 self.wfile.write('You sent:<br>%s<p>' % self.headers)
965 self.wfile.write('We are replying:<br>%s<p>' % hdr)
966 self.wfile.write('</body></html>')
967 return True
968
969 # Authentication successful.
970 self.send_response(200)
971 self.send_header('Content-type', 'text/html')
972 self.end_headers()
973 self.wfile.write('<html><head>')
974 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
975 self.wfile.write('</head><body>')
976 self.wfile.write('auth=%s<p>' % auth)
977 self.wfile.write('pairs=%s<p>' % pairs)
978 self.wfile.write('</body></html>')
979
980 return True
981
982 def SlowServerHandler(self):
983 """Wait for the user suggested time before responding. The syntax is
984 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000985 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000986 return False
987 query_char = self.path.find('?')
988 wait_sec = 1.0
989 if query_char >= 0:
990 try:
991 wait_sec = int(self.path[query_char + 1:])
992 except ValueError:
993 pass
994 time.sleep(wait_sec)
995 self.send_response(200)
996 self.send_header('Content-type', 'text/plain')
997 self.end_headers()
998 self.wfile.write("waited %d seconds" % wait_sec)
999 return True
1000
1001 def ContentTypeHandler(self):
1002 """Returns a string of html with the given content type. E.g.,
1003 /contenttype?text/css returns an html file with the Content-Type
1004 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001005 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001006 return False
1007 query_char = self.path.find('?')
1008 content_type = self.path[query_char + 1:].strip()
1009 if not content_type:
1010 content_type = 'text/html'
1011 self.send_response(200)
1012 self.send_header('Content-Type', content_type)
1013 self.end_headers()
1014 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1015 return True
1016
1017 def ServerRedirectHandler(self):
1018 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001019 '/server-redirect?http://foo.bar/asdf' to redirect to
1020 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001021
1022 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001023 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001024 return False
1025
1026 query_char = self.path.find('?')
1027 if query_char < 0 or len(self.path) <= query_char + 1:
1028 self.sendRedirectHelp(test_name)
1029 return True
1030 dest = self.path[query_char + 1:]
1031
1032 self.send_response(301) # moved permanently
1033 self.send_header('Location', dest)
1034 self.send_header('Content-type', 'text/html')
1035 self.end_headers()
1036 self.wfile.write('<html><head>')
1037 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1038
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001039 return True
initial.commit94958cf2008-07-26 22:42:52 +00001040
1041 def ClientRedirectHandler(self):
1042 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001043 '/client-redirect?http://foo.bar/asdf' to redirect to
1044 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001045
1046 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001047 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001048 return False
1049
1050 query_char = self.path.find('?');
1051 if query_char < 0 or len(self.path) <= query_char + 1:
1052 self.sendRedirectHelp(test_name)
1053 return True
1054 dest = self.path[query_char + 1:]
1055
1056 self.send_response(200)
1057 self.send_header('Content-type', 'text/html')
1058 self.end_headers()
1059 self.wfile.write('<html><head>')
1060 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1061 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1062
1063 return True
1064
tony@chromium.org03266982010-03-05 03:18:42 +00001065 def MultipartHandler(self):
1066 """Send a multipart response (10 text/html pages)."""
1067 test_name = "/multipart"
1068 if not self._ShouldHandleRequest(test_name):
1069 return False
1070
1071 num_frames = 10
1072 bound = '12345'
1073 self.send_response(200)
1074 self.send_header('Content-type',
1075 'multipart/x-mixed-replace;boundary=' + bound)
1076 self.end_headers()
1077
1078 for i in xrange(num_frames):
1079 self.wfile.write('--' + bound + '\r\n')
1080 self.wfile.write('Content-type: text/html\r\n\r\n')
1081 self.wfile.write('<title>page ' + str(i) + '</title>')
1082 self.wfile.write('page ' + str(i))
1083
1084 self.wfile.write('--' + bound + '--')
1085 return True
1086
initial.commit94958cf2008-07-26 22:42:52 +00001087 def DefaultResponseHandler(self):
1088 """This is the catch-all response handler for requests that aren't handled
1089 by one of the special handlers above.
1090 Note that we specify the content-length as without it the https connection
1091 is not closed properly (and the browser keeps expecting data)."""
1092
1093 contents = "Default response given for path: " + self.path
1094 self.send_response(200)
1095 self.send_header('Content-type', 'text/html')
1096 self.send_header("Content-Length", len(contents))
1097 self.end_headers()
1098 self.wfile.write(contents)
1099 return True
1100
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001101 def RedirectConnectHandler(self):
1102 """Sends a redirect to the CONNECT request for www.redirect.com. This
1103 response is not specified by the RFC, so the browser should not follow
1104 the redirect."""
1105
1106 if (self.path.find("www.redirect.com") < 0):
1107 return False
1108
1109 dest = "http://www.destination.com/foo.js"
1110
1111 self.send_response(302) # moved temporarily
1112 self.send_header('Location', dest)
1113 self.send_header('Connection', 'close')
1114 self.end_headers()
1115 return True
1116
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001117 def ServerAuthConnectHandler(self):
1118 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1119 response doesn't make sense because the proxy server cannot request
1120 server authentication."""
1121
1122 if (self.path.find("www.server-auth.com") < 0):
1123 return False
1124
1125 challenge = 'Basic realm="WallyWorld"'
1126
1127 self.send_response(401) # unauthorized
1128 self.send_header('WWW-Authenticate', challenge)
1129 self.send_header('Connection', 'close')
1130 self.end_headers()
1131 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001132
1133 def DefaultConnectResponseHandler(self):
1134 """This is the catch-all response handler for CONNECT requests that aren't
1135 handled by one of the special handlers above. Real Web servers respond
1136 with 400 to CONNECT requests."""
1137
1138 contents = "Your client has issued a malformed or illegal request."
1139 self.send_response(400) # bad request
1140 self.send_header('Content-type', 'text/html')
1141 self.send_header("Content-Length", len(contents))
1142 self.end_headers()
1143 self.wfile.write(contents)
1144 return True
1145
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001146 def DeviceManagementHandler(self):
1147 """Delegates to the device management service used for cloud policy."""
1148 if not self._ShouldHandleRequest("/device_management"):
1149 return False
1150
1151 length = int(self.headers.getheader('content-length'))
1152 raw_request = self.rfile.read(length)
1153
1154 if not self.server._device_management_handler:
1155 import device_management
1156 policy_path = os.path.join(self.server.data_dir, 'device_management')
1157 self.server._device_management_handler = (
1158 device_management.TestServer(policy_path))
1159
1160 http_response, raw_reply = (
1161 self.server._device_management_handler.HandleRequest(self.path,
1162 self.headers,
1163 raw_request))
1164 self.send_response(http_response)
1165 self.end_headers()
1166 self.wfile.write(raw_reply)
1167 return True
1168
initial.commit94958cf2008-07-26 22:42:52 +00001169 # called by the redirect handling function when there is no parameter
1170 def sendRedirectHelp(self, redirect_name):
1171 self.send_response(200)
1172 self.send_header('Content-type', 'text/html')
1173 self.end_headers()
1174 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1175 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1176 self.wfile.write('</body></html>')
1177
akalin@chromium.org154bb132010-11-12 02:20:27 +00001178
1179class SyncPageHandler(BasePageHandler):
1180 """Handler for the main HTTP sync server."""
1181
1182 def __init__(self, request, client_address, sync_http_server):
1183 get_handlers = [self.ChromiumSyncTimeHandler]
1184 post_handlers = [self.ChromiumSyncCommandHandler]
1185 BasePageHandler.__init__(self, request, client_address,
1186 sync_http_server, [], get_handlers,
1187 post_handlers, [])
1188
1189 def ChromiumSyncTimeHandler(self):
1190 """Handle Chromium sync .../time requests.
1191
1192 The syncer sometimes checks server reachability by examining /time.
1193 """
1194 test_name = "/chromiumsync/time"
1195 if not self._ShouldHandleRequest(test_name):
1196 return False
1197
1198 self.send_response(200)
1199 self.send_header('Content-type', 'text/html')
1200 self.end_headers()
1201 return True
1202
1203 def ChromiumSyncCommandHandler(self):
1204 """Handle a chromiumsync command arriving via http.
1205
1206 This covers all sync protocol commands: authentication, getupdates, and
1207 commit.
1208 """
1209 test_name = "/chromiumsync/command"
1210 if not self._ShouldHandleRequest(test_name):
1211 return False
1212
1213 length = int(self.headers.getheader('content-length'))
1214 raw_request = self.rfile.read(length)
1215
1216 http_response, raw_reply = self.server.HandleCommand(
1217 self.path, raw_request)
1218 self.send_response(http_response)
1219 self.end_headers()
1220 self.wfile.write(raw_reply)
1221 return True
1222
1223
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001224def MakeDataDir():
1225 if options.data_dir:
1226 if not os.path.isdir(options.data_dir):
1227 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1228 return None
1229 my_data_dir = options.data_dir
1230 else:
1231 # Create the default path to our data dir, relative to the exe dir.
1232 my_data_dir = os.path.dirname(sys.argv[0])
1233 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00001234 "test", "data")
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001235
1236 #TODO(ibrar): Must use Find* funtion defined in google\tools
1237 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1238
1239 return my_data_dir
1240
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001241class FileMultiplexer:
1242 def __init__(self, fd1, fd2) :
1243 self.__fd1 = fd1
1244 self.__fd2 = fd2
1245
1246 def __del__(self) :
1247 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1248 self.__fd1.close()
1249 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1250 self.__fd2.close()
1251
1252 def write(self, text) :
1253 self.__fd1.write(text)
1254 self.__fd2.write(text)
1255
1256 def flush(self) :
1257 self.__fd1.flush()
1258 self.__fd2.flush()
1259
initial.commit94958cf2008-07-26 22:42:52 +00001260def main(options, args):
initial.commit94958cf2008-07-26 22:42:52 +00001261 logfile = open('testserver.log', 'w')
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001262 sys.stdout = FileMultiplexer(sys.stdout, logfile)
1263 sys.stderr = FileMultiplexer(sys.stderr, logfile)
initial.commit94958cf2008-07-26 22:42:52 +00001264
1265 port = options.port
1266
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001267 if options.server_type == SERVER_HTTP:
1268 if options.cert:
1269 # let's make sure the cert file exists.
1270 if not os.path.isfile(options.cert):
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001271 print 'specified server cert file not found: ' + options.cert + \
1272 ' exiting...'
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001273 return
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001274 for ca_cert in options.ssl_client_ca:
1275 if not os.path.isfile(ca_cert):
1276 print 'specified trusted client CA file not found: ' + ca_cert + \
1277 ' exiting...'
1278 return
davidben@chromium.org31282a12010-08-07 01:10:02 +00001279 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001280 options.ssl_client_auth, options.ssl_client_ca,
1281 options.ssl_bulk_cipher)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001282 print 'HTTPS server started on port %d...' % server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001283 else:
phajdan.jr@chromium.orgfa49b5f2010-07-23 20:38:56 +00001284 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001285 print 'HTTP server started on port %d...' % server.server_port
erikkay@google.com70397b62008-12-30 21:49:21 +00001286
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001287 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001288 server.file_root_url = options.file_root_url
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001289 listen_port = server.server_port
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001290 server._device_management_handler = None
akalin@chromium.org154bb132010-11-12 02:20:27 +00001291 elif options.server_type == SERVER_SYNC:
1292 server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1293 print 'Sync HTTP server started on port %d...' % server.server_port
1294 listen_port = server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001295 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001296 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001297 my_data_dir = MakeDataDir()
1298
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001299 # Instantiate a dummy authorizer for managing 'virtual' users
1300 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1301
1302 # Define a new user having full r/w permissions and a read-only
1303 # anonymous user
1304 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1305
1306 authorizer.add_anonymous(my_data_dir)
1307
1308 # Instantiate FTP handler class
1309 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1310 ftp_handler.authorizer = authorizer
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001311
1312 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001313 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1314 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001315
1316 # Instantiate FTP server class and listen to 127.0.0.1:port
1317 address = ('127.0.0.1', port)
1318 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001319 listen_port = server.socket.getsockname()[1]
1320 print 'FTP server started on port %d...' % listen_port
initial.commit94958cf2008-07-26 22:42:52 +00001321
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001322 # Notify the parent that we've started. (BaseServer subclasses
1323 # bind their sockets on construction.)
1324 if options.startup_pipe is not None:
akalin@chromium.org73b7b5c2010-11-26 19:42:26 +00001325 server_data = {
1326 'port': listen_port
1327 }
1328 server_data_json = simplejson.dumps(server_data)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001329 if sys.platform == 'win32':
1330 fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1331 else:
1332 fd = options.startup_pipe
1333 startup_pipe = os.fdopen(fd, "w")
akalin@chromium.org2dec8822010-11-25 11:00:09 +00001334 # Write the listening port as a 2 byte value. This is _not_ using
1335 # network byte ordering since the other end of the pipe is on the same
1336 # machine.
akalin@chromium.org2c7605d2010-11-26 03:21:37 +00001337 startup_pipe.write(struct.pack('@H', listen_port))
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001338 startup_pipe.close()
1339
initial.commit94958cf2008-07-26 22:42:52 +00001340 try:
1341 server.serve_forever()
1342 except KeyboardInterrupt:
1343 print 'shutting down server'
1344 server.stop = True
1345
1346if __name__ == '__main__':
1347 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001348 option_parser.add_option("-f", '--ftp', action='store_const',
1349 const=SERVER_FTP, default=SERVER_HTTP,
1350 dest='server_type',
akalin@chromium.org154bb132010-11-12 02:20:27 +00001351 help='start up an FTP server.')
1352 option_parser.add_option('', '--sync', action='store_const',
1353 const=SERVER_SYNC, default=SERVER_HTTP,
1354 dest='server_type',
1355 help='start up a sync server.')
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001356 option_parser.add_option('', '--port', default='0', type='int',
1357 help='Port used by the server. If unspecified, the '
1358 'server will listen on an ephemeral port.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001359 option_parser.add_option('', '--data-dir', dest='data_dir',
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001360 help='Directory from which to read the files.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001361 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001362 help='Specify that https should be used, specify '
1363 'the path to the cert containing the private key '
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001364 'the server should use.')
davidben@chromium.org31282a12010-08-07 01:10:02 +00001365 option_parser.add_option('', '--ssl-client-auth', action='store_true',
1366 help='Require SSL client auth on every connection.')
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001367 option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1368 help='Specify that the client certificate request '
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001369 'should include the CA named in the subject of '
1370 'the DER-encoded certificate contained in the '
1371 'specified file. This option may appear multiple '
1372 'times, indicating multiple CA names should be '
1373 'sent in the request.')
1374 option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1375 help='Specify the bulk encryption algorithm(s)'
1376 'that will be accepted by the SSL server. Valid '
1377 'values are "aes256", "aes128", "3des", "rc4". If '
1378 'omitted, all algorithms will be used. This '
1379 'option may appear multiple times, indicating '
1380 'multiple algorithms should be enabled.');
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001381 option_parser.add_option('', '--file-root-url', default='/files/',
1382 help='Specify a root URL for files served.')
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001383 option_parser.add_option('', '--startup-pipe', type='int',
1384 dest='startup_pipe',
1385 help='File handle of pipe to parent process')
initial.commit94958cf2008-07-26 22:42:52 +00001386 options, args = option_parser.parse_args()
1387
1388 sys.exit(main(options, args))