blob: acfafae79516c3e82ca85e4578329ae77079545b [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
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +000016import asyncore
initial.commit94958cf2008-07-26 22:42:52 +000017import base64
18import BaseHTTPServer
19import cgi
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +000020import errno
initial.commit94958cf2008-07-26 22:42:52 +000021import optparse
22import os
23import re
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +000024import select
akalin@chromium.org18e34882010-11-26 07:10:41 +000025import simplejson
initial.commit94958cf2008-07-26 22:42:52 +000026import SocketServer
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +000027import socket
initial.commit94958cf2008-07-26 22:42:52 +000028import sys
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +000029import struct
initial.commit94958cf2008-07-26 22:42:52 +000030import time
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +000031import urlparse
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +000032import warnings
33
34# Ignore deprecation warnings, they make our output more cluttered.
35warnings.filterwarnings("ignore", category=DeprecationWarning)
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000036
37import pyftpdlib.ftpserver
initial.commit94958cf2008-07-26 22:42:52 +000038import tlslite
39import tlslite.api
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +000040
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +000041try:
42 import hashlib
43 _new_md5 = hashlib.md5
44except ImportError:
45 import md5
46 _new_md5 = md5.new
47
davidben@chromium.org06fcf202010-09-22 18:15:23 +000048if sys.platform == 'win32':
49 import msvcrt
50
maruel@chromium.org756cf982009-03-05 12:46:38 +000051SERVER_HTTP = 0
erikkay@google.comd5182ff2009-01-08 20:45:27 +000052SERVER_FTP = 1
akalin@chromium.org154bb132010-11-12 02:20:27 +000053SERVER_SYNC = 2
initial.commit94958cf2008-07-26 22:42:52 +000054
akalin@chromium.orgf8479c62010-11-27 01:52:52 +000055# Using debug() seems to cause hangs on XP: see http://crbug.com/64515 .
initial.commit94958cf2008-07-26 22:42:52 +000056debug_output = sys.stderr
57def debug(str):
58 debug_output.write(str + "\n")
59 debug_output.flush()
60
61class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
62 """This is a specialization of of BaseHTTPServer to allow it
63 to be exited cleanly (by setting its "stop" member to True)."""
64
65 def serve_forever(self):
66 self.stop = False
tonyg@chromium.org75054202010-03-31 22:06:10 +000067 self.nonce_time = None
initial.commit94958cf2008-07-26 22:42:52 +000068 while not self.stop:
69 self.handle_request()
70 self.socket.close()
71
72class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer):
73 """This is a specialization of StoppableHTTPerver that add https support."""
74
davidben@chromium.org31282a12010-08-07 01:10:02 +000075 def __init__(self, server_address, request_hander_class, cert_path,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000076 ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers):
initial.commit94958cf2008-07-26 22:42:52 +000077 s = open(cert_path).read()
78 x509 = tlslite.api.X509()
79 x509.parse(s)
80 self.cert_chain = tlslite.api.X509CertChain([x509])
81 s = open(cert_path).read()
82 self.private_key = tlslite.api.parsePEMKey(s, private=True)
davidben@chromium.org31282a12010-08-07 01:10:02 +000083 self.ssl_client_auth = ssl_client_auth
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +000084 self.ssl_client_cas = []
85 for ca_file in ssl_client_cas:
86 s = open(ca_file).read()
87 x509 = tlslite.api.X509()
88 x509.parse(s)
89 self.ssl_client_cas.append(x509.subject)
rsleevi@chromium.org2124c812010-10-28 11:57:36 +000090 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
91 if ssl_bulk_ciphers is not None:
92 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
initial.commit94958cf2008-07-26 22:42:52 +000093
94 self.session_cache = tlslite.api.SessionCache()
95 StoppableHTTPServer.__init__(self, server_address, request_hander_class)
96
97 def handshake(self, tlsConnection):
98 """Creates the SSL connection."""
99 try:
100 tlsConnection.handshakeServer(certChain=self.cert_chain,
101 privateKey=self.private_key,
davidben@chromium.org31282a12010-08-07 01:10:02 +0000102 sessionCache=self.session_cache,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000103 reqCert=self.ssl_client_auth,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +0000104 settings=self.ssl_handshake_settings,
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +0000105 reqCAs=self.ssl_client_cas)
initial.commit94958cf2008-07-26 22:42:52 +0000106 tlsConnection.ignoreAbruptClose = True
107 return True
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +0000108 except tlslite.api.TLSAbruptCloseError:
109 # Ignore abrupt close.
110 return True
initial.commit94958cf2008-07-26 22:42:52 +0000111 except tlslite.api.TLSError, error:
112 print "Handshake failure:", str(error)
113 return False
114
akalin@chromium.org154bb132010-11-12 02:20:27 +0000115
116class SyncHTTPServer(StoppableHTTPServer):
117 """An HTTP server that handles sync commands."""
118
119 def __init__(self, server_address, request_handler_class):
120 # We import here to avoid pulling in chromiumsync's dependencies
121 # unless strictly necessary.
122 import chromiumsync
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000123 import xmppserver
akalin@chromium.org154bb132010-11-12 02:20:27 +0000124 StoppableHTTPServer.__init__(self, server_address, request_handler_class)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000125 self._sync_handler = chromiumsync.TestServer()
126 self._xmpp_socket_map = {}
127 self._xmpp_server = xmppserver.XmppServer(
128 self._xmpp_socket_map, ('localhost', 0))
129 self.xmpp_port = self._xmpp_server.getsockname()[1]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000130
131 def HandleCommand(self, query, raw_request):
132 return self._sync_handler.HandleCommand(query, raw_request)
133
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000134 def HandleRequestNoBlock(self):
135 """Handles a single request.
136
137 Copied from SocketServer._handle_request_noblock().
138 """
139 try:
140 request, client_address = self.get_request()
141 except socket.error:
142 return
143 if self.verify_request(request, client_address):
144 try:
145 self.process_request(request, client_address)
146 except:
147 self.handle_error(request, client_address)
148 self.close_request(request)
149
150 def serve_forever(self):
151 """This is a merge of asyncore.loop() and SocketServer.serve_forever().
152 """
153
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000154 def HandleXmppSocket(fd, socket_map, handler):
155 """Runs the handler for the xmpp connection for fd.
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000156
157 Adapted from asyncore.read() et al.
158 """
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000159 xmpp_connection = socket_map.get(fd)
160 # This could happen if a previous handler call caused fd to get
161 # removed from socket_map.
162 if xmpp_connection is None:
163 return
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000164 try:
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000165 handler(xmpp_connection)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000166 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
167 raise
168 except:
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000169 xmpp_connection.handle_error()
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000170
171 while True:
172 read_fds = [ self.fileno() ]
173 write_fds = []
174 exceptional_fds = []
175
176 for fd, xmpp_connection in self._xmpp_socket_map.items():
177 is_r = xmpp_connection.readable()
178 is_w = xmpp_connection.writable()
179 if is_r:
180 read_fds.append(fd)
181 if is_w:
182 write_fds.append(fd)
183 if is_r or is_w:
184 exceptional_fds.append(fd)
185
186 try:
187 read_fds, write_fds, exceptional_fds = (
188 select.select(read_fds, write_fds, exceptional_fds))
189 except select.error, err:
190 if err.args[0] != errno.EINTR:
191 raise
192 else:
193 continue
194
195 for fd in read_fds:
196 if fd == self.fileno():
197 self.HandleRequestNoBlock()
198 continue
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000199 HandleXmppSocket(fd, self._xmpp_socket_map,
200 asyncore.dispatcher.handle_read_event)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000201
202 for fd in write_fds:
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000203 HandleXmppSocket(fd, self._xmpp_socket_map,
204 asyncore.dispatcher.handle_write_event)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000205
206 for fd in exceptional_fds:
akalin@chromium.org3c0f42f2010-12-20 21:23:44 +0000207 HandleXmppSocket(fd, self._xmpp_socket_map,
208 asyncore.dispatcher.handle_expt_event)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +0000209
akalin@chromium.org154bb132010-11-12 02:20:27 +0000210
211class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler):
212
213 def __init__(self, request, client_address, socket_server,
214 connect_handlers, get_handlers, post_handlers, put_handlers):
215 self._connect_handlers = connect_handlers
216 self._get_handlers = get_handlers
217 self._post_handlers = post_handlers
218 self._put_handlers = put_handlers
219 BaseHTTPServer.BaseHTTPRequestHandler.__init__(
220 self, request, client_address, socket_server)
221
222 def log_request(self, *args, **kwargs):
223 # Disable request logging to declutter test log output.
224 pass
225
226 def _ShouldHandleRequest(self, handler_name):
227 """Determines if the path can be handled by the handler.
228
229 We consider a handler valid if the path begins with the
230 handler name. It can optionally be followed by "?*", "/*".
231 """
232
233 pattern = re.compile('%s($|\?|/).*' % handler_name)
234 return pattern.match(self.path)
235
236 def do_CONNECT(self):
237 for handler in self._connect_handlers:
238 if handler():
239 return
240
241 def do_GET(self):
242 for handler in self._get_handlers:
243 if handler():
244 return
245
246 def do_POST(self):
247 for handler in self._post_handlers:
248 if handler():
249 return
250
251 def do_PUT(self):
252 for handler in self._put_handlers:
253 if handler():
254 return
255
256
257class TestPageHandler(BasePageHandler):
initial.commit94958cf2008-07-26 22:42:52 +0000258
259 def __init__(self, request, client_address, socket_server):
akalin@chromium.org154bb132010-11-12 02:20:27 +0000260 connect_handlers = [
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000261 self.RedirectConnectHandler,
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +0000262 self.ServerAuthConnectHandler,
wtc@chromium.org743d77b2009-02-11 02:48:15 +0000263 self.DefaultConnectResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000264 get_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000265 self.NoCacheMaxAgeTimeHandler,
266 self.NoCacheTimeHandler,
267 self.CacheTimeHandler,
268 self.CacheExpiresHandler,
269 self.CacheProxyRevalidateHandler,
270 self.CachePrivateHandler,
271 self.CachePublicHandler,
272 self.CacheSMaxAgeHandler,
273 self.CacheMustRevalidateHandler,
274 self.CacheMustRevalidateMaxAgeHandler,
275 self.CacheNoStoreHandler,
276 self.CacheNoStoreMaxAgeHandler,
277 self.CacheNoTransformHandler,
278 self.DownloadHandler,
279 self.DownloadFinishHandler,
280 self.EchoHeader,
ananta@chromium.org219b2062009-10-23 16:09:41 +0000281 self.EchoHeaderOverride,
ericroman@google.coma47622b2008-11-15 04:36:51 +0000282 self.EchoAllHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000283 self.FileHandler,
284 self.RealFileWithCommonHeaderHandler,
285 self.RealBZ2FileWithCommonHeaderHandler,
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000286 self.SetCookieHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000287 self.AuthBasicHandler,
288 self.AuthDigestHandler,
289 self.SlowServerHandler,
290 self.ContentTypeHandler,
291 self.ServerRedirectHandler,
292 self.ClientRedirectHandler,
tony@chromium.org03266982010-03-05 03:18:42 +0000293 self.MultipartHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000294 self.DefaultResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000295 post_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000296 self.EchoTitleHandler,
297 self.EchoAllHandler,
mnissler@chromium.org7c939802010-11-11 08:47:14 +0000298 self.EchoHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000299 self.DeviceManagementHandler] + get_handlers
300 put_handlers = [
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000301 self.EchoTitleHandler,
302 self.EchoAllHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000303 self.EchoHandler] + get_handlers
initial.commit94958cf2008-07-26 22:42:52 +0000304
maruel@google.come250a9b2009-03-10 17:39:46 +0000305 self._mime_types = {
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000306 'crx' : 'application/x-chrome-extension',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000307 'exe' : 'application/octet-stream',
maruel@google.come250a9b2009-03-10 17:39:46 +0000308 'gif': 'image/gif',
309 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000310 'jpg' : 'image/jpeg',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000311 'pdf' : 'application/pdf',
312 'xml' : 'text/xml'
maruel@google.come250a9b2009-03-10 17:39:46 +0000313 }
initial.commit94958cf2008-07-26 22:42:52 +0000314 self._default_mime_type = 'text/html'
315
akalin@chromium.org154bb132010-11-12 02:20:27 +0000316 BasePageHandler.__init__(self, request, client_address, socket_server,
317 connect_handlers, get_handlers, post_handlers,
318 put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000319
initial.commit94958cf2008-07-26 22:42:52 +0000320 def GetMIMETypeFromName(self, file_name):
321 """Returns the mime type for the specified file_name. So far it only looks
322 at the file extension."""
323
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000324 (shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000325 if len(extension) == 0:
326 # no extension.
327 return self._default_mime_type
328
ericroman@google.comc17ca532009-05-07 03:51:05 +0000329 # extension starts with a dot, so we need to remove it
330 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000331
initial.commit94958cf2008-07-26 22:42:52 +0000332 def NoCacheMaxAgeTimeHandler(self):
333 """This request handler yields a page with the title set to the current
334 system time, and no caching requested."""
335
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000336 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000337 return False
338
339 self.send_response(200)
340 self.send_header('Cache-Control', 'max-age=0')
341 self.send_header('Content-type', 'text/html')
342 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 NoCacheTimeHandler(self):
350 """This request handler yields a page with the title set to the current
351 system time, and no caching requested."""
352
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000353 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000354 return False
355
356 self.send_response(200)
357 self.send_header('Cache-Control', 'no-cache')
358 self.send_header('Content-type', 'text/html')
359 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 CacheTimeHandler(self):
367 """This request handler yields a page with the title set to the current
368 system time, and allows caching for one minute."""
369
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000370 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000371 return False
372
373 self.send_response(200)
374 self.send_header('Cache-Control', 'max-age=60')
375 self.send_header('Content-type', 'text/html')
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 CacheExpiresHandler(self):
384 """This request handler yields a page with the title set to the current
385 system time, and set the page to expire on 1 Jan 2099."""
386
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000387 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000388 return False
389
390 self.send_response(200)
391 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
392 self.send_header('Content-type', 'text/html')
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 CacheProxyRevalidateHandler(self):
401 """This request handler yields a page with the title set to the current
402 system time, and allows caching for 60 seconds"""
403
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000404 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000405 return False
406
407 self.send_response(200)
408 self.send_header('Content-type', 'text/html')
409 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
410 self.end_headers()
411
maruel@google.come250a9b2009-03-10 17:39:46 +0000412 self.wfile.write('<html><head><title>%s</title></head></html>' %
413 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000414
415 return True
416
417 def CachePrivateHandler(self):
418 """This request handler yields a page with the title set to the current
419 system time, and allows caching for 5 seconds."""
420
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000421 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000422 return False
423
424 self.send_response(200)
425 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000426 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000427 self.end_headers()
428
maruel@google.come250a9b2009-03-10 17:39:46 +0000429 self.wfile.write('<html><head><title>%s</title></head></html>' %
430 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000431
432 return True
433
434 def CachePublicHandler(self):
435 """This request handler yields a page with the title set to the current
436 system time, and allows caching for 5 seconds."""
437
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000438 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000439 return False
440
441 self.send_response(200)
442 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000443 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000444 self.end_headers()
445
maruel@google.come250a9b2009-03-10 17:39:46 +0000446 self.wfile.write('<html><head><title>%s</title></head></html>' %
447 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000448
449 return True
450
451 def CacheSMaxAgeHandler(self):
452 """This request handler yields a page with the title set to the current
453 system time, and does not allow for caching."""
454
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000455 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000456 return False
457
458 self.send_response(200)
459 self.send_header('Content-type', 'text/html')
460 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
461 self.end_headers()
462
maruel@google.come250a9b2009-03-10 17:39:46 +0000463 self.wfile.write('<html><head><title>%s</title></head></html>' %
464 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000465
466 return True
467
468 def CacheMustRevalidateHandler(self):
469 """This request handler yields a page with the title set to the current
470 system time, and does not allow caching."""
471
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000472 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000473 return False
474
475 self.send_response(200)
476 self.send_header('Content-type', 'text/html')
477 self.send_header('Cache-Control', 'must-revalidate')
478 self.end_headers()
479
maruel@google.come250a9b2009-03-10 17:39:46 +0000480 self.wfile.write('<html><head><title>%s</title></head></html>' %
481 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000482
483 return True
484
485 def CacheMustRevalidateMaxAgeHandler(self):
486 """This request handler yields a page with the title set to the current
487 system time, and does not allow caching event though max-age of 60
488 seconds is specified."""
489
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000490 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000491 return False
492
493 self.send_response(200)
494 self.send_header('Content-type', 'text/html')
495 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
496 self.end_headers()
497
maruel@google.come250a9b2009-03-10 17:39:46 +0000498 self.wfile.write('<html><head><title>%s</title></head></html>' %
499 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000500
501 return True
502
initial.commit94958cf2008-07-26 22:42:52 +0000503 def CacheNoStoreHandler(self):
504 """This request handler yields a page with the title set to the current
505 system time, and does not allow the page to be stored."""
506
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000507 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000508 return False
509
510 self.send_response(200)
511 self.send_header('Content-type', 'text/html')
512 self.send_header('Cache-Control', 'no-store')
513 self.end_headers()
514
maruel@google.come250a9b2009-03-10 17:39:46 +0000515 self.wfile.write('<html><head><title>%s</title></head></html>' %
516 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000517
518 return True
519
520 def CacheNoStoreMaxAgeHandler(self):
521 """This request handler yields a page with the title set to the current
522 system time, and does not allow the page to be stored even though max-age
523 of 60 seconds is specified."""
524
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000525 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000526 return False
527
528 self.send_response(200)
529 self.send_header('Content-type', 'text/html')
530 self.send_header('Cache-Control', 'max-age=60, no-store')
531 self.end_headers()
532
maruel@google.come250a9b2009-03-10 17:39:46 +0000533 self.wfile.write('<html><head><title>%s</title></head></html>' %
534 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000535
536 return True
537
538
539 def CacheNoTransformHandler(self):
540 """This request handler yields a page with the title set to the current
541 system time, and does not allow the content to transformed during
542 user-agent caching"""
543
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000544 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000545 return False
546
547 self.send_response(200)
548 self.send_header('Content-type', 'text/html')
549 self.send_header('Cache-Control', 'no-transform')
550 self.end_headers()
551
maruel@google.come250a9b2009-03-10 17:39:46 +0000552 self.wfile.write('<html><head><title>%s</title></head></html>' %
553 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000554
555 return True
556
557 def EchoHeader(self):
558 """This handler echoes back the value of a specific request header."""
ananta@chromium.org219b2062009-10-23 16:09:41 +0000559 """The only difference between this function and the EchoHeaderOverride"""
560 """function is in the parameter being passed to the helper function"""
561 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000562
ananta@chromium.org219b2062009-10-23 16:09:41 +0000563 def EchoHeaderOverride(self):
564 """This handler echoes back the value of a specific request header."""
565 """The UrlRequest unit tests also execute for ChromeFrame which uses"""
566 """IE to issue HTTP requests using the host network stack."""
567 """The Accept and Charset tests which expect the server to echo back"""
568 """the corresponding headers fail here as IE returns cached responses"""
569 """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
570 """treats this request as a new request and does not cache it."""
571 return self.EchoHeaderHelper("/echoheaderoverride")
572
573 def EchoHeaderHelper(self, echo_header):
574 """This function echoes back the value of the request header passed in."""
575 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000576 return False
577
578 query_char = self.path.find('?')
579 if query_char != -1:
580 header_name = self.path[query_char+1:]
581
582 self.send_response(200)
583 self.send_header('Content-type', 'text/plain')
584 self.send_header('Cache-control', 'max-age=60000')
585 # insert a vary header to properly indicate that the cachability of this
586 # request is subject to value of the request header being echoed.
587 if len(header_name) > 0:
588 self.send_header('Vary', header_name)
589 self.end_headers()
590
591 if len(header_name) > 0:
592 self.wfile.write(self.headers.getheader(header_name))
593
594 return True
595
596 def EchoHandler(self):
597 """This handler just echoes back the payload of the request, for testing
598 form submission."""
599
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000600 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000601 return False
602
603 self.send_response(200)
604 self.send_header('Content-type', 'text/html')
605 self.end_headers()
606 length = int(self.headers.getheader('content-length'))
607 request = self.rfile.read(length)
608 self.wfile.write(request)
609 return True
610
611 def EchoTitleHandler(self):
612 """This handler is like Echo, but sets the page title to the request."""
613
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000614 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000615 return False
616
617 self.send_response(200)
618 self.send_header('Content-type', 'text/html')
619 self.end_headers()
620 length = int(self.headers.getheader('content-length'))
621 request = self.rfile.read(length)
622 self.wfile.write('<html><head><title>')
623 self.wfile.write(request)
624 self.wfile.write('</title></head></html>')
625 return True
626
627 def EchoAllHandler(self):
628 """This handler yields a (more) human-readable page listing information
629 about the request header & contents."""
630
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000631 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000632 return False
633
634 self.send_response(200)
635 self.send_header('Content-type', 'text/html')
636 self.end_headers()
637 self.wfile.write('<html><head><style>'
638 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
639 '</style></head><body>'
640 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000641 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000642 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000643
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000644 if self.command == 'POST' or self.command == 'PUT':
ericroman@google.coma47622b2008-11-15 04:36:51 +0000645 length = int(self.headers.getheader('content-length'))
646 qs = self.rfile.read(length)
647 params = cgi.parse_qs(qs, keep_blank_values=1)
648
649 for param in params:
650 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000651
652 self.wfile.write('</pre>')
653
654 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
655
656 self.wfile.write('</body></html>')
657 return True
658
659 def DownloadHandler(self):
660 """This handler sends a downloadable file with or without reporting
661 the size (6K)."""
662
663 if self.path.startswith("/download-unknown-size"):
664 send_length = False
665 elif self.path.startswith("/download-known-size"):
666 send_length = True
667 else:
668 return False
669
670 #
671 # The test which uses this functionality is attempting to send
672 # small chunks of data to the client. Use a fairly large buffer
673 # so that we'll fill chrome's IO buffer enough to force it to
674 # actually write the data.
675 # See also the comments in the client-side of this test in
676 # download_uitest.cc
677 #
678 size_chunk1 = 35*1024
679 size_chunk2 = 10*1024
680
681 self.send_response(200)
682 self.send_header('Content-type', 'application/octet-stream')
683 self.send_header('Cache-Control', 'max-age=0')
684 if send_length:
685 self.send_header('Content-Length', size_chunk1 + size_chunk2)
686 self.end_headers()
687
688 # First chunk of data:
689 self.wfile.write("*" * size_chunk1)
690 self.wfile.flush()
691
692 # handle requests until one of them clears this flag.
693 self.server.waitForDownload = True
694 while self.server.waitForDownload:
695 self.server.handle_request()
696
697 # Second chunk of data:
698 self.wfile.write("*" * size_chunk2)
699 return True
700
701 def DownloadFinishHandler(self):
702 """This handler just tells the server to finish the current download."""
703
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000704 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000705 return False
706
707 self.server.waitForDownload = False
708 self.send_response(200)
709 self.send_header('Content-type', 'text/html')
710 self.send_header('Cache-Control', 'max-age=0')
711 self.end_headers()
712 return True
713
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000714 def _ReplaceFileData(self, data, query_parameters):
715 """Replaces matching substrings in a file.
716
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000717 If the 'replace_text' URL query parameter is present, it is expected to be
718 of the form old_text:new_text, which indicates that any old_text strings in
719 the file are replaced with new_text. Multiple 'replace_text' parameters may
720 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000721
722 If the parameters are not present, |data| is returned.
723 """
724 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000725 replace_text_values = query_dict.get('replace_text', [])
726 for replace_text_value in replace_text_values:
727 replace_text_args = replace_text_value.split(':')
728 if len(replace_text_args) != 2:
729 raise ValueError(
730 'replace_text must be of form old_text:new_text. Actual value: %s' %
731 replace_text_value)
732 old_text_b64, new_text_b64 = replace_text_args
733 old_text = base64.urlsafe_b64decode(old_text_b64)
734 new_text = base64.urlsafe_b64decode(new_text_b64)
735 data = data.replace(old_text, new_text)
736 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000737
initial.commit94958cf2008-07-26 22:42:52 +0000738 def FileHandler(self):
739 """This handler sends the contents of the requested file. Wow, it's like
740 a real webserver!"""
741
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000742 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000743 if not self.path.startswith(prefix):
744 return False
745
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000746 # Consume a request body if present.
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000747 if self.command == 'POST' or self.command == 'PUT' :
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000748 self.rfile.read(int(self.headers.getheader('content-length')))
749
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000750 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
751 sub_path = url_path[len(prefix):]
752 entries = sub_path.split('/')
753 file_path = os.path.join(self.server.data_dir, *entries)
754 if os.path.isdir(file_path):
755 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000756
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000757 if not os.path.isfile(file_path):
758 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000759 self.send_error(404)
760 return True
761
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000762 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000763 data = f.read()
764 f.close()
765
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000766 data = self._ReplaceFileData(data, query)
767
initial.commit94958cf2008-07-26 22:42:52 +0000768 # If file.mock-http-headers exists, it contains the headers we
769 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000770 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000771 if os.path.isfile(headers_path):
772 f = open(headers_path, "r")
773
774 # "HTTP/1.1 200 OK"
775 response = f.readline()
776 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
777 self.send_response(int(status_code))
778
779 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000780 header_values = re.findall('(\S+):\s*(.*)', line)
781 if len(header_values) > 0:
782 # "name: value"
783 name, value = header_values[0]
784 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000785 f.close()
786 else:
787 # Could be more generic once we support mime-type sniffing, but for
788 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000789
790 range = self.headers.get('Range')
791 if range and range.startswith('bytes='):
792 # Note this doesn't handle all valid byte range values (i.e. open ended
793 # ones), just enough for what we needed so far.
794 range = range[6:].split('-')
795 start = int(range[0])
796 end = int(range[1])
797
798 self.send_response(206)
799 content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
800 str(len(data))
801 self.send_header('Content-Range', content_range)
802 data = data[start: end + 1]
803 else:
804 self.send_response(200)
805
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000806 self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000807 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000808 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000809 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000810 self.end_headers()
811
812 self.wfile.write(data)
813
814 return True
815
816 def RealFileWithCommonHeaderHandler(self):
817 """This handler sends the contents of the requested file without the pseudo
818 http head!"""
819
820 prefix='/realfiles/'
821 if not self.path.startswith(prefix):
822 return False
823
824 file = self.path[len(prefix):]
825 path = os.path.join(self.server.data_dir, file)
826
827 try:
828 f = open(path, "rb")
829 data = f.read()
830 f.close()
831
832 # just simply set the MIME as octal stream
833 self.send_response(200)
834 self.send_header('Content-type', 'application/octet-stream')
835 self.end_headers()
836
837 self.wfile.write(data)
838 except:
839 self.send_error(404)
840
841 return True
842
843 def RealBZ2FileWithCommonHeaderHandler(self):
844 """This handler sends the bzip2 contents of the requested file with
845 corresponding Content-Encoding field in http head!"""
846
847 prefix='/realbz2files/'
848 if not self.path.startswith(prefix):
849 return False
850
851 parts = self.path.split('?')
852 file = parts[0][len(prefix):]
853 path = os.path.join(self.server.data_dir, file) + '.bz2'
854
855 if len(parts) > 1:
856 options = parts[1]
857 else:
858 options = ''
859
860 try:
861 self.send_response(200)
862 accept_encoding = self.headers.get("Accept-Encoding")
863 if accept_encoding.find("bzip2") != -1:
864 f = open(path, "rb")
865 data = f.read()
866 f.close()
867 self.send_header('Content-Encoding', 'bzip2')
868 self.send_header('Content-type', 'application/x-bzip2')
869 self.end_headers()
870 if options == 'incremental-header':
871 self.wfile.write(data[:1])
872 self.wfile.flush()
873 time.sleep(1.0)
874 self.wfile.write(data[1:])
875 else:
876 self.wfile.write(data)
877 else:
878 """client do not support bzip2 format, send pseudo content
879 """
880 self.send_header('Content-type', 'text/html; charset=ISO-8859-1')
881 self.end_headers()
882 self.wfile.write("you do not support bzip2 encoding")
883 except:
884 self.send_error(404)
885
886 return True
887
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000888 def SetCookieHandler(self):
889 """This handler just sets a cookie, for testing cookie handling."""
890
891 if not self._ShouldHandleRequest("/set-cookie"):
892 return False
893
894 query_char = self.path.find('?')
895 if query_char != -1:
896 cookie_values = self.path[query_char + 1:].split('&')
897 else:
898 cookie_values = ("",)
899 self.send_response(200)
900 self.send_header('Content-type', 'text/html')
901 for cookie_value in cookie_values:
902 self.send_header('Set-Cookie', '%s' % cookie_value)
903 self.end_headers()
904 for cookie_value in cookie_values:
905 self.wfile.write('%s' % cookie_value)
906 return True
907
initial.commit94958cf2008-07-26 22:42:52 +0000908 def AuthBasicHandler(self):
909 """This handler tests 'Basic' authentication. It just sends a page with
910 title 'user/pass' if you succeed."""
911
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000912 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000913 return False
914
915 username = userpass = password = b64str = ""
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000916 expected_password = 'secret'
917 realm = 'testrealm'
918 set_cookie_if_challenged = False
initial.commit94958cf2008-07-26 22:42:52 +0000919
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000920 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
921 query_params = cgi.parse_qs(query, True)
922 if 'set-cookie-if-challenged' in query_params:
923 set_cookie_if_challenged = True
924 if 'password' in query_params:
925 expected_password = query_params['password'][0]
926 if 'realm' in query_params:
927 realm = query_params['realm'][0]
ericroman@google.com239b4d82009-03-27 04:00:22 +0000928
initial.commit94958cf2008-07-26 22:42:52 +0000929 auth = self.headers.getheader('authorization')
930 try:
931 if not auth:
932 raise Exception('no auth')
933 b64str = re.findall(r'Basic (\S+)', auth)[0]
934 userpass = base64.b64decode(b64str)
935 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000936 if password != expected_password:
initial.commit94958cf2008-07-26 22:42:52 +0000937 raise Exception('wrong password')
938 except Exception, e:
939 # Authentication failed.
940 self.send_response(401)
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000941 self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
initial.commit94958cf2008-07-26 22:42:52 +0000942 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000943 if set_cookie_if_challenged:
944 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000945 self.end_headers()
946 self.wfile.write('<html><head>')
947 self.wfile.write('<title>Denied: %s</title>' % e)
948 self.wfile.write('</head><body>')
949 self.wfile.write('auth=%s<p>' % auth)
950 self.wfile.write('b64str=%s<p>' % b64str)
951 self.wfile.write('username: %s<p>' % username)
952 self.wfile.write('userpass: %s<p>' % userpass)
953 self.wfile.write('password: %s<p>' % password)
954 self.wfile.write('You sent:<br>%s<p>' % self.headers)
955 self.wfile.write('</body></html>')
956 return True
957
958 # Authentication successful. (Return a cachable response to allow for
959 # testing cached pages that require authentication.)
960 if_none_match = self.headers.getheader('if-none-match')
961 if if_none_match == "abc":
962 self.send_response(304)
963 self.end_headers()
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000964 elif url_path.endswith(".gif"):
965 # Using chrome/test/data/google/logo.gif as the test image
966 test_image_path = ['google', 'logo.gif']
967 gif_path = os.path.join(self.server.data_dir, *test_image_path)
968 if not os.path.isfile(gif_path):
969 self.send_error(404)
970 return True
971
972 f = open(gif_path, "rb")
973 data = f.read()
974 f.close()
975
976 self.send_response(200)
977 self.send_header('Content-type', 'image/gif')
978 self.send_header('Cache-control', 'max-age=60000')
979 self.send_header('Etag', 'abc')
980 self.end_headers()
981 self.wfile.write(data)
initial.commit94958cf2008-07-26 22:42:52 +0000982 else:
983 self.send_response(200)
984 self.send_header('Content-type', 'text/html')
985 self.send_header('Cache-control', 'max-age=60000')
986 self.send_header('Etag', 'abc')
987 self.end_headers()
988 self.wfile.write('<html><head>')
989 self.wfile.write('<title>%s/%s</title>' % (username, password))
990 self.wfile.write('</head><body>')
991 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000992 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000993 self.wfile.write('</body></html>')
994
995 return True
996
tonyg@chromium.org75054202010-03-31 22:06:10 +0000997 def GetNonce(self, force_reset=False):
998 """Returns a nonce that's stable per request path for the server's lifetime.
initial.commit94958cf2008-07-26 22:42:52 +0000999
tonyg@chromium.org75054202010-03-31 22:06:10 +00001000 This is a fake implementation. A real implementation would only use a given
1001 nonce a single time (hence the name n-once). However, for the purposes of
1002 unittesting, we don't care about the security of the nonce.
1003
1004 Args:
1005 force_reset: Iff set, the nonce will be changed. Useful for testing the
1006 "stale" response.
1007 """
1008 if force_reset or not self.server.nonce_time:
1009 self.server.nonce_time = time.time()
1010 return _new_md5('privatekey%s%d' %
1011 (self.path, self.server.nonce_time)).hexdigest()
1012
1013 def AuthDigestHandler(self):
1014 """This handler tests 'Digest' authentication.
1015
1016 It just sends a page with title 'user/pass' if you succeed.
1017
1018 A stale response is sent iff "stale" is present in the request path.
1019 """
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001020 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +00001021 return False
1022
tonyg@chromium.org75054202010-03-31 22:06:10 +00001023 stale = 'stale' in self.path
1024 nonce = self.GetNonce(force_reset=stale)
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001025 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001026 password = 'secret'
1027 realm = 'testrealm'
1028
1029 auth = self.headers.getheader('authorization')
1030 pairs = {}
1031 try:
1032 if not auth:
1033 raise Exception('no auth')
1034 if not auth.startswith('Digest'):
1035 raise Exception('not digest')
1036 # Pull out all the name="value" pairs as a dictionary.
1037 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1038
1039 # Make sure it's all valid.
1040 if pairs['nonce'] != nonce:
1041 raise Exception('wrong nonce')
1042 if pairs['opaque'] != opaque:
1043 raise Exception('wrong opaque')
1044
1045 # Check the 'response' value and make sure it matches our magic hash.
1046 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +00001047 hash_a1 = _new_md5(
1048 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001049 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001050 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001051 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +00001052 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1053 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +00001054 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +00001055
1056 if pairs['response'] != response:
1057 raise Exception('wrong password')
1058 except Exception, e:
1059 # Authentication failed.
1060 self.send_response(401)
1061 hdr = ('Digest '
1062 'realm="%s", '
1063 'domain="/", '
1064 'qop="auth", '
1065 'algorithm=MD5, '
1066 'nonce="%s", '
1067 'opaque="%s"') % (realm, nonce, opaque)
1068 if stale:
1069 hdr += ', stale="TRUE"'
1070 self.send_header('WWW-Authenticate', hdr)
1071 self.send_header('Content-type', 'text/html')
1072 self.end_headers()
1073 self.wfile.write('<html><head>')
1074 self.wfile.write('<title>Denied: %s</title>' % e)
1075 self.wfile.write('</head><body>')
1076 self.wfile.write('auth=%s<p>' % auth)
1077 self.wfile.write('pairs=%s<p>' % pairs)
1078 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1079 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1080 self.wfile.write('</body></html>')
1081 return True
1082
1083 # Authentication successful.
1084 self.send_response(200)
1085 self.send_header('Content-type', 'text/html')
1086 self.end_headers()
1087 self.wfile.write('<html><head>')
1088 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1089 self.wfile.write('</head><body>')
1090 self.wfile.write('auth=%s<p>' % auth)
1091 self.wfile.write('pairs=%s<p>' % pairs)
1092 self.wfile.write('</body></html>')
1093
1094 return True
1095
1096 def SlowServerHandler(self):
1097 """Wait for the user suggested time before responding. The syntax is
1098 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001099 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +00001100 return False
1101 query_char = self.path.find('?')
1102 wait_sec = 1.0
1103 if query_char >= 0:
1104 try:
1105 wait_sec = int(self.path[query_char + 1:])
1106 except ValueError:
1107 pass
1108 time.sleep(wait_sec)
1109 self.send_response(200)
1110 self.send_header('Content-type', 'text/plain')
1111 self.end_headers()
1112 self.wfile.write("waited %d seconds" % wait_sec)
1113 return True
1114
1115 def ContentTypeHandler(self):
1116 """Returns a string of html with the given content type. E.g.,
1117 /contenttype?text/css returns an html file with the Content-Type
1118 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001119 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001120 return False
1121 query_char = self.path.find('?')
1122 content_type = self.path[query_char + 1:].strip()
1123 if not content_type:
1124 content_type = 'text/html'
1125 self.send_response(200)
1126 self.send_header('Content-Type', content_type)
1127 self.end_headers()
1128 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1129 return True
1130
1131 def ServerRedirectHandler(self):
1132 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001133 '/server-redirect?http://foo.bar/asdf' to redirect to
1134 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001135
1136 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001137 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001138 return False
1139
1140 query_char = self.path.find('?')
1141 if query_char < 0 or len(self.path) <= query_char + 1:
1142 self.sendRedirectHelp(test_name)
1143 return True
1144 dest = self.path[query_char + 1:]
1145
1146 self.send_response(301) # moved permanently
1147 self.send_header('Location', dest)
1148 self.send_header('Content-type', 'text/html')
1149 self.end_headers()
1150 self.wfile.write('<html><head>')
1151 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1152
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001153 return True
initial.commit94958cf2008-07-26 22:42:52 +00001154
1155 def ClientRedirectHandler(self):
1156 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001157 '/client-redirect?http://foo.bar/asdf' to redirect to
1158 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001159
1160 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001161 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001162 return False
1163
1164 query_char = self.path.find('?');
1165 if query_char < 0 or len(self.path) <= query_char + 1:
1166 self.sendRedirectHelp(test_name)
1167 return True
1168 dest = self.path[query_char + 1:]
1169
1170 self.send_response(200)
1171 self.send_header('Content-type', 'text/html')
1172 self.end_headers()
1173 self.wfile.write('<html><head>')
1174 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1175 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1176
1177 return True
1178
tony@chromium.org03266982010-03-05 03:18:42 +00001179 def MultipartHandler(self):
1180 """Send a multipart response (10 text/html pages)."""
1181 test_name = "/multipart"
1182 if not self._ShouldHandleRequest(test_name):
1183 return False
1184
1185 num_frames = 10
1186 bound = '12345'
1187 self.send_response(200)
1188 self.send_header('Content-type',
1189 'multipart/x-mixed-replace;boundary=' + bound)
1190 self.end_headers()
1191
1192 for i in xrange(num_frames):
1193 self.wfile.write('--' + bound + '\r\n')
1194 self.wfile.write('Content-type: text/html\r\n\r\n')
1195 self.wfile.write('<title>page ' + str(i) + '</title>')
1196 self.wfile.write('page ' + str(i))
1197
1198 self.wfile.write('--' + bound + '--')
1199 return True
1200
initial.commit94958cf2008-07-26 22:42:52 +00001201 def DefaultResponseHandler(self):
1202 """This is the catch-all response handler for requests that aren't handled
1203 by one of the special handlers above.
1204 Note that we specify the content-length as without it the https connection
1205 is not closed properly (and the browser keeps expecting data)."""
1206
1207 contents = "Default response given for path: " + self.path
1208 self.send_response(200)
1209 self.send_header('Content-type', 'text/html')
1210 self.send_header("Content-Length", len(contents))
1211 self.end_headers()
1212 self.wfile.write(contents)
1213 return True
1214
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001215 def RedirectConnectHandler(self):
1216 """Sends a redirect to the CONNECT request for www.redirect.com. This
1217 response is not specified by the RFC, so the browser should not follow
1218 the redirect."""
1219
1220 if (self.path.find("www.redirect.com") < 0):
1221 return False
1222
1223 dest = "http://www.destination.com/foo.js"
1224
1225 self.send_response(302) # moved temporarily
1226 self.send_header('Location', dest)
1227 self.send_header('Connection', 'close')
1228 self.end_headers()
1229 return True
1230
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001231 def ServerAuthConnectHandler(self):
1232 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1233 response doesn't make sense because the proxy server cannot request
1234 server authentication."""
1235
1236 if (self.path.find("www.server-auth.com") < 0):
1237 return False
1238
1239 challenge = 'Basic realm="WallyWorld"'
1240
1241 self.send_response(401) # unauthorized
1242 self.send_header('WWW-Authenticate', challenge)
1243 self.send_header('Connection', 'close')
1244 self.end_headers()
1245 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001246
1247 def DefaultConnectResponseHandler(self):
1248 """This is the catch-all response handler for CONNECT requests that aren't
1249 handled by one of the special handlers above. Real Web servers respond
1250 with 400 to CONNECT requests."""
1251
1252 contents = "Your client has issued a malformed or illegal request."
1253 self.send_response(400) # bad request
1254 self.send_header('Content-type', 'text/html')
1255 self.send_header("Content-Length", len(contents))
1256 self.end_headers()
1257 self.wfile.write(contents)
1258 return True
1259
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001260 def DeviceManagementHandler(self):
1261 """Delegates to the device management service used for cloud policy."""
1262 if not self._ShouldHandleRequest("/device_management"):
1263 return False
1264
1265 length = int(self.headers.getheader('content-length'))
1266 raw_request = self.rfile.read(length)
1267
1268 if not self.server._device_management_handler:
1269 import device_management
1270 policy_path = os.path.join(self.server.data_dir, 'device_management')
1271 self.server._device_management_handler = (
1272 device_management.TestServer(policy_path))
1273
1274 http_response, raw_reply = (
1275 self.server._device_management_handler.HandleRequest(self.path,
1276 self.headers,
1277 raw_request))
1278 self.send_response(http_response)
1279 self.end_headers()
1280 self.wfile.write(raw_reply)
1281 return True
1282
initial.commit94958cf2008-07-26 22:42:52 +00001283 # called by the redirect handling function when there is no parameter
1284 def sendRedirectHelp(self, redirect_name):
1285 self.send_response(200)
1286 self.send_header('Content-type', 'text/html')
1287 self.end_headers()
1288 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1289 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1290 self.wfile.write('</body></html>')
1291
akalin@chromium.org154bb132010-11-12 02:20:27 +00001292
1293class SyncPageHandler(BasePageHandler):
1294 """Handler for the main HTTP sync server."""
1295
1296 def __init__(self, request, client_address, sync_http_server):
1297 get_handlers = [self.ChromiumSyncTimeHandler]
1298 post_handlers = [self.ChromiumSyncCommandHandler]
1299 BasePageHandler.__init__(self, request, client_address,
1300 sync_http_server, [], get_handlers,
1301 post_handlers, [])
1302
1303 def ChromiumSyncTimeHandler(self):
1304 """Handle Chromium sync .../time requests.
1305
1306 The syncer sometimes checks server reachability by examining /time.
1307 """
1308 test_name = "/chromiumsync/time"
1309 if not self._ShouldHandleRequest(test_name):
1310 return False
1311
1312 self.send_response(200)
1313 self.send_header('Content-type', 'text/html')
1314 self.end_headers()
1315 return True
1316
1317 def ChromiumSyncCommandHandler(self):
1318 """Handle a chromiumsync command arriving via http.
1319
1320 This covers all sync protocol commands: authentication, getupdates, and
1321 commit.
1322 """
1323 test_name = "/chromiumsync/command"
1324 if not self._ShouldHandleRequest(test_name):
1325 return False
1326
1327 length = int(self.headers.getheader('content-length'))
1328 raw_request = self.rfile.read(length)
1329
1330 http_response, raw_reply = self.server.HandleCommand(
1331 self.path, raw_request)
1332 self.send_response(http_response)
1333 self.end_headers()
1334 self.wfile.write(raw_reply)
1335 return True
1336
1337
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001338def MakeDataDir():
1339 if options.data_dir:
1340 if not os.path.isdir(options.data_dir):
1341 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1342 return None
1343 my_data_dir = options.data_dir
1344 else:
1345 # Create the default path to our data dir, relative to the exe dir.
1346 my_data_dir = os.path.dirname(sys.argv[0])
1347 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00001348 "test", "data")
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001349
1350 #TODO(ibrar): Must use Find* funtion defined in google\tools
1351 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1352
1353 return my_data_dir
1354
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001355class FileMultiplexer:
1356 def __init__(self, fd1, fd2) :
1357 self.__fd1 = fd1
1358 self.__fd2 = fd2
1359
1360 def __del__(self) :
1361 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1362 self.__fd1.close()
1363 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1364 self.__fd2.close()
1365
1366 def write(self, text) :
1367 self.__fd1.write(text)
1368 self.__fd2.write(text)
1369
1370 def flush(self) :
1371 self.__fd1.flush()
1372 self.__fd2.flush()
1373
initial.commit94958cf2008-07-26 22:42:52 +00001374def main(options, args):
initial.commit94958cf2008-07-26 22:42:52 +00001375 logfile = open('testserver.log', 'w')
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001376 sys.stdout = FileMultiplexer(sys.stdout, logfile)
1377 sys.stderr = FileMultiplexer(sys.stderr, logfile)
initial.commit94958cf2008-07-26 22:42:52 +00001378
1379 port = options.port
1380
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001381 server_data = {}
1382
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001383 if options.server_type == SERVER_HTTP:
1384 if options.cert:
1385 # let's make sure the cert file exists.
1386 if not os.path.isfile(options.cert):
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001387 print 'specified server cert file not found: ' + options.cert + \
1388 ' exiting...'
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001389 return
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001390 for ca_cert in options.ssl_client_ca:
1391 if not os.path.isfile(ca_cert):
1392 print 'specified trusted client CA file not found: ' + ca_cert + \
1393 ' exiting...'
1394 return
davidben@chromium.org31282a12010-08-07 01:10:02 +00001395 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001396 options.ssl_client_auth, options.ssl_client_ca,
1397 options.ssl_bulk_cipher)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001398 print 'HTTPS server started on port %d...' % server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001399 else:
phajdan.jr@chromium.orgfa49b5f2010-07-23 20:38:56 +00001400 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001401 print 'HTTP server started on port %d...' % server.server_port
erikkay@google.com70397b62008-12-30 21:49:21 +00001402
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001403 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001404 server.file_root_url = options.file_root_url
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001405 server_data['port'] = server.server_port
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001406 server._device_management_handler = None
akalin@chromium.org154bb132010-11-12 02:20:27 +00001407 elif options.server_type == SERVER_SYNC:
1408 server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1409 print 'Sync HTTP server started on port %d...' % server.server_port
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001410 print 'Sync XMPP server started on port %d...' % server.xmpp_port
1411 server_data['port'] = server.server_port
1412 server_data['xmpp_port'] = server.xmpp_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001413 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001414 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001415 my_data_dir = MakeDataDir()
1416
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001417 # Instantiate a dummy authorizer for managing 'virtual' users
1418 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1419
1420 # Define a new user having full r/w permissions and a read-only
1421 # anonymous user
1422 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1423
1424 authorizer.add_anonymous(my_data_dir)
1425
1426 # Instantiate FTP handler class
1427 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1428 ftp_handler.authorizer = authorizer
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001429
1430 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001431 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1432 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001433
1434 # Instantiate FTP server class and listen to 127.0.0.1:port
1435 address = ('127.0.0.1', port)
1436 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001437 server_data['port'] = server.socket.getsockname()[1]
1438 print 'FTP server started on port %d...' % server_data['port']
initial.commit94958cf2008-07-26 22:42:52 +00001439
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001440 # Notify the parent that we've started. (BaseServer subclasses
1441 # bind their sockets on construction.)
1442 if options.startup_pipe is not None:
akalin@chromium.org73b7b5c2010-11-26 19:42:26 +00001443 server_data_json = simplejson.dumps(server_data)
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001444 server_data_len = len(server_data_json)
1445 print 'sending server_data: %s (%d bytes)' % (
1446 server_data_json, server_data_len)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001447 if sys.platform == 'win32':
1448 fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1449 else:
1450 fd = options.startup_pipe
1451 startup_pipe = os.fdopen(fd, "w")
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001452 # First write the data length as an unsigned 4-byte value. This
1453 # is _not_ using network byte ordering since the other end of the
1454 # pipe is on the same machine.
1455 startup_pipe.write(struct.pack('=L', server_data_len))
1456 startup_pipe.write(server_data_json)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001457 startup_pipe.close()
1458
initial.commit94958cf2008-07-26 22:42:52 +00001459 try:
1460 server.serve_forever()
1461 except KeyboardInterrupt:
1462 print 'shutting down server'
1463 server.stop = True
1464
1465if __name__ == '__main__':
1466 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001467 option_parser.add_option("-f", '--ftp', action='store_const',
1468 const=SERVER_FTP, default=SERVER_HTTP,
1469 dest='server_type',
akalin@chromium.org154bb132010-11-12 02:20:27 +00001470 help='start up an FTP server.')
1471 option_parser.add_option('', '--sync', action='store_const',
1472 const=SERVER_SYNC, default=SERVER_HTTP,
1473 dest='server_type',
1474 help='start up a sync server.')
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001475 option_parser.add_option('', '--port', default='0', type='int',
1476 help='Port used by the server. If unspecified, the '
1477 'server will listen on an ephemeral port.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001478 option_parser.add_option('', '--data-dir', dest='data_dir',
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001479 help='Directory from which to read the files.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001480 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001481 help='Specify that https should be used, specify '
1482 'the path to the cert containing the private key '
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001483 'the server should use.')
davidben@chromium.org31282a12010-08-07 01:10:02 +00001484 option_parser.add_option('', '--ssl-client-auth', action='store_true',
1485 help='Require SSL client auth on every connection.')
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001486 option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1487 help='Specify that the client certificate request '
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001488 'should include the CA named in the subject of '
1489 'the DER-encoded certificate contained in the '
1490 'specified file. This option may appear multiple '
1491 'times, indicating multiple CA names should be '
1492 'sent in the request.')
1493 option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1494 help='Specify the bulk encryption algorithm(s)'
1495 'that will be accepted by the SSL server. Valid '
1496 'values are "aes256", "aes128", "3des", "rc4". If '
1497 'omitted, all algorithms will be used. This '
1498 'option may appear multiple times, indicating '
1499 'multiple algorithms should be enabled.');
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001500 option_parser.add_option('', '--file-root-url', default='/files/',
1501 help='Specify a root URL for files served.')
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001502 option_parser.add_option('', '--startup-pipe', type='int',
1503 dest='startup_pipe',
1504 help='File handle of pipe to parent process')
initial.commit94958cf2008-07-26 22:42:52 +00001505 options, args = option_parser.parse_args()
1506
1507 sys.exit(main(options, args))