blob: b69eb2f141efd0f9ce499ab7f70d8880f6bc0b8a [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
initial.commit94958cf2008-07-26 22:42:52 +000023import SocketServer
24import sys
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +000025import struct
initial.commit94958cf2008-07-26 22:42:52 +000026import time
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +000027import urlparse
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +000028import warnings
29
akalin@chromium.org2b0a9c42010-11-25 06:43:12 +000030# If we use simplejson always, we get some warnings when we run under
31# 2.6.
32if sys.version_info < (2, 6):
33 import simplejson as json
34else:
35 import json
36
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +000037# Ignore deprecation warnings, they make our output more cluttered.
38warnings.filterwarnings("ignore", category=DeprecationWarning)
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000039
40import pyftpdlib.ftpserver
initial.commit94958cf2008-07-26 22:42:52 +000041import tlslite
42import tlslite.api
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000043
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +000044try:
45 import hashlib
46 _new_md5 = hashlib.md5
47except ImportError:
48 import md5
49 _new_md5 = md5.new
50
davidben@chromium.org06fcf202010-09-22 18:15:23 +000051if sys.platform == 'win32':
52 import msvcrt
53
maruel@chromium.org756cf982009-03-05 12:46:38 +000054SERVER_HTTP = 0
erikkay@google.comd5182ff2009-01-08 20:45:27 +000055SERVER_FTP = 1
akalin@chromium.org154bb132010-11-12 02:20:27 +000056SERVER_SYNC = 2
initial.commit94958cf2008-07-26 22:42:52 +000057
58debug_output = sys.stderr
59def debug(str):
60 debug_output.write(str + "\n")
61 debug_output.flush()
62
63class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
64 """This is a specialization of of BaseHTTPServer to allow it
65 to be exited cleanly (by setting its "stop" member to True)."""
66
67 def serve_forever(self):
68 self.stop = False
tonyg@chromium.org75054202010-03-31 22:06:10 +000069 self.nonce_time = None
initial.commit94958cf2008-07-26 22:42:52 +000070 while not self.stop:
71 self.handle_request()
72 self.socket.close()
73
74class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
75 """This is a specialization of StoppableHTTPerver that add https support."""
76
davidben@chromium.org31282a12010-08-07 01:10:02 +000077 def __init__(self, server_address, request_hander_class, cert_path,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000078 ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers):
initial.commit94958cf2008-07-26 22:42:52 +000079 s = open(cert_path).read()
80 x509 = tlslite.api.X509()
81 x509.parse(s)
82 self.cert_chain = tlslite.api.X509CertChain([x509])
83 s = open(cert_path).read()
84 self.private_key = tlslite.api.parsePEMKey(s, private=True)
davidben@chromium.org31282a12010-08-07 01:10:02 +000085 self.ssl_client_auth = ssl_client_auth
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +000086 self.ssl_client_cas = []
87 for ca_file in ssl_client_cas:
88 s = open(ca_file).read()
89 x509 = tlslite.api.X509()
90 x509.parse(s)
91 self.ssl_client_cas.append(x509.subject)
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000092 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
93 if ssl_bulk_ciphers is not None:
94 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
initial.commit94958cf2008-07-26 22:42:52 +000095
96 self.session_cache = tlslite.api.SessionCache()
97 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
98
99 def handshake(self, tlsConnection):
100 """Creates the SSL connection."""
101 try:
102 tlsConnection.handshakeServer(certChain=self.cert_chain,
103 privateKey=self.private_key,
davidben@chromium.org31282a12010-08-07 01:10:02 +0000104 sessionCache=self.session_cache,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000105 reqCert=self.ssl_client_auth,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +0000106 settings=self.ssl_handshake_settings,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000107 reqCAs=self.ssl_client_cas)
initial.commit94958cf2008-07-26 22:42:52 +0000108 tlsConnection.ignoreAbruptClose = True
109 return True
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +0000110 except tlslite.api.TLSAbruptCloseError:
111 # Ignore abrupt close.
112 return True
initial.commit94958cf2008-07-26 22:42:52 +0000113 except tlslite.api.TLSError, error:
114 print "Handshake failure:", str(error)
115 return False
116
akalin@chromium.org154bb132010-11-12 02:20:27 +0000117
118class SyncHTTPServer(StoppableHTTPServer):
119 """An HTTP server that handles sync commands."""
120
121 def __init__(self, server_address, request_handler_class):
122 # We import here to avoid pulling in chromiumsync's dependencies
123 # unless strictly necessary.
124 import chromiumsync
125 self._sync_handler = chromiumsync.TestServer()
126 StoppableHTTPServer.__init__(self, server_address, request_handler_class)
127
128 def HandleCommand(self, query, raw_request):
129 return self._sync_handler.HandleCommand(query, raw_request)
130
131
132class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
133
134 def __init__(self, request, client_address, socket_server,
135 connect_handlers, get_handlers, post_handlers, put_handlers):
136 self._connect_handlers = connect_handlers
137 self._get_handlers = get_handlers
138 self._post_handlers = post_handlers
139 self._put_handlers = put_handlers
140 BaseHTTPServer.BaseHTTPRequestHandler.__init__(
141 self, request, client_address, socket_server)
142
143 def log_request(self, *args, **kwargs):
144 # Disable request logging to declutter test log output.
145 pass
146
147 def _ShouldHandleRequest(self, handler_name):
148 """Determines if the path can be handled by the handler.
149
150 We consider a handler valid if the path begins with the
151 handler name. It can optionally be followed by "?*", "/*".
152 """
153
154 pattern = re.compile('%s($|\?|/).*' % handler_name)
155 return pattern.match(self.path)
156
157 def do_CONNECT(self):
158 for handler in self._connect_handlers:
159 if handler():
160 return
161
162 def do_GET(self):
163 for handler in self._get_handlers:
164 if handler():
165 return
166
167 def do_POST(self):
168 for handler in self._post_handlers:
169 if handler():
170 return
171
172 def do_PUT(self):
173 for handler in self._put_handlers:
174 if handler():
175 return
176
177
178class TestPageHandler(BasePageHandler):
initial.commit94958cf2008-07-26 22:42:52 +0000179
180 def __init__(self, request, client_address, socket_server):
akalin@chromium.org154bb132010-11-12 02:20:27 +0000181 connect_handlers = [
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000182 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000183 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000184 self.DefaultConnectResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000185 get_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000186 self.NoCacheMaxAgeTimeHandler,
187 self.NoCacheTimeHandler,
188 self.CacheTimeHandler,
189 self.CacheExpiresHandler,
190 self.CacheProxyRevalidateHandler,
191 self.CachePrivateHandler,
192 self.CachePublicHandler,
193 self.CacheSMaxAgeHandler,
194 self.CacheMustRevalidateHandler,
195 self.CacheMustRevalidateMaxAgeHandler,
196 self.CacheNoStoreHandler,
197 self.CacheNoStoreMaxAgeHandler,
198 self.CacheNoTransformHandler,
199 self.DownloadHandler,
200 self.DownloadFinishHandler,
201 self.EchoHeader,
ananta@chromium.org219b2062009-10-23 16:09:41 +0000202 self.EchoHeaderOverride,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000203 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000204 self.FileHandler,
205 self.RealFileWithCommonHeaderHandler,
206 self.RealBZ2FileWithCommonHeaderHandler,
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000207 self.SetCookieHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000208 self.AuthBasicHandler,
209 self.AuthDigestHandler,
210 self.SlowServerHandler,
211 self.ContentTypeHandler,
212 self.ServerRedirectHandler,
213 self.ClientRedirectHandler,
tony@chromium.org03266982010-03-05 03:18:42 +0000214 self.MultipartHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000215 self.DefaultResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000216 post_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000217 self.EchoTitleHandler,
218 self.EchoAllHandler,
mnissler@chromium.org7c939802010-11-11 08:47:14 +0000219 self.EchoHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000220 self.DeviceManagementHandler] + get_handlers
221 put_handlers = [
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000222 self.EchoTitleHandler,
223 self.EchoAllHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000224 self.EchoHandler] + get_handlers
initial.commit94958cf2008-07-26 22:42:52 +0000225
maruel@google.come250a9b2009-03-10 17:39:46 +0000226 self._mime_types = {
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000227 'crx' : 'application/x-chrome-extension',
maruel@google.come250a9b2009-03-10 17:39:46 +0000228 'gif': 'image/gif',
229 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000230 'jpg' : 'image/jpeg',
jam@chromium.org41550782010-11-17 23:47:50 +0000231 'xml' : 'text/xml',
232 'pdf' : 'application/pdf'
maruel@google.come250a9b2009-03-10 17:39:46 +0000233 }
initial.commit94958cf2008-07-26 22:42:52 +0000234 self._default_mime_type = 'text/html'
235
akalin@chromium.org154bb132010-11-12 02:20:27 +0000236 BasePageHandler.__init__(self, request, client_address, socket_server,
237 connect_handlers, get_handlers, post_handlers,
238 put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000239
initial.commit94958cf2008-07-26 22:42:52 +0000240 def GetMIMETypeFromName(self, file_name):
241 """Returns the mime type for the specified file_name. So far it only looks
242 at the file extension."""
243
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000244 (shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000245 if len(extension) == 0:
246 # no extension.
247 return self._default_mime_type
248
ericroman@google.comc17ca532009-05-07 03:51:05 +0000249 # extension starts with a dot, so we need to remove it
250 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000251
initial.commit94958cf2008-07-26 22:42:52 +0000252 def NoCacheMaxAgeTimeHandler(self):
253 """This request handler yields a page with the title set to the current
254 system time, and no caching requested."""
255
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000256 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000257 return False
258
259 self.send_response(200)
260 self.send_header('Cache-Control', 'max-age=0')
261 self.send_header('Content-type', 'text/html')
262 self.end_headers()
263
maruel@google.come250a9b2009-03-10 17:39:46 +0000264 self.wfile.write('<html><head><title>%s</title></head></html>' %
265 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000266
267 return True
268
269 def NoCacheTimeHandler(self):
270 """This request handler yields a page with the title set to the current
271 system time, and no caching requested."""
272
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000273 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000274 return False
275
276 self.send_response(200)
277 self.send_header('Cache-Control', 'no-cache')
278 self.send_header('Content-type', 'text/html')
279 self.end_headers()
280
maruel@google.come250a9b2009-03-10 17:39:46 +0000281 self.wfile.write('<html><head><title>%s</title></head></html>' %
282 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000283
284 return True
285
286 def CacheTimeHandler(self):
287 """This request handler yields a page with the title set to the current
288 system time, and allows caching for one minute."""
289
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000290 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000291 return False
292
293 self.send_response(200)
294 self.send_header('Cache-Control', 'max-age=60')
295 self.send_header('Content-type', 'text/html')
296 self.end_headers()
297
maruel@google.come250a9b2009-03-10 17:39:46 +0000298 self.wfile.write('<html><head><title>%s</title></head></html>' %
299 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000300
301 return True
302
303 def CacheExpiresHandler(self):
304 """This request handler yields a page with the title set to the current
305 system time, and set the page to expire on 1 Jan 2099."""
306
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000307 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000308 return False
309
310 self.send_response(200)
311 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
312 self.send_header('Content-type', 'text/html')
313 self.end_headers()
314
maruel@google.come250a9b2009-03-10 17:39:46 +0000315 self.wfile.write('<html><head><title>%s</title></head></html>' %
316 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000317
318 return True
319
320 def CacheProxyRevalidateHandler(self):
321 """This request handler yields a page with the title set to the current
322 system time, and allows caching for 60 seconds"""
323
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000324 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000325 return False
326
327 self.send_response(200)
328 self.send_header('Content-type', 'text/html')
329 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
330 self.end_headers()
331
maruel@google.come250a9b2009-03-10 17:39:46 +0000332 self.wfile.write('<html><head><title>%s</title></head></html>' %
333 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000334
335 return True
336
337 def CachePrivateHandler(self):
338 """This request handler yields a page with the title set to the current
339 system time, and allows caching for 5 seconds."""
340
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000341 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000342 return False
343
344 self.send_response(200)
345 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000346 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000347 self.end_headers()
348
maruel@google.come250a9b2009-03-10 17:39:46 +0000349 self.wfile.write('<html><head><title>%s</title></head></html>' %
350 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000351
352 return True
353
354 def CachePublicHandler(self):
355 """This request handler yields a page with the title set to the current
356 system time, and allows caching for 5 seconds."""
357
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000358 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000359 return False
360
361 self.send_response(200)
362 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000363 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000364 self.end_headers()
365
maruel@google.come250a9b2009-03-10 17:39:46 +0000366 self.wfile.write('<html><head><title>%s</title></head></html>' %
367 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000368
369 return True
370
371 def CacheSMaxAgeHandler(self):
372 """This request handler yields a page with the title set to the current
373 system time, and does not allow for caching."""
374
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000375 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000376 return False
377
378 self.send_response(200)
379 self.send_header('Content-type', 'text/html')
380 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
381 self.end_headers()
382
maruel@google.come250a9b2009-03-10 17:39:46 +0000383 self.wfile.write('<html><head><title>%s</title></head></html>' %
384 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000385
386 return True
387
388 def CacheMustRevalidateHandler(self):
389 """This request handler yields a page with the title set to the current
390 system time, and does not allow caching."""
391
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000392 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000393 return False
394
395 self.send_response(200)
396 self.send_header('Content-type', 'text/html')
397 self.send_header('Cache-Control', 'must-revalidate')
398 self.end_headers()
399
maruel@google.come250a9b2009-03-10 17:39:46 +0000400 self.wfile.write('<html><head><title>%s</title></head></html>' %
401 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000402
403 return True
404
405 def CacheMustRevalidateMaxAgeHandler(self):
406 """This request handler yields a page with the title set to the current
407 system time, and does not allow caching event though max-age of 60
408 seconds is specified."""
409
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000410 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000411 return False
412
413 self.send_response(200)
414 self.send_header('Content-type', 'text/html')
415 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
416 self.end_headers()
417
maruel@google.come250a9b2009-03-10 17:39:46 +0000418 self.wfile.write('<html><head><title>%s</title></head></html>' %
419 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000420
421 return True
422
initial.commit94958cf2008-07-26 22:42:52 +0000423 def CacheNoStoreHandler(self):
424 """This request handler yields a page with the title set to the current
425 system time, and does not allow the page to be stored."""
426
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000427 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000428 return False
429
430 self.send_response(200)
431 self.send_header('Content-type', 'text/html')
432 self.send_header('Cache-Control', 'no-store')
433 self.end_headers()
434
maruel@google.come250a9b2009-03-10 17:39:46 +0000435 self.wfile.write('<html><head><title>%s</title></head></html>' %
436 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000437
438 return True
439
440 def CacheNoStoreMaxAgeHandler(self):
441 """This request handler yields a page with the title set to the current
442 system time, and does not allow the page to be stored even though max-age
443 of 60 seconds is specified."""
444
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000445 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000446 return False
447
448 self.send_response(200)
449 self.send_header('Content-type', 'text/html')
450 self.send_header('Cache-Control', 'max-age=60, no-store')
451 self.end_headers()
452
maruel@google.come250a9b2009-03-10 17:39:46 +0000453 self.wfile.write('<html><head><title>%s</title></head></html>' %
454 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000455
456 return True
457
458
459 def CacheNoTransformHandler(self):
460 """This request handler yields a page with the title set to the current
461 system time, and does not allow the content to transformed during
462 user-agent caching"""
463
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000464 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000465 return False
466
467 self.send_response(200)
468 self.send_header('Content-type', 'text/html')
469 self.send_header('Cache-Control', 'no-transform')
470 self.end_headers()
471
maruel@google.come250a9b2009-03-10 17:39:46 +0000472 self.wfile.write('<html><head><title>%s</title></head></html>' %
473 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000474
475 return True
476
477 def EchoHeader(self):
478 """This handler echoes back the value of a specific request header."""
ananta@chromium.org219b2062009-10-23 16:09:41 +0000479 """The only difference between this function and the EchoHeaderOverride"""
480 """function is in the parameter being passed to the helper function"""
481 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000482
ananta@chromium.org219b2062009-10-23 16:09:41 +0000483 def EchoHeaderOverride(self):
484 """This handler echoes back the value of a specific request header."""
485 """The UrlRequest unit tests also execute for ChromeFrame which uses"""
486 """IE to issue HTTP requests using the host network stack."""
487 """The Accept and Charset tests which expect the server to echo back"""
488 """the corresponding headers fail here as IE returns cached responses"""
489 """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
490 """treats this request as a new request and does not cache it."""
491 return self.EchoHeaderHelper("/echoheaderoverride")
492
493 def EchoHeaderHelper(self, echo_header):
494 """This function echoes back the value of the request header passed in."""
495 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000496 return False
497
498 query_char = self.path.find('?')
499 if query_char != -1:
500 header_name = self.path[query_char+1:]
501
502 self.send_response(200)
503 self.send_header('Content-type', 'text/plain')
504 self.send_header('Cache-control', 'max-age=60000')
505 # insert a vary header to properly indicate that the cachability of this
506 # request is subject to value of the request header being echoed.
507 if len(header_name) > 0:
508 self.send_header('Vary', header_name)
509 self.end_headers()
510
511 if len(header_name) > 0:
512 self.wfile.write(self.headers.getheader(header_name))
513
514 return True
515
516 def EchoHandler(self):
517 """This handler just echoes back the payload of the request, for testing
518 form submission."""
519
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000520 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000521 return False
522
523 self.send_response(200)
524 self.send_header('Content-type', 'text/html')
525 self.end_headers()
526 length = int(self.headers.getheader('content-length'))
527 request = self.rfile.read(length)
528 self.wfile.write(request)
529 return True
530
531 def EchoTitleHandler(self):
532 """This handler is like Echo, but sets the page title to the request."""
533
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000534 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000535 return False
536
537 self.send_response(200)
538 self.send_header('Content-type', 'text/html')
539 self.end_headers()
540 length = int(self.headers.getheader('content-length'))
541 request = self.rfile.read(length)
542 self.wfile.write('<html><head><title>')
543 self.wfile.write(request)
544 self.wfile.write('</title></head></html>')
545 return True
546
547 def EchoAllHandler(self):
548 """This handler yields a (more) human-readable page listing information
549 about the request header & contents."""
550
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000551 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000552 return False
553
554 self.send_response(200)
555 self.send_header('Content-type', 'text/html')
556 self.end_headers()
557 self.wfile.write('<html><head><style>'
558 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
559 '</style></head><body>'
560 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000561 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000562 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000563
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000564 if self.command == 'POST' or self.command == 'PUT':
ericroman@google.coma47622b2008-11-15 04:36:51 +0000565 length = int(self.headers.getheader('content-length'))
566 qs = self.rfile.read(length)
567 params = cgi.parse_qs(qs, keep_blank_values=1)
568
569 for param in params:
570 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000571
572 self.wfile.write('</pre>')
573
574 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
575
576 self.wfile.write('</body></html>')
577 return True
578
579 def DownloadHandler(self):
580 """This handler sends a downloadable file with or without reporting
581 the size (6K)."""
582
583 if self.path.startswith("/download-unknown-size"):
584 send_length = False
585 elif self.path.startswith("/download-known-size"):
586 send_length = True
587 else:
588 return False
589
590 #
591 # The test which uses this functionality is attempting to send
592 # small chunks of data to the client. Use a fairly large buffer
593 # so that we'll fill chrome's IO buffer enough to force it to
594 # actually write the data.
595 # See also the comments in the client-side of this test in
596 # download_uitest.cc
597 #
598 size_chunk1 = 35*1024
599 size_chunk2 = 10*1024
600
601 self.send_response(200)
602 self.send_header('Content-type', 'application/octet-stream')
603 self.send_header('Cache-Control', 'max-age=0')
604 if send_length:
605 self.send_header('Content-Length', size_chunk1 + size_chunk2)
606 self.end_headers()
607
608 # First chunk of data:
609 self.wfile.write("*" * size_chunk1)
610 self.wfile.flush()
611
612 # handle requests until one of them clears this flag.
613 self.server.waitForDownload = True
614 while self.server.waitForDownload:
615 self.server.handle_request()
616
617 # Second chunk of data:
618 self.wfile.write("*" * size_chunk2)
619 return True
620
621 def DownloadFinishHandler(self):
622 """This handler just tells the server to finish the current download."""
623
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000624 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000625 return False
626
627 self.server.waitForDownload = False
628 self.send_response(200)
629 self.send_header('Content-type', 'text/html')
630 self.send_header('Cache-Control', 'max-age=0')
631 self.end_headers()
632 return True
633
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000634 def _ReplaceFileData(self, data, query_parameters):
635 """Replaces matching substrings in a file.
636
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000637 If the 'replace_text' URL query parameter is present, it is expected to be
638 of the form old_text:new_text, which indicates that any old_text strings in
639 the file are replaced with new_text. Multiple 'replace_text' parameters may
640 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000641
642 If the parameters are not present, |data| is returned.
643 """
644 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000645 replace_text_values = query_dict.get('replace_text', [])
646 for replace_text_value in replace_text_values:
647 replace_text_args = replace_text_value.split(':')
648 if len(replace_text_args) != 2:
649 raise ValueError(
650 'replace_text must be of form old_text:new_text. Actual value: %s' %
651 replace_text_value)
652 old_text_b64, new_text_b64 = replace_text_args
653 old_text = base64.urlsafe_b64decode(old_text_b64)
654 new_text = base64.urlsafe_b64decode(new_text_b64)
655 data = data.replace(old_text, new_text)
656 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000657
initial.commit94958cf2008-07-26 22:42:52 +0000658 def FileHandler(self):
659 """This handler sends the contents of the requested file. Wow, it's like
660 a real webserver!"""
661
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000662 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000663 if not self.path.startswith(prefix):
664 return False
665
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000666 # Consume a request body if present.
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000667 if self.command == 'POST' or self.command == 'PUT' :
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000668 self.rfile.read(int(self.headers.getheader('content-length')))
669
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000670 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
671 sub_path = url_path[len(prefix):]
672 entries = sub_path.split('/')
673 file_path = os.path.join(self.server.data_dir, *entries)
674 if os.path.isdir(file_path):
675 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000676
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000677 if not os.path.isfile(file_path):
678 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000679 self.send_error(404)
680 return True
681
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000682 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000683 data = f.read()
684 f.close()
685
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000686 data = self._ReplaceFileData(data, query)
687
initial.commit94958cf2008-07-26 22:42:52 +0000688 # If file.mock-http-headers exists, it contains the headers we
689 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000690 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000691 if os.path.isfile(headers_path):
692 f = open(headers_path, "r")
693
694 # "HTTP/1.1 200 OK"
695 response = f.readline()
696 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
697 self.send_response(int(status_code))
698
699 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000700 header_values = re.findall('(\S+):\s*(.*)', line)
701 if len(header_values) > 0:
702 # "name: value"
703 name, value = header_values[0]
704 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000705 f.close()
706 else:
707 # Could be more generic once we support mime-type sniffing, but for
708 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000709
710 range = self.headers.get('Range')
711 if range and range.startswith('bytes='):
712 # Note this doesn't handle all valid byte range values (i.e. open ended
713 # ones), just enough for what we needed so far.
714 range = range[6:].split('-')
715 start = int(range[0])
716 end = int(range[1])
717
718 self.send_response(206)
719 content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
720 str(len(data))
721 self.send_header('Content-Range', content_range)
722 data = data[start: end + 1]
723 else:
724 self.send_response(200)
725
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000726 self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000727 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000728 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000729 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000730 self.end_headers()
731
732 self.wfile.write(data)
733
734 return True
735
736 def RealFileWithCommonHeaderHandler(self):
737 """This handler sends the contents of the requested file without the pseudo
738 http head!"""
739
740 prefix='/realfiles/'
741 if not self.path.startswith(prefix):
742 return False
743
744 file = self.path[len(prefix):]
745 path = os.path.join(self.server.data_dir, file)
746
747 try:
748 f = open(path, "rb")
749 data = f.read()
750 f.close()
751
752 # just simply set the MIME as octal stream
753 self.send_response(200)
754 self.send_header('Content-type', 'application/octet-stream')
755 self.end_headers()
756
757 self.wfile.write(data)
758 except:
759 self.send_error(404)
760
761 return True
762
763 def RealBZ2FileWithCommonHeaderHandler(self):
764 """This handler sends the bzip2 contents of the requested file with
765 corresponding Content-Encoding field in http head!"""
766
767 prefix='/realbz2files/'
768 if not self.path.startswith(prefix):
769 return False
770
771 parts = self.path.split('?')
772 file = parts[0][len(prefix):]
773 path = os.path.join(self.server.data_dir, file) + '.bz2'
774
775 if len(parts) > 1:
776 options = parts[1]
777 else:
778 options = ''
779
780 try:
781 self.send_response(200)
782 accept_encoding = self.headers.get("Accept-Encoding")
783 if accept_encoding.find("bzip2") != -1:
784 f = open(path, "rb")
785 data = f.read()
786 f.close()
787 self.send_header('Content-Encoding', 'bzip2')
788 self.send_header('Content-type', 'application/x-bzip2')
789 self.end_headers()
790 if options == 'incremental-header':
791 self.wfile.write(data[:1])
792 self.wfile.flush()
793 time.sleep(1.0)
794 self.wfile.write(data[1:])
795 else:
796 self.wfile.write(data)
797 else:
798 """client do not support bzip2 format, send pseudo content
799 """
800 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
801 self.end_headers()
802 self.wfile.write("you do not support bzip2 encoding")
803 except:
804 self.send_error(404)
805
806 return True
807
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000808 def SetCookieHandler(self):
809 """This handler just sets a cookie, for testing cookie handling."""
810
811 if not self._ShouldHandleRequest("/set-cookie"):
812 return False
813
814 query_char = self.path.find('?')
815 if query_char != -1:
816 cookie_values = self.path[query_char + 1:].split('&')
817 else:
818 cookie_values = ("",)
819 self.send_response(200)
820 self.send_header('Content-type', 'text/html')
821 for cookie_value in cookie_values:
822 self.send_header('Set-Cookie', '%s' % cookie_value)
823 self.end_headers()
824 for cookie_value in cookie_values:
825 self.wfile.write('%s' % cookie_value)
826 return True
827
initial.commit94958cf2008-07-26 22:42:52 +0000828 def AuthBasicHandler(self):
829 """This handler tests 'Basic' authentication. It just sends a page with
830 title 'user/pass' if you succeed."""
831
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000832 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000833 return False
834
835 username = userpass = password = b64str = ""
836
ericroman@google.com239b4d82009-03-27 04:00:22 +0000837 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
838
initial.commit94958cf2008-07-26 22:42:52 +0000839 auth = self.headers.getheader('authorization')
840 try:
841 if not auth:
842 raise Exception('no auth')
843 b64str = re.findall(r'Basic (\S+)', auth)[0]
844 userpass = base64.b64decode(b64str)
845 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
846 if password != 'secret':
847 raise Exception('wrong password')
848 except Exception, e:
849 # Authentication failed.
850 self.send_response(401)
851 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
852 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000853 if set_cookie_if_challenged:
854 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000855 self.end_headers()
856 self.wfile.write('<html><head>')
857 self.wfile.write('<title>Denied: %s</title>' % e)
858 self.wfile.write('</head><body>')
859 self.wfile.write('auth=%s<p>' % auth)
860 self.wfile.write('b64str=%s<p>' % b64str)
861 self.wfile.write('username: %s<p>' % username)
862 self.wfile.write('userpass: %s<p>' % userpass)
863 self.wfile.write('password: %s<p>' % password)
864 self.wfile.write('You sent:<br>%s<p>' % self.headers)
865 self.wfile.write('</body></html>')
866 return True
867
868 # Authentication successful. (Return a cachable response to allow for
869 # testing cached pages that require authentication.)
870 if_none_match = self.headers.getheader('if-none-match')
871 if if_none_match == "abc":
872 self.send_response(304)
873 self.end_headers()
874 else:
875 self.send_response(200)
876 self.send_header('Content-type', 'text/html')
877 self.send_header('Cache-control', 'max-age=60000')
878 self.send_header('Etag', 'abc')
879 self.end_headers()
880 self.wfile.write('<html><head>')
881 self.wfile.write('<title>%s/%s</title>' % (username, password))
882 self.wfile.write('</head><body>')
883 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000884 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000885 self.wfile.write('</body></html>')
886
887 return True
888
tonyg@chromium.org75054202010-03-31 22:06:10 +0000889 def GetNonce(self, force_reset=False):
890 """Returns a nonce that's stable per request path for the server's lifetime.
initial.commit94958cf2008-07-26 22:42:52 +0000891
tonyg@chromium.org75054202010-03-31 22:06:10 +0000892 This is a fake implementation. A real implementation would only use a given
893 nonce a single time (hence the name n-once). However, for the purposes of
894 unittesting, we don't care about the security of the nonce.
895
896 Args:
897 force_reset: Iff set, the nonce will be changed. Useful for testing the
898 "stale" response.
899 """
900 if force_reset or not self.server.nonce_time:
901 self.server.nonce_time = time.time()
902 return _new_md5('privatekey%s%d' %
903 (self.path, self.server.nonce_time)).hexdigest()
904
905 def AuthDigestHandler(self):
906 """This handler tests 'Digest' authentication.
907
908 It just sends a page with title 'user/pass' if you succeed.
909
910 A stale response is sent iff "stale" is present in the request path.
911 """
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000912 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000913 return False
914
tonyg@chromium.org75054202010-03-31 22:06:10 +0000915 stale = 'stale' in self.path
916 nonce = self.GetNonce(force_reset=stale)
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000917 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000918 password = 'secret'
919 realm = 'testrealm'
920
921 auth = self.headers.getheader('authorization')
922 pairs = {}
923 try:
924 if not auth:
925 raise Exception('no auth')
926 if not auth.startswith('Digest'):
927 raise Exception('not digest')
928 # Pull out all the name="value" pairs as a dictionary.
929 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
930
931 # Make sure it's all valid.
932 if pairs['nonce'] != nonce:
933 raise Exception('wrong nonce')
934 if pairs['opaque'] != opaque:
935 raise Exception('wrong opaque')
936
937 # Check the 'response' value and make sure it matches our magic hash.
938 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000939 hash_a1 = _new_md5(
940 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000941 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000942 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000943 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000944 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
945 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000946 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000947
948 if pairs['response'] != response:
949 raise Exception('wrong password')
950 except Exception, e:
951 # Authentication failed.
952 self.send_response(401)
953 hdr = ('Digest '
954 'realm="%s", '
955 'domain="/", '
956 'qop="auth", '
957 'algorithm=MD5, '
958 'nonce="%s", '
959 'opaque="%s"') % (realm, nonce, opaque)
960 if stale:
961 hdr += ', stale="TRUE"'
962 self.send_header('WWW-Authenticate', hdr)
963 self.send_header('Content-type', 'text/html')
964 self.end_headers()
965 self.wfile.write('<html><head>')
966 self.wfile.write('<title>Denied: %s</title>' % e)
967 self.wfile.write('</head><body>')
968 self.wfile.write('auth=%s<p>' % auth)
969 self.wfile.write('pairs=%s<p>' % pairs)
970 self.wfile.write('You sent:<br>%s<p>' % self.headers)
971 self.wfile.write('We are replying:<br>%s<p>' % hdr)
972 self.wfile.write('</body></html>')
973 return True
974
975 # Authentication successful.
976 self.send_response(200)
977 self.send_header('Content-type', 'text/html')
978 self.end_headers()
979 self.wfile.write('<html><head>')
980 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
981 self.wfile.write('</head><body>')
982 self.wfile.write('auth=%s<p>' % auth)
983 self.wfile.write('pairs=%s<p>' % pairs)
984 self.wfile.write('</body></html>')
985
986 return True
987
988 def SlowServerHandler(self):
989 """Wait for the user suggested time before responding. The syntax is
990 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000991 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000992 return False
993 query_char = self.path.find('?')
994 wait_sec = 1.0
995 if query_char >= 0:
996 try:
997 wait_sec = int(self.path[query_char + 1:])
998 except ValueError:
999 pass
1000 time.sleep(wait_sec)
1001 self.send_response(200)
1002 self.send_header('Content-type', 'text/plain')
1003 self.end_headers()
1004 self.wfile.write("waited %d seconds" % wait_sec)
1005 return True
1006
1007 def ContentTypeHandler(self):
1008 """Returns a string of html with the given content type. E.g.,
1009 /contenttype?text/css returns an html file with the Content-Type
1010 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001011 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001012 return False
1013 query_char = self.path.find('?')
1014 content_type = self.path[query_char + 1:].strip()
1015 if not content_type:
1016 content_type = 'text/html'
1017 self.send_response(200)
1018 self.send_header('Content-Type', content_type)
1019 self.end_headers()
1020 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1021 return True
1022
1023 def ServerRedirectHandler(self):
1024 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001025 '/server-redirect?http://foo.bar/asdf' to redirect to
1026 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001027
1028 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001029 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001030 return False
1031
1032 query_char = self.path.find('?')
1033 if query_char < 0 or len(self.path) <= query_char + 1:
1034 self.sendRedirectHelp(test_name)
1035 return True
1036 dest = self.path[query_char + 1:]
1037
1038 self.send_response(301) # moved permanently
1039 self.send_header('Location', dest)
1040 self.send_header('Content-type', 'text/html')
1041 self.end_headers()
1042 self.wfile.write('<html><head>')
1043 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1044
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001045 return True
initial.commit94958cf2008-07-26 22:42:52 +00001046
1047 def ClientRedirectHandler(self):
1048 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001049 '/client-redirect?http://foo.bar/asdf' to redirect to
1050 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001051
1052 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001053 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001054 return False
1055
1056 query_char = self.path.find('?');
1057 if query_char < 0 or len(self.path) <= query_char + 1:
1058 self.sendRedirectHelp(test_name)
1059 return True
1060 dest = self.path[query_char + 1:]
1061
1062 self.send_response(200)
1063 self.send_header('Content-type', 'text/html')
1064 self.end_headers()
1065 self.wfile.write('<html><head>')
1066 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1067 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1068
1069 return True
1070
tony@chromium.org03266982010-03-05 03:18:42 +00001071 def MultipartHandler(self):
1072 """Send a multipart response (10 text/html pages)."""
1073 test_name = "/multipart"
1074 if not self._ShouldHandleRequest(test_name):
1075 return False
1076
1077 num_frames = 10
1078 bound = '12345'
1079 self.send_response(200)
1080 self.send_header('Content-type',
1081 'multipart/x-mixed-replace;boundary=' + bound)
1082 self.end_headers()
1083
1084 for i in xrange(num_frames):
1085 self.wfile.write('--' + bound + '\r\n')
1086 self.wfile.write('Content-type: text/html\r\n\r\n')
1087 self.wfile.write('<title>page ' + str(i) + '</title>')
1088 self.wfile.write('page ' + str(i))
1089
1090 self.wfile.write('--' + bound + '--')
1091 return True
1092
initial.commit94958cf2008-07-26 22:42:52 +00001093 def DefaultResponseHandler(self):
1094 """This is the catch-all response handler for requests that aren't handled
1095 by one of the special handlers above.
1096 Note that we specify the content-length as without it the https connection
1097 is not closed properly (and the browser keeps expecting data)."""
1098
1099 contents = "Default response given for path: " + self.path
1100 self.send_response(200)
1101 self.send_header('Content-type', 'text/html')
1102 self.send_header("Content-Length", len(contents))
1103 self.end_headers()
1104 self.wfile.write(contents)
1105 return True
1106
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001107 def RedirectConnectHandler(self):
1108 """Sends a redirect to the CONNECT request for www.redirect.com. This
1109 response is not specified by the RFC, so the browser should not follow
1110 the redirect."""
1111
1112 if (self.path.find("www.redirect.com") < 0):
1113 return False
1114
1115 dest = "http://www.destination.com/foo.js"
1116
1117 self.send_response(302) # moved temporarily
1118 self.send_header('Location', dest)
1119 self.send_header('Connection', 'close')
1120 self.end_headers()
1121 return True
1122
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001123 def ServerAuthConnectHandler(self):
1124 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1125 response doesn't make sense because the proxy server cannot request
1126 server authentication."""
1127
1128 if (self.path.find("www.server-auth.com") < 0):
1129 return False
1130
1131 challenge = 'Basic realm="WallyWorld"'
1132
1133 self.send_response(401) # unauthorized
1134 self.send_header('WWW-Authenticate', challenge)
1135 self.send_header('Connection', 'close')
1136 self.end_headers()
1137 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001138
1139 def DefaultConnectResponseHandler(self):
1140 """This is the catch-all response handler for CONNECT requests that aren't
1141 handled by one of the special handlers above. Real Web servers respond
1142 with 400 to CONNECT requests."""
1143
1144 contents = "Your client has issued a malformed or illegal request."
1145 self.send_response(400) # bad request
1146 self.send_header('Content-type', 'text/html')
1147 self.send_header("Content-Length", len(contents))
1148 self.end_headers()
1149 self.wfile.write(contents)
1150 return True
1151
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001152 def DeviceManagementHandler(self):
1153 """Delegates to the device management service used for cloud policy."""
1154 if not self._ShouldHandleRequest("/device_management"):
1155 return False
1156
1157 length = int(self.headers.getheader('content-length'))
1158 raw_request = self.rfile.read(length)
1159
1160 if not self.server._device_management_handler:
1161 import device_management
1162 policy_path = os.path.join(self.server.data_dir, 'device_management')
1163 self.server._device_management_handler = (
1164 device_management.TestServer(policy_path))
1165
1166 http_response, raw_reply = (
1167 self.server._device_management_handler.HandleRequest(self.path,
1168 self.headers,
1169 raw_request))
1170 self.send_response(http_response)
1171 self.end_headers()
1172 self.wfile.write(raw_reply)
1173 return True
1174
initial.commit94958cf2008-07-26 22:42:52 +00001175 # called by the redirect handling function when there is no parameter
1176 def sendRedirectHelp(self, redirect_name):
1177 self.send_response(200)
1178 self.send_header('Content-type', 'text/html')
1179 self.end_headers()
1180 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1181 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1182 self.wfile.write('</body></html>')
1183
akalin@chromium.org154bb132010-11-12 02:20:27 +00001184
1185class SyncPageHandler(BasePageHandler):
1186 """Handler for the main HTTP sync server."""
1187
1188 def __init__(self, request, client_address, sync_http_server):
1189 get_handlers = [self.ChromiumSyncTimeHandler]
1190 post_handlers = [self.ChromiumSyncCommandHandler]
1191 BasePageHandler.__init__(self, request, client_address,
1192 sync_http_server, [], get_handlers,
1193 post_handlers, [])
1194
1195 def ChromiumSyncTimeHandler(self):
1196 """Handle Chromium sync .../time requests.
1197
1198 The syncer sometimes checks server reachability by examining /time.
1199 """
1200 test_name = "/chromiumsync/time"
1201 if not self._ShouldHandleRequest(test_name):
1202 return False
1203
1204 self.send_response(200)
1205 self.send_header('Content-type', 'text/html')
1206 self.end_headers()
1207 return True
1208
1209 def ChromiumSyncCommandHandler(self):
1210 """Handle a chromiumsync command arriving via http.
1211
1212 This covers all sync protocol commands: authentication, getupdates, and
1213 commit.
1214 """
1215 test_name = "/chromiumsync/command"
1216 if not self._ShouldHandleRequest(test_name):
1217 return False
1218
1219 length = int(self.headers.getheader('content-length'))
1220 raw_request = self.rfile.read(length)
1221
1222 http_response, raw_reply = self.server.HandleCommand(
1223 self.path, raw_request)
1224 self.send_response(http_response)
1225 self.end_headers()
1226 self.wfile.write(raw_reply)
1227 return True
1228
1229
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001230def MakeDataDir():
1231 if options.data_dir:
1232 if not os.path.isdir(options.data_dir):
1233 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1234 return None
1235 my_data_dir = options.data_dir
1236 else:
1237 # Create the default path to our data dir, relative to the exe dir.
1238 my_data_dir = os.path.dirname(sys.argv[0])
1239 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00001240 "test", "data")
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001241
1242 #TODO(ibrar): Must use Find* funtion defined in google\tools
1243 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1244
1245 return my_data_dir
1246
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001247class FileMultiplexer:
1248 def __init__(self, fd1, fd2) :
1249 self.__fd1 = fd1
1250 self.__fd2 = fd2
1251
1252 def __del__(self) :
1253 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1254 self.__fd1.close()
1255 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1256 self.__fd2.close()
1257
1258 def write(self, text) :
1259 self.__fd1.write(text)
1260 self.__fd2.write(text)
1261
1262 def flush(self) :
1263 self.__fd1.flush()
1264 self.__fd2.flush()
1265
initial.commit94958cf2008-07-26 22:42:52 +00001266def main(options, args):
initial.commit94958cf2008-07-26 22:42:52 +00001267 logfile = open('testserver.log', 'w')
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001268 sys.stdout = FileMultiplexer(sys.stdout, logfile)
1269 sys.stderr = FileMultiplexer(sys.stderr, logfile)
initial.commit94958cf2008-07-26 22:42:52 +00001270
1271 port = options.port
1272
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001273 if options.server_type == SERVER_HTTP:
1274 if options.cert:
1275 # let's make sure the cert file exists.
1276 if not os.path.isfile(options.cert):
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001277 print 'specified server cert file not found: ' + options.cert + \
1278 ' exiting...'
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001279 return
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001280 for ca_cert in options.ssl_client_ca:
1281 if not os.path.isfile(ca_cert):
1282 print 'specified trusted client CA file not found: ' + ca_cert + \
1283 ' exiting...'
1284 return
davidben@chromium.org31282a12010-08-07 01:10:02 +00001285 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001286 options.ssl_client_auth, options.ssl_client_ca,
1287 options.ssl_bulk_cipher)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001288 print 'HTTPS server started on port %d...' % server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001289 else:
phajdan.jr@chromium.orgfa49b5f2010-07-23 20:38:56 +00001290 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001291 print 'HTTP server started on port %d...' % server.server_port
erikkay@google.com70397b62008-12-30 21:49:21 +00001292
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001293 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001294 server.file_root_url = options.file_root_url
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001295 listen_port = server.server_port
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001296 server._device_management_handler = None
akalin@chromium.org154bb132010-11-12 02:20:27 +00001297 elif options.server_type == SERVER_SYNC:
1298 server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1299 print 'Sync HTTP server started on port %d...' % server.server_port
1300 listen_port = server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001301 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001302 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001303 my_data_dir = MakeDataDir()
1304
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001305 # Instantiate a dummy authorizer for managing 'virtual' users
1306 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1307
1308 # Define a new user having full r/w permissions and a read-only
1309 # anonymous user
1310 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1311
1312 authorizer.add_anonymous(my_data_dir)
1313
1314 # Instantiate FTP handler class
1315 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1316 ftp_handler.authorizer = authorizer
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001317
1318 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001319 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1320 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001321
1322 # Instantiate FTP server class and listen to 127.0.0.1:port
1323 address = ('127.0.0.1', port)
1324 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001325 listen_port = server.socket.getsockname()[1]
1326 print 'FTP server started on port %d...' % listen_port
initial.commit94958cf2008-07-26 22:42:52 +00001327
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001328 # Notify the parent that we've started. (BaseServer subclasses
1329 # bind their sockets on construction.)
1330 if options.startup_pipe is not None:
akalin@chromium.org2b0a9c42010-11-25 06:43:12 +00001331 server_data = {
1332 'port': listen_port
1333 }
1334 server_data_json = json.dumps(server_data)
1335 debug('sending server_data: %s' % server_data_json)
1336 server_data_len = len(server_data_json)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001337 if sys.platform == 'win32':
1338 fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1339 else:
1340 fd = options.startup_pipe
1341 startup_pipe = os.fdopen(fd, "w")
akalin@chromium.org2b0a9c42010-11-25 06:43:12 +00001342 # First write the data length as an unsigned 4-byte value. This
1343 # is _not_ using network byte ordering since the other end of the
1344 # pipe is on the same machine.
1345 startup_pipe.write(struct.pack('=L', server_data_len))
1346 startup_pipe.write(server_data_json)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001347 startup_pipe.close()
1348
initial.commit94958cf2008-07-26 22:42:52 +00001349 try:
1350 server.serve_forever()
1351 except KeyboardInterrupt:
1352 print 'shutting down server'
1353 server.stop = True
1354
1355if __name__ == '__main__':
1356 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001357 option_parser.add_option("-f", '--ftp', action='store_const',
1358 const=SERVER_FTP, default=SERVER_HTTP,
1359 dest='server_type',
akalin@chromium.org154bb132010-11-12 02:20:27 +00001360 help='start up an FTP server.')
1361 option_parser.add_option('', '--sync', action='store_const',
1362 const=SERVER_SYNC, default=SERVER_HTTP,
1363 dest='server_type',
1364 help='start up a sync server.')
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001365 option_parser.add_option('', '--port', default='0', type='int',
1366 help='Port used by the server. If unspecified, the '
1367 'server will listen on an ephemeral port.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001368 option_parser.add_option('', '--data-dir', dest='data_dir',
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001369 help='Directory from which to read the files.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001370 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001371 help='Specify that https should be used, specify '
1372 'the path to the cert containing the private key '
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001373 'the server should use.')
davidben@chromium.org31282a12010-08-07 01:10:02 +00001374 option_parser.add_option('', '--ssl-client-auth', action='store_true',
1375 help='Require SSL client auth on every connection.')
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001376 option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1377 help='Specify that the client certificate request '
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001378 'should include the CA named in the subject of '
1379 'the DER-encoded certificate contained in the '
1380 'specified file. This option may appear multiple '
1381 'times, indicating multiple CA names should be '
1382 'sent in the request.')
1383 option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1384 help='Specify the bulk encryption algorithm(s)'
1385 'that will be accepted by the SSL server. Valid '
1386 'values are "aes256", "aes128", "3des", "rc4". If '
1387 'omitted, all algorithms will be used. This '
1388 'option may appear multiple times, indicating '
1389 'multiple algorithms should be enabled.');
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001390 option_parser.add_option('', '--file-root-url', default='/files/',
1391 help='Specify a root URL for files served.')
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001392 option_parser.add_option('', '--startup-pipe', type='int',
1393 dest='startup_pipe',
1394 help='File handle of pipe to parent process')
initial.commit94958cf2008-07-26 22:42:52 +00001395 options, args = option_parser.parse_args()
1396
1397 sys.exit(main(options, args))