blob: 7ce812353981a5251b8c33a42b02c068f6b395ad [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
akalin@chromium.orgf8479c62010-11-27 01:52:52 +000052# Using debug() seems to cause hangs on XP: see http://crbug.com/64515 .
initial.commit94958cf2008-07-26 22:42:52 +000053debug_output = sys.stderr
54def debug(str):
55 debug_output.write(str + "\n")
56 debug_output.flush()
57
58class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
59 """This is a specialization of of BaseHTTPServer to allow it
60 to be exited cleanly (by setting its "stop" member to True)."""
61
62 def serve_forever(self):
63 self.stop = False
tonyg@chromium.org75054202010-03-31 22:06:10 +000064 self.nonce_time = None
initial.commit94958cf2008-07-26 22:42:52 +000065 while not self.stop:
66 self.handle_request()
67 self.socket.close()
68
69class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
70 """This is a specialization of StoppableHTTPerver that add https support."""
71
davidben@chromium.org31282a12010-08-07 01:10:02 +000072 def __init__(self, server_address, request_hander_class, cert_path,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000073 ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers):
initial.commit94958cf2008-07-26 22:42:52 +000074 s = open(cert_path).read()
75 x509 = tlslite.api.X509()
76 x509.parse(s)
77 self.cert_chain = tlslite.api.X509CertChain([x509])
78 s = open(cert_path).read()
79 self.private_key = tlslite.api.parsePEMKey(s, private=True)
davidben@chromium.org31282a12010-08-07 01:10:02 +000080 self.ssl_client_auth = ssl_client_auth
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +000081 self.ssl_client_cas = []
82 for ca_file in ssl_client_cas:
83 s = open(ca_file).read()
84 x509 = tlslite.api.X509()
85 x509.parse(s)
86 self.ssl_client_cas.append(x509.subject)
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000087 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
88 if ssl_bulk_ciphers is not None:
89 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
initial.commit94958cf2008-07-26 22:42:52 +000090
91 self.session_cache = tlslite.api.SessionCache()
92 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
93
94 def handshake(self, tlsConnection):
95 """Creates the SSL connection."""
96 try:
97 tlsConnection.handshakeServer(certChain=self.cert_chain,
98 privateKey=self.private_key,
davidben@chromium.org31282a12010-08-07 01:10:02 +000099 sessionCache=self.session_cache,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000100 reqCert=self.ssl_client_auth,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +0000101 settings=self.ssl_handshake_settings,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000102 reqCAs=self.ssl_client_cas)
initial.commit94958cf2008-07-26 22:42:52 +0000103 tlsConnection.ignoreAbruptClose = True
104 return True
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +0000105 except tlslite.api.TLSAbruptCloseError:
106 # Ignore abrupt close.
107 return True
initial.commit94958cf2008-07-26 22:42:52 +0000108 except tlslite.api.TLSError, error:
109 print "Handshake failure:", str(error)
110 return False
111
akalin@chromium.org154bb132010-11-12 02:20:27 +0000112
113class SyncHTTPServer(StoppableHTTPServer):
114 """An HTTP server that handles sync commands."""
115
116 def __init__(self, server_address, request_handler_class):
117 # We import here to avoid pulling in chromiumsync's dependencies
118 # unless strictly necessary.
119 import chromiumsync
120 self._sync_handler = chromiumsync.TestServer()
121 StoppableHTTPServer.__init__(self, server_address, request_handler_class)
122
123 def HandleCommand(self, query, raw_request):
124 return self._sync_handler.HandleCommand(query, raw_request)
125
126
127class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
128
129 def __init__(self, request, client_address, socket_server,
130 connect_handlers, get_handlers, post_handlers, put_handlers):
131 self._connect_handlers = connect_handlers
132 self._get_handlers = get_handlers
133 self._post_handlers = post_handlers
134 self._put_handlers = put_handlers
135 BaseHTTPServer.BaseHTTPRequestHandler.__init__(
136 self, request, client_address, socket_server)
137
138 def log_request(self, *args, **kwargs):
139 # Disable request logging to declutter test log output.
140 pass
141
142 def _ShouldHandleRequest(self, handler_name):
143 """Determines if the path can be handled by the handler.
144
145 We consider a handler valid if the path begins with the
146 handler name. It can optionally be followed by "?*", "/*".
147 """
148
149 pattern = re.compile('%s($|\?|/).*' % handler_name)
150 return pattern.match(self.path)
151
152 def do_CONNECT(self):
153 for handler in self._connect_handlers:
154 if handler():
155 return
156
157 def do_GET(self):
158 for handler in self._get_handlers:
159 if handler():
160 return
161
162 def do_POST(self):
163 for handler in self._post_handlers:
164 if handler():
165 return
166
167 def do_PUT(self):
168 for handler in self._put_handlers:
169 if handler():
170 return
171
172
173class TestPageHandler(BasePageHandler):
initial.commit94958cf2008-07-26 22:42:52 +0000174
175 def __init__(self, request, client_address, socket_server):
akalin@chromium.org154bb132010-11-12 02:20:27 +0000176 connect_handlers = [
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000177 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000178 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000179 self.DefaultConnectResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000180 get_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000181 self.NoCacheMaxAgeTimeHandler,
182 self.NoCacheTimeHandler,
183 self.CacheTimeHandler,
184 self.CacheExpiresHandler,
185 self.CacheProxyRevalidateHandler,
186 self.CachePrivateHandler,
187 self.CachePublicHandler,
188 self.CacheSMaxAgeHandler,
189 self.CacheMustRevalidateHandler,
190 self.CacheMustRevalidateMaxAgeHandler,
191 self.CacheNoStoreHandler,
192 self.CacheNoStoreMaxAgeHandler,
193 self.CacheNoTransformHandler,
194 self.DownloadHandler,
195 self.DownloadFinishHandler,
196 self.EchoHeader,
ananta@chromium.org219b2062009-10-23 16:09:41 +0000197 self.EchoHeaderOverride,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000198 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000199 self.FileHandler,
200 self.RealFileWithCommonHeaderHandler,
201 self.RealBZ2FileWithCommonHeaderHandler,
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000202 self.SetCookieHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000203 self.AuthBasicHandler,
204 self.AuthDigestHandler,
205 self.SlowServerHandler,
206 self.ContentTypeHandler,
207 self.ServerRedirectHandler,
208 self.ClientRedirectHandler,
tony@chromium.org03266982010-03-05 03:18:42 +0000209 self.MultipartHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000210 self.DefaultResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000211 post_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000212 self.EchoTitleHandler,
213 self.EchoAllHandler,
mnissler@chromium.org7c939802010-11-11 08:47:14 +0000214 self.EchoHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000215 self.DeviceManagementHandler] + get_handlers
216 put_handlers = [
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000217 self.EchoTitleHandler,
218 self.EchoAllHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000219 self.EchoHandler] + get_handlers
initial.commit94958cf2008-07-26 22:42:52 +0000220
maruel@google.come250a9b2009-03-10 17:39:46 +0000221 self._mime_types = {
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000222 'crx' : 'application/x-chrome-extension',
maruel@google.come250a9b2009-03-10 17:39:46 +0000223 'gif': 'image/gif',
224 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000225 'jpg' : 'image/jpeg',
jam@chromium.org41550782010-11-17 23:47:50 +0000226 'xml' : 'text/xml',
227 'pdf' : 'application/pdf'
maruel@google.come250a9b2009-03-10 17:39:46 +0000228 }
initial.commit94958cf2008-07-26 22:42:52 +0000229 self._default_mime_type = 'text/html'
230
akalin@chromium.org154bb132010-11-12 02:20:27 +0000231 BasePageHandler.__init__(self, request, client_address, socket_server,
232 connect_handlers, get_handlers, post_handlers,
233 put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000234
initial.commit94958cf2008-07-26 22:42:52 +0000235 def GetMIMETypeFromName(self, file_name):
236 """Returns the mime type for the specified file_name. So far it only looks
237 at the file extension."""
238
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000239 (shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000240 if len(extension) == 0:
241 # no extension.
242 return self._default_mime_type
243
ericroman@google.comc17ca532009-05-07 03:51:05 +0000244 # extension starts with a dot, so we need to remove it
245 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000246
initial.commit94958cf2008-07-26 22:42:52 +0000247 def NoCacheMaxAgeTimeHandler(self):
248 """This request handler yields a page with the title set to the current
249 system time, and no caching requested."""
250
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000251 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000252 return False
253
254 self.send_response(200)
255 self.send_header('Cache-Control', 'max-age=0')
256 self.send_header('Content-type', 'text/html')
257 self.end_headers()
258
maruel@google.come250a9b2009-03-10 17:39:46 +0000259 self.wfile.write('<html><head><title>%s</title></head></html>' %
260 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000261
262 return True
263
264 def NoCacheTimeHandler(self):
265 """This request handler yields a page with the title set to the current
266 system time, and no caching requested."""
267
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000268 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000269 return False
270
271 self.send_response(200)
272 self.send_header('Cache-Control', 'no-cache')
273 self.send_header('Content-type', 'text/html')
274 self.end_headers()
275
maruel@google.come250a9b2009-03-10 17:39:46 +0000276 self.wfile.write('<html><head><title>%s</title></head></html>' %
277 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000278
279 return True
280
281 def CacheTimeHandler(self):
282 """This request handler yields a page with the title set to the current
283 system time, and allows caching for one minute."""
284
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000285 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000286 return False
287
288 self.send_response(200)
289 self.send_header('Cache-Control', 'max-age=60')
290 self.send_header('Content-type', 'text/html')
291 self.end_headers()
292
maruel@google.come250a9b2009-03-10 17:39:46 +0000293 self.wfile.write('<html><head><title>%s</title></head></html>' %
294 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000295
296 return True
297
298 def CacheExpiresHandler(self):
299 """This request handler yields a page with the title set to the current
300 system time, and set the page to expire on 1 Jan 2099."""
301
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000302 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000303 return False
304
305 self.send_response(200)
306 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
307 self.send_header('Content-type', 'text/html')
308 self.end_headers()
309
maruel@google.come250a9b2009-03-10 17:39:46 +0000310 self.wfile.write('<html><head><title>%s</title></head></html>' %
311 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000312
313 return True
314
315 def CacheProxyRevalidateHandler(self):
316 """This request handler yields a page with the title set to the current
317 system time, and allows caching for 60 seconds"""
318
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000319 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000320 return False
321
322 self.send_response(200)
323 self.send_header('Content-type', 'text/html')
324 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
325 self.end_headers()
326
maruel@google.come250a9b2009-03-10 17:39:46 +0000327 self.wfile.write('<html><head><title>%s</title></head></html>' %
328 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000329
330 return True
331
332 def CachePrivateHandler(self):
333 """This request handler yields a page with the title set to the current
334 system time, and allows caching for 5 seconds."""
335
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000336 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000337 return False
338
339 self.send_response(200)
340 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000341 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000342 self.end_headers()
343
maruel@google.come250a9b2009-03-10 17:39:46 +0000344 self.wfile.write('<html><head><title>%s</title></head></html>' %
345 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000346
347 return True
348
349 def CachePublicHandler(self):
350 """This request handler yields a page with the title set to the current
351 system time, and allows caching for 5 seconds."""
352
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000353 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000354 return False
355
356 self.send_response(200)
357 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000358 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000359 self.end_headers()
360
maruel@google.come250a9b2009-03-10 17:39:46 +0000361 self.wfile.write('<html><head><title>%s</title></head></html>' %
362 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000363
364 return True
365
366 def CacheSMaxAgeHandler(self):
367 """This request handler yields a page with the title set to the current
368 system time, and does not allow for caching."""
369
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000370 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000371 return False
372
373 self.send_response(200)
374 self.send_header('Content-type', 'text/html')
375 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
376 self.end_headers()
377
maruel@google.come250a9b2009-03-10 17:39:46 +0000378 self.wfile.write('<html><head><title>%s</title></head></html>' %
379 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000380
381 return True
382
383 def CacheMustRevalidateHandler(self):
384 """This request handler yields a page with the title set to the current
385 system time, and does not allow caching."""
386
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000387 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000388 return False
389
390 self.send_response(200)
391 self.send_header('Content-type', 'text/html')
392 self.send_header('Cache-Control', 'must-revalidate')
393 self.end_headers()
394
maruel@google.come250a9b2009-03-10 17:39:46 +0000395 self.wfile.write('<html><head><title>%s</title></head></html>' %
396 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000397
398 return True
399
400 def CacheMustRevalidateMaxAgeHandler(self):
401 """This request handler yields a page with the title set to the current
402 system time, and does not allow caching event though max-age of 60
403 seconds is specified."""
404
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000405 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000406 return False
407
408 self.send_response(200)
409 self.send_header('Content-type', 'text/html')
410 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
411 self.end_headers()
412
maruel@google.come250a9b2009-03-10 17:39:46 +0000413 self.wfile.write('<html><head><title>%s</title></head></html>' %
414 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000415
416 return True
417
initial.commit94958cf2008-07-26 22:42:52 +0000418 def CacheNoStoreHandler(self):
419 """This request handler yields a page with the title set to the current
420 system time, and does not allow the page to be stored."""
421
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000422 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000423 return False
424
425 self.send_response(200)
426 self.send_header('Content-type', 'text/html')
427 self.send_header('Cache-Control', 'no-store')
428 self.end_headers()
429
maruel@google.come250a9b2009-03-10 17:39:46 +0000430 self.wfile.write('<html><head><title>%s</title></head></html>' %
431 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000432
433 return True
434
435 def CacheNoStoreMaxAgeHandler(self):
436 """This request handler yields a page with the title set to the current
437 system time, and does not allow the page to be stored even though max-age
438 of 60 seconds is specified."""
439
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000440 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000441 return False
442
443 self.send_response(200)
444 self.send_header('Content-type', 'text/html')
445 self.send_header('Cache-Control', 'max-age=60, no-store')
446 self.end_headers()
447
maruel@google.come250a9b2009-03-10 17:39:46 +0000448 self.wfile.write('<html><head><title>%s</title></head></html>' %
449 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000450
451 return True
452
453
454 def CacheNoTransformHandler(self):
455 """This request handler yields a page with the title set to the current
456 system time, and does not allow the content to transformed during
457 user-agent caching"""
458
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000459 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000460 return False
461
462 self.send_response(200)
463 self.send_header('Content-type', 'text/html')
464 self.send_header('Cache-Control', 'no-transform')
465 self.end_headers()
466
maruel@google.come250a9b2009-03-10 17:39:46 +0000467 self.wfile.write('<html><head><title>%s</title></head></html>' %
468 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000469
470 return True
471
472 def EchoHeader(self):
473 """This handler echoes back the value of a specific request header."""
ananta@chromium.org219b2062009-10-23 16:09:41 +0000474 """The only difference between this function and the EchoHeaderOverride"""
475 """function is in the parameter being passed to the helper function"""
476 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000477
ananta@chromium.org219b2062009-10-23 16:09:41 +0000478 def EchoHeaderOverride(self):
479 """This handler echoes back the value of a specific request header."""
480 """The UrlRequest unit tests also execute for ChromeFrame which uses"""
481 """IE to issue HTTP requests using the host network stack."""
482 """The Accept and Charset tests which expect the server to echo back"""
483 """the corresponding headers fail here as IE returns cached responses"""
484 """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
485 """treats this request as a new request and does not cache it."""
486 return self.EchoHeaderHelper("/echoheaderoverride")
487
488 def EchoHeaderHelper(self, echo_header):
489 """This function echoes back the value of the request header passed in."""
490 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000491 return False
492
493 query_char = self.path.find('?')
494 if query_char != -1:
495 header_name = self.path[query_char+1:]
496
497 self.send_response(200)
498 self.send_header('Content-type', 'text/plain')
499 self.send_header('Cache-control', 'max-age=60000')
500 # insert a vary header to properly indicate that the cachability of this
501 # request is subject to value of the request header being echoed.
502 if len(header_name) > 0:
503 self.send_header('Vary', header_name)
504 self.end_headers()
505
506 if len(header_name) > 0:
507 self.wfile.write(self.headers.getheader(header_name))
508
509 return True
510
511 def EchoHandler(self):
512 """This handler just echoes back the payload of the request, for testing
513 form submission."""
514
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000515 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000516 return False
517
518 self.send_response(200)
519 self.send_header('Content-type', 'text/html')
520 self.end_headers()
521 length = int(self.headers.getheader('content-length'))
522 request = self.rfile.read(length)
523 self.wfile.write(request)
524 return True
525
526 def EchoTitleHandler(self):
527 """This handler is like Echo, but sets the page title to the request."""
528
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000529 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000530 return False
531
532 self.send_response(200)
533 self.send_header('Content-type', 'text/html')
534 self.end_headers()
535 length = int(self.headers.getheader('content-length'))
536 request = self.rfile.read(length)
537 self.wfile.write('<html><head><title>')
538 self.wfile.write(request)
539 self.wfile.write('</title></head></html>')
540 return True
541
542 def EchoAllHandler(self):
543 """This handler yields a (more) human-readable page listing information
544 about the request header & contents."""
545
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000546 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000547 return False
548
549 self.send_response(200)
550 self.send_header('Content-type', 'text/html')
551 self.end_headers()
552 self.wfile.write('<html><head><style>'
553 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
554 '</style></head><body>'
555 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000556 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000557 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000558
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000559 if self.command == 'POST' or self.command == 'PUT':
ericroman@google.coma47622b2008-11-15 04:36:51 +0000560 length = int(self.headers.getheader('content-length'))
561 qs = self.rfile.read(length)
562 params = cgi.parse_qs(qs, keep_blank_values=1)
563
564 for param in params:
565 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000566
567 self.wfile.write('</pre>')
568
569 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
570
571 self.wfile.write('</body></html>')
572 return True
573
574 def DownloadHandler(self):
575 """This handler sends a downloadable file with or without reporting
576 the size (6K)."""
577
578 if self.path.startswith("/download-unknown-size"):
579 send_length = False
580 elif self.path.startswith("/download-known-size"):
581 send_length = True
582 else:
583 return False
584
585 #
586 # The test which uses this functionality is attempting to send
587 # small chunks of data to the client. Use a fairly large buffer
588 # so that we'll fill chrome's IO buffer enough to force it to
589 # actually write the data.
590 # See also the comments in the client-side of this test in
591 # download_uitest.cc
592 #
593 size_chunk1 = 35*1024
594 size_chunk2 = 10*1024
595
596 self.send_response(200)
597 self.send_header('Content-type', 'application/octet-stream')
598 self.send_header('Cache-Control', 'max-age=0')
599 if send_length:
600 self.send_header('Content-Length', size_chunk1 + size_chunk2)
601 self.end_headers()
602
603 # First chunk of data:
604 self.wfile.write("*" * size_chunk1)
605 self.wfile.flush()
606
607 # handle requests until one of them clears this flag.
608 self.server.waitForDownload = True
609 while self.server.waitForDownload:
610 self.server.handle_request()
611
612 # Second chunk of data:
613 self.wfile.write("*" * size_chunk2)
614 return True
615
616 def DownloadFinishHandler(self):
617 """This handler just tells the server to finish the current download."""
618
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000619 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000620 return False
621
622 self.server.waitForDownload = False
623 self.send_response(200)
624 self.send_header('Content-type', 'text/html')
625 self.send_header('Cache-Control', 'max-age=0')
626 self.end_headers()
627 return True
628
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000629 def _ReplaceFileData(self, data, query_parameters):
630 """Replaces matching substrings in a file.
631
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000632 If the 'replace_text' URL query parameter is present, it is expected to be
633 of the form old_text:new_text, which indicates that any old_text strings in
634 the file are replaced with new_text. Multiple 'replace_text' parameters may
635 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000636
637 If the parameters are not present, |data| is returned.
638 """
639 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000640 replace_text_values = query_dict.get('replace_text', [])
641 for replace_text_value in replace_text_values:
642 replace_text_args = replace_text_value.split(':')
643 if len(replace_text_args) != 2:
644 raise ValueError(
645 'replace_text must be of form old_text:new_text. Actual value: %s' %
646 replace_text_value)
647 old_text_b64, new_text_b64 = replace_text_args
648 old_text = base64.urlsafe_b64decode(old_text_b64)
649 new_text = base64.urlsafe_b64decode(new_text_b64)
650 data = data.replace(old_text, new_text)
651 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000652
initial.commit94958cf2008-07-26 22:42:52 +0000653 def FileHandler(self):
654 """This handler sends the contents of the requested file. Wow, it's like
655 a real webserver!"""
656
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000657 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000658 if not self.path.startswith(prefix):
659 return False
660
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000661 # Consume a request body if present.
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000662 if self.command == 'POST' or self.command == 'PUT' :
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000663 self.rfile.read(int(self.headers.getheader('content-length')))
664
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000665 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
666 sub_path = url_path[len(prefix):]
667 entries = sub_path.split('/')
668 file_path = os.path.join(self.server.data_dir, *entries)
669 if os.path.isdir(file_path):
670 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000671
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000672 if not os.path.isfile(file_path):
673 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000674 self.send_error(404)
675 return True
676
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000677 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000678 data = f.read()
679 f.close()
680
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000681 data = self._ReplaceFileData(data, query)
682
initial.commit94958cf2008-07-26 22:42:52 +0000683 # If file.mock-http-headers exists, it contains the headers we
684 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000685 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000686 if os.path.isfile(headers_path):
687 f = open(headers_path, "r")
688
689 # "HTTP/1.1 200 OK"
690 response = f.readline()
691 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
692 self.send_response(int(status_code))
693
694 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000695 header_values = re.findall('(\S+):\s*(.*)', line)
696 if len(header_values) > 0:
697 # "name: value"
698 name, value = header_values[0]
699 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000700 f.close()
701 else:
702 # Could be more generic once we support mime-type sniffing, but for
703 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000704
705 range = self.headers.get('Range')
706 if range and range.startswith('bytes='):
707 # Note this doesn't handle all valid byte range values (i.e. open ended
708 # ones), just enough for what we needed so far.
709 range = range[6:].split('-')
710 start = int(range[0])
711 end = int(range[1])
712
713 self.send_response(206)
714 content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
715 str(len(data))
716 self.send_header('Content-Range', content_range)
717 data = data[start: end + 1]
718 else:
719 self.send_response(200)
720
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000721 self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000722 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000723 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000724 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000725 self.end_headers()
726
727 self.wfile.write(data)
728
729 return True
730
731 def RealFileWithCommonHeaderHandler(self):
732 """This handler sends the contents of the requested file without the pseudo
733 http head!"""
734
735 prefix='/realfiles/'
736 if not self.path.startswith(prefix):
737 return False
738
739 file = self.path[len(prefix):]
740 path = os.path.join(self.server.data_dir, file)
741
742 try:
743 f = open(path, "rb")
744 data = f.read()
745 f.close()
746
747 # just simply set the MIME as octal stream
748 self.send_response(200)
749 self.send_header('Content-type', 'application/octet-stream')
750 self.end_headers()
751
752 self.wfile.write(data)
753 except:
754 self.send_error(404)
755
756 return True
757
758 def RealBZ2FileWithCommonHeaderHandler(self):
759 """This handler sends the bzip2 contents of the requested file with
760 corresponding Content-Encoding field in http head!"""
761
762 prefix='/realbz2files/'
763 if not self.path.startswith(prefix):
764 return False
765
766 parts = self.path.split('?')
767 file = parts[0][len(prefix):]
768 path = os.path.join(self.server.data_dir, file) + '.bz2'
769
770 if len(parts) > 1:
771 options = parts[1]
772 else:
773 options = ''
774
775 try:
776 self.send_response(200)
777 accept_encoding = self.headers.get("Accept-Encoding")
778 if accept_encoding.find("bzip2") != -1:
779 f = open(path, "rb")
780 data = f.read()
781 f.close()
782 self.send_header('Content-Encoding', 'bzip2')
783 self.send_header('Content-type', 'application/x-bzip2')
784 self.end_headers()
785 if options == 'incremental-header':
786 self.wfile.write(data[:1])
787 self.wfile.flush()
788 time.sleep(1.0)
789 self.wfile.write(data[1:])
790 else:
791 self.wfile.write(data)
792 else:
793 """client do not support bzip2 format, send pseudo content
794 """
795 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
796 self.end_headers()
797 self.wfile.write("you do not support bzip2 encoding")
798 except:
799 self.send_error(404)
800
801 return True
802
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000803 def SetCookieHandler(self):
804 """This handler just sets a cookie, for testing cookie handling."""
805
806 if not self._ShouldHandleRequest("/set-cookie"):
807 return False
808
809 query_char = self.path.find('?')
810 if query_char != -1:
811 cookie_values = self.path[query_char + 1:].split('&')
812 else:
813 cookie_values = ("",)
814 self.send_response(200)
815 self.send_header('Content-type', 'text/html')
816 for cookie_value in cookie_values:
817 self.send_header('Set-Cookie', '%s' % cookie_value)
818 self.end_headers()
819 for cookie_value in cookie_values:
820 self.wfile.write('%s' % cookie_value)
821 return True
822
initial.commit94958cf2008-07-26 22:42:52 +0000823 def AuthBasicHandler(self):
824 """This handler tests 'Basic' authentication. It just sends a page with
825 title 'user/pass' if you succeed."""
826
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000827 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000828 return False
829
830 username = userpass = password = b64str = ""
831
ericroman@google.com239b4d82009-03-27 04:00:22 +0000832 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0
833
initial.commit94958cf2008-07-26 22:42:52 +0000834 auth = self.headers.getheader('authorization')
835 try:
836 if not auth:
837 raise Exception('no auth')
838 b64str = re.findall(r'Basic (\S+)', auth)[0]
839 userpass = base64.b64decode(b64str)
840 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
841 if password != 'secret':
842 raise Exception('wrong password')
843 except Exception, e:
844 # Authentication failed.
845 self.send_response(401)
846 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"')
847 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000848 if set_cookie_if_challenged:
849 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000850 self.end_headers()
851 self.wfile.write('<html><head>')
852 self.wfile.write('<title>Denied: %s</title>' % e)
853 self.wfile.write('</head><body>')
854 self.wfile.write('auth=%s<p>' % auth)
855 self.wfile.write('b64str=%s<p>' % b64str)
856 self.wfile.write('username: %s<p>' % username)
857 self.wfile.write('userpass: %s<p>' % userpass)
858 self.wfile.write('password: %s<p>' % password)
859 self.wfile.write('You sent:<br>%s<p>' % self.headers)
860 self.wfile.write('</body></html>')
861 return True
862
863 # Authentication successful. (Return a cachable response to allow for
864 # testing cached pages that require authentication.)
865 if_none_match = self.headers.getheader('if-none-match')
866 if if_none_match == "abc":
867 self.send_response(304)
868 self.end_headers()
869 else:
870 self.send_response(200)
871 self.send_header('Content-type', 'text/html')
872 self.send_header('Cache-control', 'max-age=60000')
873 self.send_header('Etag', 'abc')
874 self.end_headers()
875 self.wfile.write('<html><head>')
876 self.wfile.write('<title>%s/%s</title>' % (username, password))
877 self.wfile.write('</head><body>')
878 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000879 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000880 self.wfile.write('</body></html>')
881
882 return True
883
tonyg@chromium.org75054202010-03-31 22:06:10 +0000884 def GetNonce(self, force_reset=False):
885 """Returns a nonce that's stable per request path for the server's lifetime.
initial.commit94958cf2008-07-26 22:42:52 +0000886
tonyg@chromium.org75054202010-03-31 22:06:10 +0000887 This is a fake implementation. A real implementation would only use a given
888 nonce a single time (hence the name n-once). However, for the purposes of
889 unittesting, we don't care about the security of the nonce.
890
891 Args:
892 force_reset: Iff set, the nonce will be changed. Useful for testing the
893 "stale" response.
894 """
895 if force_reset or not self.server.nonce_time:
896 self.server.nonce_time = time.time()
897 return _new_md5('privatekey%s%d' %
898 (self.path, self.server.nonce_time)).hexdigest()
899
900 def AuthDigestHandler(self):
901 """This handler tests 'Digest' authentication.
902
903 It just sends a page with title 'user/pass' if you succeed.
904
905 A stale response is sent iff "stale" is present in the request path.
906 """
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000907 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000908 return False
909
tonyg@chromium.org75054202010-03-31 22:06:10 +0000910 stale = 'stale' in self.path
911 nonce = self.GetNonce(force_reset=stale)
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000912 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000913 password = 'secret'
914 realm = 'testrealm'
915
916 auth = self.headers.getheader('authorization')
917 pairs = {}
918 try:
919 if not auth:
920 raise Exception('no auth')
921 if not auth.startswith('Digest'):
922 raise Exception('not digest')
923 # Pull out all the name="value" pairs as a dictionary.
924 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
925
926 # Make sure it's all valid.
927 if pairs['nonce'] != nonce:
928 raise Exception('wrong nonce')
929 if pairs['opaque'] != opaque:
930 raise Exception('wrong opaque')
931
932 # Check the 'response' value and make sure it matches our magic hash.
933 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000934 hash_a1 = _new_md5(
935 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000936 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000937 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000938 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000939 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
940 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000941 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000942
943 if pairs['response'] != response:
944 raise Exception('wrong password')
945 except Exception, e:
946 # Authentication failed.
947 self.send_response(401)
948 hdr = ('Digest '
949 'realm="%s", '
950 'domain="/", '
951 'qop="auth", '
952 'algorithm=MD5, '
953 'nonce="%s", '
954 'opaque="%s"') % (realm, nonce, opaque)
955 if stale:
956 hdr += ', stale="TRUE"'
957 self.send_header('WWW-Authenticate', hdr)
958 self.send_header('Content-type', 'text/html')
959 self.end_headers()
960 self.wfile.write('<html><head>')
961 self.wfile.write('<title>Denied: %s</title>' % e)
962 self.wfile.write('</head><body>')
963 self.wfile.write('auth=%s<p>' % auth)
964 self.wfile.write('pairs=%s<p>' % pairs)
965 self.wfile.write('You sent:<br>%s<p>' % self.headers)
966 self.wfile.write('We are replying:<br>%s<p>' % hdr)
967 self.wfile.write('</body></html>')
968 return True
969
970 # Authentication successful.
971 self.send_response(200)
972 self.send_header('Content-type', 'text/html')
973 self.end_headers()
974 self.wfile.write('<html><head>')
975 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
976 self.wfile.write('</head><body>')
977 self.wfile.write('auth=%s<p>' % auth)
978 self.wfile.write('pairs=%s<p>' % pairs)
979 self.wfile.write('</body></html>')
980
981 return True
982
983 def SlowServerHandler(self):
984 """Wait for the user suggested time before responding. The syntax is
985 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000986 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +0000987 return False
988 query_char = self.path.find('?')
989 wait_sec = 1.0
990 if query_char >= 0:
991 try:
992 wait_sec = int(self.path[query_char + 1:])
993 except ValueError:
994 pass
995 time.sleep(wait_sec)
996 self.send_response(200)
997 self.send_header('Content-type', 'text/plain')
998 self.end_headers()
999 self.wfile.write("waited %d seconds" % wait_sec)
1000 return True
1001
1002 def ContentTypeHandler(self):
1003 """Returns a string of html with the given content type. E.g.,
1004 /contenttype?text/css returns an html file with the Content-Type
1005 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001006 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001007 return False
1008 query_char = self.path.find('?')
1009 content_type = self.path[query_char + 1:].strip()
1010 if not content_type:
1011 content_type = 'text/html'
1012 self.send_response(200)
1013 self.send_header('Content-Type', content_type)
1014 self.end_headers()
1015 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1016 return True
1017
1018 def ServerRedirectHandler(self):
1019 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001020 '/server-redirect?http://foo.bar/asdf' to redirect to
1021 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001022
1023 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001024 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001025 return False
1026
1027 query_char = self.path.find('?')
1028 if query_char < 0 or len(self.path) <= query_char + 1:
1029 self.sendRedirectHelp(test_name)
1030 return True
1031 dest = self.path[query_char + 1:]
1032
1033 self.send_response(301) # moved permanently
1034 self.send_header('Location', dest)
1035 self.send_header('Content-type', 'text/html')
1036 self.end_headers()
1037 self.wfile.write('<html><head>')
1038 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1039
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001040 return True
initial.commit94958cf2008-07-26 22:42:52 +00001041
1042 def ClientRedirectHandler(self):
1043 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001044 '/client-redirect?http://foo.bar/asdf' to redirect to
1045 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001046
1047 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001048 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001049 return False
1050
1051 query_char = self.path.find('?');
1052 if query_char < 0 or len(self.path) <= query_char + 1:
1053 self.sendRedirectHelp(test_name)
1054 return True
1055 dest = self.path[query_char + 1:]
1056
1057 self.send_response(200)
1058 self.send_header('Content-type', 'text/html')
1059 self.end_headers()
1060 self.wfile.write('<html><head>')
1061 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1062 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1063
1064 return True
1065
tony@chromium.org03266982010-03-05 03:18:42 +00001066 def MultipartHandler(self):
1067 """Send a multipart response (10 text/html pages)."""
1068 test_name = "/multipart"
1069 if not self._ShouldHandleRequest(test_name):
1070 return False
1071
1072 num_frames = 10
1073 bound = '12345'
1074 self.send_response(200)
1075 self.send_header('Content-type',
1076 'multipart/x-mixed-replace;boundary=' + bound)
1077 self.end_headers()
1078
1079 for i in xrange(num_frames):
1080 self.wfile.write('--' + bound + '\r\n')
1081 self.wfile.write('Content-type: text/html\r\n\r\n')
1082 self.wfile.write('<title>page ' + str(i) + '</title>')
1083 self.wfile.write('page ' + str(i))
1084
1085 self.wfile.write('--' + bound + '--')
1086 return True
1087
initial.commit94958cf2008-07-26 22:42:52 +00001088 def DefaultResponseHandler(self):
1089 """This is the catch-all response handler for requests that aren't handled
1090 by one of the special handlers above.
1091 Note that we specify the content-length as without it the https connection
1092 is not closed properly (and the browser keeps expecting data)."""
1093
1094 contents = "Default response given for path: " + self.path
1095 self.send_response(200)
1096 self.send_header('Content-type', 'text/html')
1097 self.send_header("Content-Length", len(contents))
1098 self.end_headers()
1099 self.wfile.write(contents)
1100 return True
1101
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001102 def RedirectConnectHandler(self):
1103 """Sends a redirect to the CONNECT request for www.redirect.com. This
1104 response is not specified by the RFC, so the browser should not follow
1105 the redirect."""
1106
1107 if (self.path.find("www.redirect.com") < 0):
1108 return False
1109
1110 dest = "http://www.destination.com/foo.js"
1111
1112 self.send_response(302) # moved temporarily
1113 self.send_header('Location', dest)
1114 self.send_header('Connection', 'close')
1115 self.end_headers()
1116 return True
1117
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001118 def ServerAuthConnectHandler(self):
1119 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1120 response doesn't make sense because the proxy server cannot request
1121 server authentication."""
1122
1123 if (self.path.find("www.server-auth.com") < 0):
1124 return False
1125
1126 challenge = 'Basic realm="WallyWorld"'
1127
1128 self.send_response(401) # unauthorized
1129 self.send_header('WWW-Authenticate', challenge)
1130 self.send_header('Connection', 'close')
1131 self.end_headers()
1132 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001133
1134 def DefaultConnectResponseHandler(self):
1135 """This is the catch-all response handler for CONNECT requests that aren't
1136 handled by one of the special handlers above. Real Web servers respond
1137 with 400 to CONNECT requests."""
1138
1139 contents = "Your client has issued a malformed or illegal request."
1140 self.send_response(400) # bad request
1141 self.send_header('Content-type', 'text/html')
1142 self.send_header("Content-Length", len(contents))
1143 self.end_headers()
1144 self.wfile.write(contents)
1145 return True
1146
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001147 def DeviceManagementHandler(self):
1148 """Delegates to the device management service used for cloud policy."""
1149 if not self._ShouldHandleRequest("/device_management"):
1150 return False
1151
1152 length = int(self.headers.getheader('content-length'))
1153 raw_request = self.rfile.read(length)
1154
1155 if not self.server._device_management_handler:
1156 import device_management
1157 policy_path = os.path.join(self.server.data_dir, 'device_management')
1158 self.server._device_management_handler = (
1159 device_management.TestServer(policy_path))
1160
1161 http_response, raw_reply = (
1162 self.server._device_management_handler.HandleRequest(self.path,
1163 self.headers,
1164 raw_request))
1165 self.send_response(http_response)
1166 self.end_headers()
1167 self.wfile.write(raw_reply)
1168 return True
1169
initial.commit94958cf2008-07-26 22:42:52 +00001170 # called by the redirect handling function when there is no parameter
1171 def sendRedirectHelp(self, redirect_name):
1172 self.send_response(200)
1173 self.send_header('Content-type', 'text/html')
1174 self.end_headers()
1175 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1176 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1177 self.wfile.write('</body></html>')
1178
akalin@chromium.org154bb132010-11-12 02:20:27 +00001179
1180class SyncPageHandler(BasePageHandler):
1181 """Handler for the main HTTP sync server."""
1182
1183 def __init__(self, request, client_address, sync_http_server):
1184 get_handlers = [self.ChromiumSyncTimeHandler]
1185 post_handlers = [self.ChromiumSyncCommandHandler]
1186 BasePageHandler.__init__(self, request, client_address,
1187 sync_http_server, [], get_handlers,
1188 post_handlers, [])
1189
1190 def ChromiumSyncTimeHandler(self):
1191 """Handle Chromium sync .../time requests.
1192
1193 The syncer sometimes checks server reachability by examining /time.
1194 """
1195 test_name = "/chromiumsync/time"
1196 if not self._ShouldHandleRequest(test_name):
1197 return False
1198
1199 self.send_response(200)
1200 self.send_header('Content-type', 'text/html')
1201 self.end_headers()
1202 return True
1203
1204 def ChromiumSyncCommandHandler(self):
1205 """Handle a chromiumsync command arriving via http.
1206
1207 This covers all sync protocol commands: authentication, getupdates, and
1208 commit.
1209 """
1210 test_name = "/chromiumsync/command"
1211 if not self._ShouldHandleRequest(test_name):
1212 return False
1213
1214 length = int(self.headers.getheader('content-length'))
1215 raw_request = self.rfile.read(length)
1216
1217 http_response, raw_reply = self.server.HandleCommand(
1218 self.path, raw_request)
1219 self.send_response(http_response)
1220 self.end_headers()
1221 self.wfile.write(raw_reply)
1222 return True
1223
1224
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001225def MakeDataDir():
1226 if options.data_dir:
1227 if not os.path.isdir(options.data_dir):
1228 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1229 return None
1230 my_data_dir = options.data_dir
1231 else:
1232 # Create the default path to our data dir, relative to the exe dir.
1233 my_data_dir = os.path.dirname(sys.argv[0])
1234 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00001235 "test", "data")
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001236
1237 #TODO(ibrar): Must use Find* funtion defined in google\tools
1238 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1239
1240 return my_data_dir
1241
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001242class FileMultiplexer:
1243 def __init__(self, fd1, fd2) :
1244 self.__fd1 = fd1
1245 self.__fd2 = fd2
1246
1247 def __del__(self) :
1248 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1249 self.__fd1.close()
1250 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1251 self.__fd2.close()
1252
1253 def write(self, text) :
1254 self.__fd1.write(text)
1255 self.__fd2.write(text)
1256
1257 def flush(self) :
1258 self.__fd1.flush()
1259 self.__fd2.flush()
1260
initial.commit94958cf2008-07-26 22:42:52 +00001261def main(options, args):
initial.commit94958cf2008-07-26 22:42:52 +00001262 logfile = open('testserver.log', 'w')
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001263 sys.stdout = FileMultiplexer(sys.stdout, logfile)
1264 sys.stderr = FileMultiplexer(sys.stderr, logfile)
initial.commit94958cf2008-07-26 22:42:52 +00001265
1266 port = options.port
1267
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001268 if options.server_type == SERVER_HTTP:
1269 if options.cert:
1270 # let's make sure the cert file exists.
1271 if not os.path.isfile(options.cert):
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001272 print 'specified server cert file not found: ' + options.cert + \
1273 ' exiting...'
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001274 return
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001275 for ca_cert in options.ssl_client_ca:
1276 if not os.path.isfile(ca_cert):
1277 print 'specified trusted client CA file not found: ' + ca_cert + \
1278 ' exiting...'
1279 return
davidben@chromium.org31282a12010-08-07 01:10:02 +00001280 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001281 options.ssl_client_auth, options.ssl_client_ca,
1282 options.ssl_bulk_cipher)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001283 print 'HTTPS server started on port %d...' % server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001284 else:
phajdan.jr@chromium.orgfa49b5f2010-07-23 20:38:56 +00001285 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001286 print 'HTTP server started on port %d...' % server.server_port
erikkay@google.com70397b62008-12-30 21:49:21 +00001287
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001288 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001289 server.file_root_url = options.file_root_url
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001290 listen_port = server.server_port
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001291 server._device_management_handler = None
akalin@chromium.org154bb132010-11-12 02:20:27 +00001292 elif options.server_type == SERVER_SYNC:
1293 server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1294 print 'Sync HTTP server started on port %d...' % server.server_port
1295 listen_port = server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001296 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001297 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001298 my_data_dir = MakeDataDir()
1299
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001300 # Instantiate a dummy authorizer for managing 'virtual' users
1301 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1302
1303 # Define a new user having full r/w permissions and a read-only
1304 # anonymous user
1305 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1306
1307 authorizer.add_anonymous(my_data_dir)
1308
1309 # Instantiate FTP handler class
1310 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1311 ftp_handler.authorizer = authorizer
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001312
1313 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001314 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1315 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001316
1317 # Instantiate FTP server class and listen to 127.0.0.1:port
1318 address = ('127.0.0.1', port)
1319 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001320 listen_port = server.socket.getsockname()[1]
1321 print 'FTP server started on port %d...' % listen_port
initial.commit94958cf2008-07-26 22:42:52 +00001322
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001323 # Notify the parent that we've started. (BaseServer subclasses
1324 # bind their sockets on construction.)
1325 if options.startup_pipe is not None:
akalin@chromium.org73b7b5c2010-11-26 19:42:26 +00001326 server_data = {
1327 'port': listen_port
1328 }
1329 server_data_json = simplejson.dumps(server_data)
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001330 server_data_len = len(server_data_json)
1331 print 'sending server_data: %s (%d bytes)' % (
1332 server_data_json, server_data_len)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001333 if sys.platform == 'win32':
1334 fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1335 else:
1336 fd = options.startup_pipe
1337 startup_pipe = os.fdopen(fd, "w")
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001338 # First write the data length as an unsigned 4-byte value. This
1339 # is _not_ using network byte ordering since the other end of the
1340 # pipe is on the same machine.
1341 startup_pipe.write(struct.pack('=L', server_data_len))
1342 startup_pipe.write(server_data_json)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001343 startup_pipe.close()
1344
initial.commit94958cf2008-07-26 22:42:52 +00001345 try:
1346 server.serve_forever()
1347 except KeyboardInterrupt:
1348 print 'shutting down server'
1349 server.stop = True
1350
1351if __name__ == '__main__':
1352 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001353 option_parser.add_option("-f", '--ftp', action='store_const',
1354 const=SERVER_FTP, default=SERVER_HTTP,
1355 dest='server_type',
akalin@chromium.org154bb132010-11-12 02:20:27 +00001356 help='start up an FTP server.')
1357 option_parser.add_option('', '--sync', action='store_const',
1358 const=SERVER_SYNC, default=SERVER_HTTP,
1359 dest='server_type',
1360 help='start up a sync server.')
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001361 option_parser.add_option('', '--port', default='0', type='int',
1362 help='Port used by the server. If unspecified, the '
1363 'server will listen on an ephemeral port.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001364 option_parser.add_option('', '--data-dir', dest='data_dir',
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001365 help='Directory from which to read the files.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001366 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001367 help='Specify that https should be used, specify '
1368 'the path to the cert containing the private key '
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001369 'the server should use.')
davidben@chromium.org31282a12010-08-07 01:10:02 +00001370 option_parser.add_option('', '--ssl-client-auth', action='store_true',
1371 help='Require SSL client auth on every connection.')
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001372 option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1373 help='Specify that the client certificate request '
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001374 'should include the CA named in the subject of '
1375 'the DER-encoded certificate contained in the '
1376 'specified file. This option may appear multiple '
1377 'times, indicating multiple CA names should be '
1378 'sent in the request.')
1379 option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1380 help='Specify the bulk encryption algorithm(s)'
1381 'that will be accepted by the SSL server. Valid '
1382 'values are "aes256", "aes128", "3des", "rc4". If '
1383 'omitted, all algorithms will be used. This '
1384 'option may appear multiple times, indicating '
1385 'multiple algorithms should be enabled.');
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001386 option_parser.add_option('', '--file-root-url', default='/files/',
1387 help='Specify a root URL for files served.')
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001388 option_parser.add_option('', '--startup-pipe', type='int',
1389 dest='startup_pipe',
1390 help='File handle of pipe to parent process')
initial.commit94958cf2008-07-26 22:42:52 +00001391 options, args = option_parser.parse_args()
1392
1393 sys.exit(main(options, args))