blob: a22255802ce53738c4c78d19f99a0c8e24297e70 [file] [log] [blame]
initial.commit94958cf2008-07-26 22:42:52 +00001#!/usr/bin/python2.4
gfeher@chromium.orge0bddc12011-01-28 18:15:24 +00002# Copyright (c) 2011 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,
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000284 self.SetCookieHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000285 self.AuthBasicHandler,
286 self.AuthDigestHandler,
287 self.SlowServerHandler,
288 self.ContentTypeHandler,
289 self.ServerRedirectHandler,
290 self.ClientRedirectHandler,
tony@chromium.org03266982010-03-05 03:18:42 +0000291 self.MultipartHandler,
initial.commit94958cf2008-07-26 22:42:52 +0000292 self.DefaultResponseHandler]
akalin@chromium.org154bb132010-11-12 02:20:27 +0000293 post_handlers = [
initial.commit94958cf2008-07-26 22:42:52 +0000294 self.EchoTitleHandler,
295 self.EchoAllHandler,
mnissler@chromium.org7c939802010-11-11 08:47:14 +0000296 self.EchoHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000297 self.DeviceManagementHandler] + get_handlers
298 put_handlers = [
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000299 self.EchoTitleHandler,
300 self.EchoAllHandler,
akalin@chromium.org154bb132010-11-12 02:20:27 +0000301 self.EchoHandler] + get_handlers
initial.commit94958cf2008-07-26 22:42:52 +0000302
maruel@google.come250a9b2009-03-10 17:39:46 +0000303 self._mime_types = {
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000304 'crx' : 'application/x-chrome-extension',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000305 'exe' : 'application/octet-stream',
maruel@google.come250a9b2009-03-10 17:39:46 +0000306 'gif': 'image/gif',
307 'jpeg' : 'image/jpeg',
finnur@chromium.org88e84c32009-10-02 17:59:55 +0000308 'jpg' : 'image/jpeg',
lzheng@chromium.org02f09022010-12-16 20:24:35 +0000309 'pdf' : 'application/pdf',
310 'xml' : 'text/xml'
maruel@google.come250a9b2009-03-10 17:39:46 +0000311 }
initial.commit94958cf2008-07-26 22:42:52 +0000312 self._default_mime_type = 'text/html'
313
akalin@chromium.org154bb132010-11-12 02:20:27 +0000314 BasePageHandler.__init__(self, request, client_address, socket_server,
315 connect_handlers, get_handlers, post_handlers,
316 put_handlers)
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000317
initial.commit94958cf2008-07-26 22:42:52 +0000318 def GetMIMETypeFromName(self, file_name):
319 """Returns the mime type for the specified file_name. So far it only looks
320 at the file extension."""
321
rafaelw@chromium.orga4e76f82010-09-09 17:33:18 +0000322 (shortname, extension) = os.path.splitext(file_name.split("?")[0])
initial.commit94958cf2008-07-26 22:42:52 +0000323 if len(extension) == 0:
324 # no extension.
325 return self._default_mime_type
326
ericroman@google.comc17ca532009-05-07 03:51:05 +0000327 # extension starts with a dot, so we need to remove it
328 return self._mime_types.get(extension[1:], self._default_mime_type)
initial.commit94958cf2008-07-26 22:42:52 +0000329
initial.commit94958cf2008-07-26 22:42:52 +0000330 def NoCacheMaxAgeTimeHandler(self):
331 """This request handler yields a page with the title set to the current
332 system time, and no caching requested."""
333
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000334 if not self._ShouldHandleRequest("/nocachetime/maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000335 return False
336
337 self.send_response(200)
338 self.send_header('Cache-Control', 'max-age=0')
339 self.send_header('Content-type', 'text/html')
340 self.end_headers()
341
maruel@google.come250a9b2009-03-10 17:39:46 +0000342 self.wfile.write('<html><head><title>%s</title></head></html>' %
343 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000344
345 return True
346
347 def NoCacheTimeHandler(self):
348 """This request handler yields a page with the title set to the current
349 system time, and no caching requested."""
350
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000351 if not self._ShouldHandleRequest("/nocachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000352 return False
353
354 self.send_response(200)
355 self.send_header('Cache-Control', 'no-cache')
356 self.send_header('Content-type', 'text/html')
357 self.end_headers()
358
maruel@google.come250a9b2009-03-10 17:39:46 +0000359 self.wfile.write('<html><head><title>%s</title></head></html>' %
360 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000361
362 return True
363
364 def CacheTimeHandler(self):
365 """This request handler yields a page with the title set to the current
366 system time, and allows caching for one minute."""
367
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000368 if not self._ShouldHandleRequest("/cachetime"):
initial.commit94958cf2008-07-26 22:42:52 +0000369 return False
370
371 self.send_response(200)
372 self.send_header('Cache-Control', 'max-age=60')
373 self.send_header('Content-type', 'text/html')
374 self.end_headers()
375
maruel@google.come250a9b2009-03-10 17:39:46 +0000376 self.wfile.write('<html><head><title>%s</title></head></html>' %
377 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000378
379 return True
380
381 def CacheExpiresHandler(self):
382 """This request handler yields a page with the title set to the current
383 system time, and set the page to expire on 1 Jan 2099."""
384
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000385 if not self._ShouldHandleRequest("/cache/expires"):
initial.commit94958cf2008-07-26 22:42:52 +0000386 return False
387
388 self.send_response(200)
389 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
390 self.send_header('Content-type', 'text/html')
391 self.end_headers()
392
maruel@google.come250a9b2009-03-10 17:39:46 +0000393 self.wfile.write('<html><head><title>%s</title></head></html>' %
394 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000395
396 return True
397
398 def CacheProxyRevalidateHandler(self):
399 """This request handler yields a page with the title set to the current
400 system time, and allows caching for 60 seconds"""
401
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000402 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000403 return False
404
405 self.send_response(200)
406 self.send_header('Content-type', 'text/html')
407 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
408 self.end_headers()
409
maruel@google.come250a9b2009-03-10 17:39:46 +0000410 self.wfile.write('<html><head><title>%s</title></head></html>' %
411 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000412
413 return True
414
415 def CachePrivateHandler(self):
416 """This request handler yields a page with the title set to the current
417 system time, and allows caching for 5 seconds."""
418
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000419 if not self._ShouldHandleRequest("/cache/private"):
initial.commit94958cf2008-07-26 22:42:52 +0000420 return False
421
422 self.send_response(200)
423 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000424 self.send_header('Cache-Control', 'max-age=3, private')
initial.commit94958cf2008-07-26 22:42:52 +0000425 self.end_headers()
426
maruel@google.come250a9b2009-03-10 17:39:46 +0000427 self.wfile.write('<html><head><title>%s</title></head></html>' %
428 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000429
430 return True
431
432 def CachePublicHandler(self):
433 """This request handler yields a page with the title set to the current
434 system time, and allows caching for 5 seconds."""
435
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000436 if not self._ShouldHandleRequest("/cache/public"):
initial.commit94958cf2008-07-26 22:42:52 +0000437 return False
438
439 self.send_response(200)
440 self.send_header('Content-type', 'text/html')
huanr@chromium.orgab5be752009-05-23 02:58:44 +0000441 self.send_header('Cache-Control', 'max-age=3, public')
initial.commit94958cf2008-07-26 22:42:52 +0000442 self.end_headers()
443
maruel@google.come250a9b2009-03-10 17:39:46 +0000444 self.wfile.write('<html><head><title>%s</title></head></html>' %
445 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000446
447 return True
448
449 def CacheSMaxAgeHandler(self):
450 """This request handler yields a page with the title set to the current
451 system time, and does not allow for caching."""
452
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000453 if not self._ShouldHandleRequest("/cache/s-maxage"):
initial.commit94958cf2008-07-26 22:42:52 +0000454 return False
455
456 self.send_response(200)
457 self.send_header('Content-type', 'text/html')
458 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
459 self.end_headers()
460
maruel@google.come250a9b2009-03-10 17:39:46 +0000461 self.wfile.write('<html><head><title>%s</title></head></html>' %
462 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000463
464 return True
465
466 def CacheMustRevalidateHandler(self):
467 """This request handler yields a page with the title set to the current
468 system time, and does not allow caching."""
469
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000470 if not self._ShouldHandleRequest("/cache/must-revalidate"):
initial.commit94958cf2008-07-26 22:42:52 +0000471 return False
472
473 self.send_response(200)
474 self.send_header('Content-type', 'text/html')
475 self.send_header('Cache-Control', 'must-revalidate')
476 self.end_headers()
477
maruel@google.come250a9b2009-03-10 17:39:46 +0000478 self.wfile.write('<html><head><title>%s</title></head></html>' %
479 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000480
481 return True
482
483 def CacheMustRevalidateMaxAgeHandler(self):
484 """This request handler yields a page with the title set to the current
485 system time, and does not allow caching event though max-age of 60
486 seconds is specified."""
487
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000488 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000489 return False
490
491 self.send_response(200)
492 self.send_header('Content-type', 'text/html')
493 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
494 self.end_headers()
495
maruel@google.come250a9b2009-03-10 17:39:46 +0000496 self.wfile.write('<html><head><title>%s</title></head></html>' %
497 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000498
499 return True
500
initial.commit94958cf2008-07-26 22:42:52 +0000501 def CacheNoStoreHandler(self):
502 """This request handler yields a page with the title set to the current
503 system time, and does not allow the page to be stored."""
504
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000505 if not self._ShouldHandleRequest("/cache/no-store"):
initial.commit94958cf2008-07-26 22:42:52 +0000506 return False
507
508 self.send_response(200)
509 self.send_header('Content-type', 'text/html')
510 self.send_header('Cache-Control', 'no-store')
511 self.end_headers()
512
maruel@google.come250a9b2009-03-10 17:39:46 +0000513 self.wfile.write('<html><head><title>%s</title></head></html>' %
514 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000515
516 return True
517
518 def CacheNoStoreMaxAgeHandler(self):
519 """This request handler yields a page with the title set to the current
520 system time, and does not allow the page to be stored even though max-age
521 of 60 seconds is specified."""
522
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000523 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
initial.commit94958cf2008-07-26 22:42:52 +0000524 return False
525
526 self.send_response(200)
527 self.send_header('Content-type', 'text/html')
528 self.send_header('Cache-Control', 'max-age=60, no-store')
529 self.end_headers()
530
maruel@google.come250a9b2009-03-10 17:39:46 +0000531 self.wfile.write('<html><head><title>%s</title></head></html>' %
532 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000533
534 return True
535
536
537 def CacheNoTransformHandler(self):
538 """This request handler yields a page with the title set to the current
539 system time, and does not allow the content to transformed during
540 user-agent caching"""
541
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000542 if not self._ShouldHandleRequest("/cache/no-transform"):
initial.commit94958cf2008-07-26 22:42:52 +0000543 return False
544
545 self.send_response(200)
546 self.send_header('Content-type', 'text/html')
547 self.send_header('Cache-Control', 'no-transform')
548 self.end_headers()
549
maruel@google.come250a9b2009-03-10 17:39:46 +0000550 self.wfile.write('<html><head><title>%s</title></head></html>' %
551 time.time())
initial.commit94958cf2008-07-26 22:42:52 +0000552
553 return True
554
555 def EchoHeader(self):
556 """This handler echoes back the value of a specific request header."""
ananta@chromium.org219b2062009-10-23 16:09:41 +0000557 """The only difference between this function and the EchoHeaderOverride"""
558 """function is in the parameter being passed to the helper function"""
559 return self.EchoHeaderHelper("/echoheader")
initial.commit94958cf2008-07-26 22:42:52 +0000560
ananta@chromium.org219b2062009-10-23 16:09:41 +0000561 def EchoHeaderOverride(self):
562 """This handler echoes back the value of a specific request header."""
563 """The UrlRequest unit tests also execute for ChromeFrame which uses"""
564 """IE to issue HTTP requests using the host network stack."""
565 """The Accept and Charset tests which expect the server to echo back"""
566 """the corresponding headers fail here as IE returns cached responses"""
567 """The EchoHeaderOverride parameter is an easy way to ensure that IE"""
568 """treats this request as a new request and does not cache it."""
569 return self.EchoHeaderHelper("/echoheaderoverride")
570
571 def EchoHeaderHelper(self, echo_header):
572 """This function echoes back the value of the request header passed in."""
573 if not self._ShouldHandleRequest(echo_header):
initial.commit94958cf2008-07-26 22:42:52 +0000574 return False
575
576 query_char = self.path.find('?')
577 if query_char != -1:
578 header_name = self.path[query_char+1:]
579
580 self.send_response(200)
581 self.send_header('Content-type', 'text/plain')
582 self.send_header('Cache-control', 'max-age=60000')
583 # insert a vary header to properly indicate that the cachability of this
584 # request is subject to value of the request header being echoed.
585 if len(header_name) > 0:
586 self.send_header('Vary', header_name)
587 self.end_headers()
588
589 if len(header_name) > 0:
590 self.wfile.write(self.headers.getheader(header_name))
591
592 return True
593
satish@chromium.orgce0b1d02011-01-25 07:17:11 +0000594 def ReadRequestBody(self):
595 """This function reads the body of the current HTTP request, handling
596 both plain and chunked transfer encoded requests."""
597
598 if self.headers.getheader('transfer-encoding') != 'chunked':
599 length = int(self.headers.getheader('content-length'))
600 return self.rfile.read(length)
601
602 # Read the request body as chunks.
603 body = ""
604 while True:
605 line = self.rfile.readline()
606 length = int(line, 16)
607 if length == 0:
608 self.rfile.readline()
609 break
610 body += self.rfile.read(length)
611 self.rfile.read(2)
612 return body
613
initial.commit94958cf2008-07-26 22:42:52 +0000614 def EchoHandler(self):
615 """This handler just echoes back the payload of the request, for testing
616 form submission."""
617
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000618 if not self._ShouldHandleRequest("/echo"):
initial.commit94958cf2008-07-26 22:42:52 +0000619 return False
620
621 self.send_response(200)
622 self.send_header('Content-type', 'text/html')
623 self.end_headers()
satish@chromium.orgce0b1d02011-01-25 07:17:11 +0000624 self.wfile.write(self.ReadRequestBody())
initial.commit94958cf2008-07-26 22:42:52 +0000625 return True
626
627 def EchoTitleHandler(self):
628 """This handler is like Echo, but sets the page title to the request."""
629
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000630 if not self._ShouldHandleRequest("/echotitle"):
initial.commit94958cf2008-07-26 22:42:52 +0000631 return False
632
633 self.send_response(200)
634 self.send_header('Content-type', 'text/html')
635 self.end_headers()
satish@chromium.orgce0b1d02011-01-25 07:17:11 +0000636 request = self.ReadRequestBody()
initial.commit94958cf2008-07-26 22:42:52 +0000637 self.wfile.write('<html><head><title>')
638 self.wfile.write(request)
639 self.wfile.write('</title></head></html>')
640 return True
641
642 def EchoAllHandler(self):
643 """This handler yields a (more) human-readable page listing information
644 about the request header & contents."""
645
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000646 if not self._ShouldHandleRequest("/echoall"):
initial.commit94958cf2008-07-26 22:42:52 +0000647 return False
648
649 self.send_response(200)
650 self.send_header('Content-type', 'text/html')
651 self.end_headers()
652 self.wfile.write('<html><head><style>'
653 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
654 '</style></head><body>'
655 '<div style="float: right">'
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +0000656 '<a href="/echo">back to referring page</a></div>'
initial.commit94958cf2008-07-26 22:42:52 +0000657 '<h1>Request Body:</h1><pre>')
initial.commit94958cf2008-07-26 22:42:52 +0000658
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000659 if self.command == 'POST' or self.command == 'PUT':
satish@chromium.orgce0b1d02011-01-25 07:17:11 +0000660 qs = self.ReadRequestBody()
ericroman@google.coma47622b2008-11-15 04:36:51 +0000661 params = cgi.parse_qs(qs, keep_blank_values=1)
662
663 for param in params:
664 self.wfile.write('%s=%s\n' % (param, params[param][0]))
initial.commit94958cf2008-07-26 22:42:52 +0000665
666 self.wfile.write('</pre>')
667
668 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
669
670 self.wfile.write('</body></html>')
671 return True
672
673 def DownloadHandler(self):
674 """This handler sends a downloadable file with or without reporting
675 the size (6K)."""
676
677 if self.path.startswith("/download-unknown-size"):
678 send_length = False
679 elif self.path.startswith("/download-known-size"):
680 send_length = True
681 else:
682 return False
683
684 #
685 # The test which uses this functionality is attempting to send
686 # small chunks of data to the client. Use a fairly large buffer
687 # so that we'll fill chrome's IO buffer enough to force it to
688 # actually write the data.
689 # See also the comments in the client-side of this test in
690 # download_uitest.cc
691 #
692 size_chunk1 = 35*1024
693 size_chunk2 = 10*1024
694
695 self.send_response(200)
696 self.send_header('Content-type', 'application/octet-stream')
697 self.send_header('Cache-Control', 'max-age=0')
698 if send_length:
699 self.send_header('Content-Length', size_chunk1 + size_chunk2)
700 self.end_headers()
701
702 # First chunk of data:
703 self.wfile.write("*" * size_chunk1)
704 self.wfile.flush()
705
706 # handle requests until one of them clears this flag.
707 self.server.waitForDownload = True
708 while self.server.waitForDownload:
709 self.server.handle_request()
710
711 # Second chunk of data:
712 self.wfile.write("*" * size_chunk2)
713 return True
714
715 def DownloadFinishHandler(self):
716 """This handler just tells the server to finish the current download."""
717
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000718 if not self._ShouldHandleRequest("/download-finish"):
initial.commit94958cf2008-07-26 22:42:52 +0000719 return False
720
721 self.server.waitForDownload = False
722 self.send_response(200)
723 self.send_header('Content-type', 'text/html')
724 self.send_header('Cache-Control', 'max-age=0')
725 self.end_headers()
726 return True
727
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000728 def _ReplaceFileData(self, data, query_parameters):
729 """Replaces matching substrings in a file.
730
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000731 If the 'replace_text' URL query parameter is present, it is expected to be
732 of the form old_text:new_text, which indicates that any old_text strings in
733 the file are replaced with new_text. Multiple 'replace_text' parameters may
734 be specified.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000735
736 If the parameters are not present, |data| is returned.
737 """
738 query_dict = cgi.parse_qs(query_parameters)
cbentzel@chromium.org099a3db2010-11-11 18:16:58 +0000739 replace_text_values = query_dict.get('replace_text', [])
740 for replace_text_value in replace_text_values:
741 replace_text_args = replace_text_value.split(':')
742 if len(replace_text_args) != 2:
743 raise ValueError(
744 'replace_text must be of form old_text:new_text. Actual value: %s' %
745 replace_text_value)
746 old_text_b64, new_text_b64 = replace_text_args
747 old_text = base64.urlsafe_b64decode(old_text_b64)
748 new_text = base64.urlsafe_b64decode(new_text_b64)
749 data = data.replace(old_text, new_text)
750 return data
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000751
initial.commit94958cf2008-07-26 22:42:52 +0000752 def FileHandler(self):
753 """This handler sends the contents of the requested file. Wow, it's like
754 a real webserver!"""
755
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +0000756 prefix = self.server.file_root_url
initial.commit94958cf2008-07-26 22:42:52 +0000757 if not self.path.startswith(prefix):
758 return False
759
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000760 # Consume a request body if present.
ananta@chromium.org56d146f2010-01-11 19:03:01 +0000761 if self.command == 'POST' or self.command == 'PUT' :
satish@chromium.orgce0b1d02011-01-25 07:17:11 +0000762 self.ReadRequestBody()
darin@chromium.orgc25e5702009-07-23 19:10:23 +0000763
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000764 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
765 sub_path = url_path[len(prefix):]
766 entries = sub_path.split('/')
767 file_path = os.path.join(self.server.data_dir, *entries)
768 if os.path.isdir(file_path):
769 file_path = os.path.join(file_path, 'index.html')
initial.commit94958cf2008-07-26 22:42:52 +0000770
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000771 if not os.path.isfile(file_path):
772 print "File not found " + sub_path + " full path:" + file_path
initial.commit94958cf2008-07-26 22:42:52 +0000773 self.send_error(404)
774 return True
775
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000776 f = open(file_path, "rb")
initial.commit94958cf2008-07-26 22:42:52 +0000777 data = f.read()
778 f.close()
779
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000780 data = self._ReplaceFileData(data, query)
781
initial.commit94958cf2008-07-26 22:42:52 +0000782 # If file.mock-http-headers exists, it contains the headers we
783 # should send. Read them in and parse them.
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000784 headers_path = file_path + '.mock-http-headers'
initial.commit94958cf2008-07-26 22:42:52 +0000785 if os.path.isfile(headers_path):
786 f = open(headers_path, "r")
787
788 # "HTTP/1.1 200 OK"
789 response = f.readline()
790 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0]
791 self.send_response(int(status_code))
792
793 for line in f:
robertshield@chromium.org5e231612010-01-20 18:23:53 +0000794 header_values = re.findall('(\S+):\s*(.*)', line)
795 if len(header_values) > 0:
796 # "name: value"
797 name, value = header_values[0]
798 self.send_header(name, value)
initial.commit94958cf2008-07-26 22:42:52 +0000799 f.close()
800 else:
801 # Could be more generic once we support mime-type sniffing, but for
802 # now we need to set it explicitly.
jam@chromium.org41550782010-11-17 23:47:50 +0000803
804 range = self.headers.get('Range')
805 if range and range.startswith('bytes='):
806 # Note this doesn't handle all valid byte range values (i.e. open ended
807 # ones), just enough for what we needed so far.
808 range = range[6:].split('-')
809 start = int(range[0])
810 end = int(range[1])
811
812 self.send_response(206)
813 content_range = 'bytes ' + str(start) + '-' + str(end) + '/' + \
814 str(len(data))
815 self.send_header('Content-Range', content_range)
816 data = data[start: end + 1]
817 else:
818 self.send_response(200)
819
cbentzel@chromium.orge30b32d2010-11-06 17:33:56 +0000820 self.send_header('Content-type', self.GetMIMETypeFromName(file_path))
jam@chromium.org41550782010-11-17 23:47:50 +0000821 self.send_header('Accept-Ranges', 'bytes')
initial.commit94958cf2008-07-26 22:42:52 +0000822 self.send_header('Content-Length', len(data))
jam@chromium.org41550782010-11-17 23:47:50 +0000823 self.send_header('ETag', '\'' + file_path + '\'')
initial.commit94958cf2008-07-26 22:42:52 +0000824 self.end_headers()
825
826 self.wfile.write(data)
827
828 return True
829
levin@chromium.orgf7ee2e42009-08-26 02:33:46 +0000830 def SetCookieHandler(self):
831 """This handler just sets a cookie, for testing cookie handling."""
832
833 if not self._ShouldHandleRequest("/set-cookie"):
834 return False
835
836 query_char = self.path.find('?')
837 if query_char != -1:
838 cookie_values = self.path[query_char + 1:].split('&')
839 else:
840 cookie_values = ("",)
841 self.send_response(200)
842 self.send_header('Content-type', 'text/html')
843 for cookie_value in cookie_values:
844 self.send_header('Set-Cookie', '%s' % cookie_value)
845 self.end_headers()
846 for cookie_value in cookie_values:
847 self.wfile.write('%s' % cookie_value)
848 return True
849
initial.commit94958cf2008-07-26 22:42:52 +0000850 def AuthBasicHandler(self):
851 """This handler tests 'Basic' authentication. It just sends a page with
852 title 'user/pass' if you succeed."""
853
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000854 if not self._ShouldHandleRequest("/auth-basic"):
initial.commit94958cf2008-07-26 22:42:52 +0000855 return False
856
857 username = userpass = password = b64str = ""
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000858 expected_password = 'secret'
859 realm = 'testrealm'
860 set_cookie_if_challenged = False
initial.commit94958cf2008-07-26 22:42:52 +0000861
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000862 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
863 query_params = cgi.parse_qs(query, True)
864 if 'set-cookie-if-challenged' in query_params:
865 set_cookie_if_challenged = True
866 if 'password' in query_params:
867 expected_password = query_params['password'][0]
868 if 'realm' in query_params:
869 realm = query_params['realm'][0]
ericroman@google.com239b4d82009-03-27 04:00:22 +0000870
initial.commit94958cf2008-07-26 22:42:52 +0000871 auth = self.headers.getheader('authorization')
872 try:
873 if not auth:
874 raise Exception('no auth')
875 b64str = re.findall(r'Basic (\S+)', auth)[0]
876 userpass = base64.b64decode(b64str)
877 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000878 if password != expected_password:
initial.commit94958cf2008-07-26 22:42:52 +0000879 raise Exception('wrong password')
880 except Exception, e:
881 # Authentication failed.
882 self.send_response(401)
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000883 self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
initial.commit94958cf2008-07-26 22:42:52 +0000884 self.send_header('Content-type', 'text/html')
ericroman@google.com239b4d82009-03-27 04:00:22 +0000885 if set_cookie_if_challenged:
886 self.send_header('Set-Cookie', 'got_challenged=true')
initial.commit94958cf2008-07-26 22:42:52 +0000887 self.end_headers()
888 self.wfile.write('<html><head>')
889 self.wfile.write('<title>Denied: %s</title>' % e)
890 self.wfile.write('</head><body>')
891 self.wfile.write('auth=%s<p>' % auth)
892 self.wfile.write('b64str=%s<p>' % b64str)
893 self.wfile.write('username: %s<p>' % username)
894 self.wfile.write('userpass: %s<p>' % userpass)
895 self.wfile.write('password: %s<p>' % password)
896 self.wfile.write('You sent:<br>%s<p>' % self.headers)
897 self.wfile.write('</body></html>')
898 return True
899
900 # Authentication successful. (Return a cachable response to allow for
901 # testing cached pages that require authentication.)
902 if_none_match = self.headers.getheader('if-none-match')
903 if if_none_match == "abc":
904 self.send_response(304)
905 self.end_headers()
cbentzel@chromium.org5a808d22011-01-05 15:51:24 +0000906 elif url_path.endswith(".gif"):
907 # Using chrome/test/data/google/logo.gif as the test image
908 test_image_path = ['google', 'logo.gif']
909 gif_path = os.path.join(self.server.data_dir, *test_image_path)
910 if not os.path.isfile(gif_path):
911 self.send_error(404)
912 return True
913
914 f = open(gif_path, "rb")
915 data = f.read()
916 f.close()
917
918 self.send_response(200)
919 self.send_header('Content-type', 'image/gif')
920 self.send_header('Cache-control', 'max-age=60000')
921 self.send_header('Etag', 'abc')
922 self.end_headers()
923 self.wfile.write(data)
initial.commit94958cf2008-07-26 22:42:52 +0000924 else:
925 self.send_response(200)
926 self.send_header('Content-type', 'text/html')
927 self.send_header('Cache-control', 'max-age=60000')
928 self.send_header('Etag', 'abc')
929 self.end_headers()
930 self.wfile.write('<html><head>')
931 self.wfile.write('<title>%s/%s</title>' % (username, password))
932 self.wfile.write('</head><body>')
933 self.wfile.write('auth=%s<p>' % auth)
ericroman@google.com239b4d82009-03-27 04:00:22 +0000934 self.wfile.write('You sent:<br>%s<p>' % self.headers)
initial.commit94958cf2008-07-26 22:42:52 +0000935 self.wfile.write('</body></html>')
936
937 return True
938
tonyg@chromium.org75054202010-03-31 22:06:10 +0000939 def GetNonce(self, force_reset=False):
940 """Returns a nonce that's stable per request path for the server's lifetime.
initial.commit94958cf2008-07-26 22:42:52 +0000941
tonyg@chromium.org75054202010-03-31 22:06:10 +0000942 This is a fake implementation. A real implementation would only use a given
943 nonce a single time (hence the name n-once). However, for the purposes of
944 unittesting, we don't care about the security of the nonce.
945
946 Args:
947 force_reset: Iff set, the nonce will be changed. Useful for testing the
948 "stale" response.
949 """
950 if force_reset or not self.server.nonce_time:
951 self.server.nonce_time = time.time()
952 return _new_md5('privatekey%s%d' %
953 (self.path, self.server.nonce_time)).hexdigest()
954
955 def AuthDigestHandler(self):
956 """This handler tests 'Digest' authentication.
957
958 It just sends a page with title 'user/pass' if you succeed.
959
960 A stale response is sent iff "stale" is present in the request path.
961 """
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +0000962 if not self._ShouldHandleRequest("/auth-digest"):
initial.commit94958cf2008-07-26 22:42:52 +0000963 return False
964
tonyg@chromium.org75054202010-03-31 22:06:10 +0000965 stale = 'stale' in self.path
966 nonce = self.GetNonce(force_reset=stale)
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000967 opaque = _new_md5('opaque').hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000968 password = 'secret'
969 realm = 'testrealm'
970
971 auth = self.headers.getheader('authorization')
972 pairs = {}
973 try:
974 if not auth:
975 raise Exception('no auth')
976 if not auth.startswith('Digest'):
977 raise Exception('not digest')
978 # Pull out all the name="value" pairs as a dictionary.
979 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
980
981 # Make sure it's all valid.
982 if pairs['nonce'] != nonce:
983 raise Exception('wrong nonce')
984 if pairs['opaque'] != opaque:
985 raise Exception('wrong opaque')
986
987 # Check the 'response' value and make sure it matches our magic hash.
988 # See http://www.ietf.org/rfc/rfc2617.txt
maruel@google.come250a9b2009-03-10 17:39:46 +0000989 hash_a1 = _new_md5(
990 ':'.join([pairs['username'], realm, password])).hexdigest()
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000991 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000992 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000993 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'],
initial.commit94958cf2008-07-26 22:42:52 +0000994 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
995 else:
thomasvl@chromium.org595be8d2009-03-06 19:59:26 +0000996 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
initial.commit94958cf2008-07-26 22:42:52 +0000997
998 if pairs['response'] != response:
999 raise Exception('wrong password')
1000 except Exception, e:
1001 # Authentication failed.
1002 self.send_response(401)
1003 hdr = ('Digest '
1004 'realm="%s", '
1005 'domain="/", '
1006 'qop="auth", '
1007 'algorithm=MD5, '
1008 'nonce="%s", '
1009 'opaque="%s"') % (realm, nonce, opaque)
1010 if stale:
1011 hdr += ', stale="TRUE"'
1012 self.send_header('WWW-Authenticate', hdr)
1013 self.send_header('Content-type', 'text/html')
1014 self.end_headers()
1015 self.wfile.write('<html><head>')
1016 self.wfile.write('<title>Denied: %s</title>' % e)
1017 self.wfile.write('</head><body>')
1018 self.wfile.write('auth=%s<p>' % auth)
1019 self.wfile.write('pairs=%s<p>' % pairs)
1020 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1021 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1022 self.wfile.write('</body></html>')
1023 return True
1024
1025 # Authentication successful.
1026 self.send_response(200)
1027 self.send_header('Content-type', 'text/html')
1028 self.end_headers()
1029 self.wfile.write('<html><head>')
1030 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1031 self.wfile.write('</head><body>')
1032 self.wfile.write('auth=%s<p>' % auth)
1033 self.wfile.write('pairs=%s<p>' % pairs)
1034 self.wfile.write('</body></html>')
1035
1036 return True
1037
1038 def SlowServerHandler(self):
1039 """Wait for the user suggested time before responding. The syntax is
1040 /slow?0.5 to wait for half a second."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001041 if not self._ShouldHandleRequest("/slow"):
initial.commit94958cf2008-07-26 22:42:52 +00001042 return False
1043 query_char = self.path.find('?')
1044 wait_sec = 1.0
1045 if query_char >= 0:
1046 try:
1047 wait_sec = int(self.path[query_char + 1:])
1048 except ValueError:
1049 pass
1050 time.sleep(wait_sec)
1051 self.send_response(200)
1052 self.send_header('Content-type', 'text/plain')
1053 self.end_headers()
1054 self.wfile.write("waited %d seconds" % wait_sec)
1055 return True
1056
1057 def ContentTypeHandler(self):
1058 """Returns a string of html with the given content type. E.g.,
1059 /contenttype?text/css returns an html file with the Content-Type
1060 header set to text/css."""
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001061 if not self._ShouldHandleRequest("/contenttype"):
initial.commit94958cf2008-07-26 22:42:52 +00001062 return False
1063 query_char = self.path.find('?')
1064 content_type = self.path[query_char + 1:].strip()
1065 if not content_type:
1066 content_type = 'text/html'
1067 self.send_response(200)
1068 self.send_header('Content-Type', content_type)
1069 self.end_headers()
1070 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n");
1071 return True
1072
1073 def ServerRedirectHandler(self):
1074 """Sends a server redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001075 '/server-redirect?http://foo.bar/asdf' to redirect to
1076 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001077
1078 test_name = "/server-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001079 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001080 return False
1081
1082 query_char = self.path.find('?')
1083 if query_char < 0 or len(self.path) <= query_char + 1:
1084 self.sendRedirectHelp(test_name)
1085 return True
1086 dest = self.path[query_char + 1:]
1087
1088 self.send_response(301) # moved permanently
1089 self.send_header('Location', dest)
1090 self.send_header('Content-type', 'text/html')
1091 self.end_headers()
1092 self.wfile.write('<html><head>')
1093 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1094
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001095 return True
initial.commit94958cf2008-07-26 22:42:52 +00001096
1097 def ClientRedirectHandler(self):
1098 """Sends a client redirect to the given URL. The syntax is
maruel@google.come250a9b2009-03-10 17:39:46 +00001099 '/client-redirect?http://foo.bar/asdf' to redirect to
1100 'http://foo.bar/asdf'"""
initial.commit94958cf2008-07-26 22:42:52 +00001101
1102 test_name = "/client-redirect"
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001103 if not self._ShouldHandleRequest(test_name):
initial.commit94958cf2008-07-26 22:42:52 +00001104 return False
1105
1106 query_char = self.path.find('?');
1107 if query_char < 0 or len(self.path) <= query_char + 1:
1108 self.sendRedirectHelp(test_name)
1109 return True
1110 dest = self.path[query_char + 1:]
1111
1112 self.send_response(200)
1113 self.send_header('Content-type', 'text/html')
1114 self.end_headers()
1115 self.wfile.write('<html><head>')
1116 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1117 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1118
1119 return True
1120
tony@chromium.org03266982010-03-05 03:18:42 +00001121 def MultipartHandler(self):
1122 """Send a multipart response (10 text/html pages)."""
1123 test_name = "/multipart"
1124 if not self._ShouldHandleRequest(test_name):
1125 return False
1126
1127 num_frames = 10
1128 bound = '12345'
1129 self.send_response(200)
1130 self.send_header('Content-type',
1131 'multipart/x-mixed-replace;boundary=' + bound)
1132 self.end_headers()
1133
1134 for i in xrange(num_frames):
1135 self.wfile.write('--' + bound + '\r\n')
1136 self.wfile.write('Content-type: text/html\r\n\r\n')
1137 self.wfile.write('<title>page ' + str(i) + '</title>')
1138 self.wfile.write('page ' + str(i))
1139
1140 self.wfile.write('--' + bound + '--')
1141 return True
1142
initial.commit94958cf2008-07-26 22:42:52 +00001143 def DefaultResponseHandler(self):
1144 """This is the catch-all response handler for requests that aren't handled
1145 by one of the special handlers above.
1146 Note that we specify the content-length as without it the https connection
1147 is not closed properly (and the browser keeps expecting data)."""
1148
1149 contents = "Default response given for path: " + self.path
1150 self.send_response(200)
1151 self.send_header('Content-type', 'text/html')
1152 self.send_header("Content-Length", len(contents))
1153 self.end_headers()
1154 self.wfile.write(contents)
1155 return True
1156
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001157 def RedirectConnectHandler(self):
1158 """Sends a redirect to the CONNECT request for www.redirect.com. This
1159 response is not specified by the RFC, so the browser should not follow
1160 the redirect."""
1161
1162 if (self.path.find("www.redirect.com") < 0):
1163 return False
1164
1165 dest = "http://www.destination.com/foo.js"
1166
1167 self.send_response(302) # moved temporarily
1168 self.send_header('Location', dest)
1169 self.send_header('Connection', 'close')
1170 self.end_headers()
1171 return True
1172
wtc@chromium.orgb86c7f92009-02-14 01:45:08 +00001173 def ServerAuthConnectHandler(self):
1174 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1175 response doesn't make sense because the proxy server cannot request
1176 server authentication."""
1177
1178 if (self.path.find("www.server-auth.com") < 0):
1179 return False
1180
1181 challenge = 'Basic realm="WallyWorld"'
1182
1183 self.send_response(401) # unauthorized
1184 self.send_header('WWW-Authenticate', challenge)
1185 self.send_header('Connection', 'close')
1186 self.end_headers()
1187 return True
wtc@chromium.org743d77b2009-02-11 02:48:15 +00001188
1189 def DefaultConnectResponseHandler(self):
1190 """This is the catch-all response handler for CONNECT requests that aren't
1191 handled by one of the special handlers above. Real Web servers respond
1192 with 400 to CONNECT requests."""
1193
1194 contents = "Your client has issued a malformed or illegal request."
1195 self.send_response(400) # bad request
1196 self.send_header('Content-type', 'text/html')
1197 self.send_header("Content-Length", len(contents))
1198 self.end_headers()
1199 self.wfile.write(contents)
1200 return True
1201
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001202 def DeviceManagementHandler(self):
1203 """Delegates to the device management service used for cloud policy."""
1204 if not self._ShouldHandleRequest("/device_management"):
1205 return False
1206
satish@chromium.orgce0b1d02011-01-25 07:17:11 +00001207 raw_request = self.ReadRequestBody()
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001208
1209 if not self.server._device_management_handler:
1210 import device_management
1211 policy_path = os.path.join(self.server.data_dir, 'device_management')
1212 self.server._device_management_handler = (
gfeher@chromium.orge0bddc12011-01-28 18:15:24 +00001213 device_management.TestServer(policy_path,
1214 self.server.policy_cert_chain))
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001215
1216 http_response, raw_reply = (
1217 self.server._device_management_handler.HandleRequest(self.path,
1218 self.headers,
1219 raw_request))
1220 self.send_response(http_response)
1221 self.end_headers()
1222 self.wfile.write(raw_reply)
1223 return True
1224
initial.commit94958cf2008-07-26 22:42:52 +00001225 # called by the redirect handling function when there is no parameter
1226 def sendRedirectHelp(self, redirect_name):
1227 self.send_response(200)
1228 self.send_header('Content-type', 'text/html')
1229 self.end_headers()
1230 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1231 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1232 self.wfile.write('</body></html>')
1233
akalin@chromium.org154bb132010-11-12 02:20:27 +00001234
1235class SyncPageHandler(BasePageHandler):
1236 """Handler for the main HTTP sync server."""
1237
1238 def __init__(self, request, client_address, sync_http_server):
1239 get_handlers = [self.ChromiumSyncTimeHandler]
1240 post_handlers = [self.ChromiumSyncCommandHandler]
1241 BasePageHandler.__init__(self, request, client_address,
1242 sync_http_server, [], get_handlers,
1243 post_handlers, [])
1244
1245 def ChromiumSyncTimeHandler(self):
1246 """Handle Chromium sync .../time requests.
1247
1248 The syncer sometimes checks server reachability by examining /time.
1249 """
1250 test_name = "/chromiumsync/time"
1251 if not self._ShouldHandleRequest(test_name):
1252 return False
1253
1254 self.send_response(200)
1255 self.send_header('Content-type', 'text/html')
1256 self.end_headers()
1257 return True
1258
1259 def ChromiumSyncCommandHandler(self):
1260 """Handle a chromiumsync command arriving via http.
1261
1262 This covers all sync protocol commands: authentication, getupdates, and
1263 commit.
1264 """
1265 test_name = "/chromiumsync/command"
1266 if not self._ShouldHandleRequest(test_name):
1267 return False
1268
satish@chromium.orgc5228b12011-01-25 12:13:19 +00001269 length = int(self.headers.getheader('content-length'))
1270 raw_request = self.rfile.read(length)
akalin@chromium.org154bb132010-11-12 02:20:27 +00001271
1272 http_response, raw_reply = self.server.HandleCommand(
1273 self.path, raw_request)
1274 self.send_response(http_response)
1275 self.end_headers()
1276 self.wfile.write(raw_reply)
1277 return True
1278
1279
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001280def MakeDataDir():
1281 if options.data_dir:
1282 if not os.path.isdir(options.data_dir):
1283 print 'specified data dir not found: ' + options.data_dir + ' exiting...'
1284 return None
1285 my_data_dir = options.data_dir
1286 else:
1287 # Create the default path to our data dir, relative to the exe dir.
1288 my_data_dir = os.path.dirname(sys.argv[0])
1289 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..",
timurrrr@chromium.orgb9006f52010-04-30 14:50:58 +00001290 "test", "data")
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001291
1292 #TODO(ibrar): Must use Find* funtion defined in google\tools
1293 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1294
1295 return my_data_dir
1296
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001297class FileMultiplexer:
1298 def __init__(self, fd1, fd2) :
1299 self.__fd1 = fd1
1300 self.__fd2 = fd2
1301
1302 def __del__(self) :
1303 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr:
1304 self.__fd1.close()
1305 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr:
1306 self.__fd2.close()
1307
1308 def write(self, text) :
1309 self.__fd1.write(text)
1310 self.__fd2.write(text)
1311
1312 def flush(self) :
1313 self.__fd1.flush()
1314 self.__fd2.flush()
1315
initial.commit94958cf2008-07-26 22:42:52 +00001316def main(options, args):
initial.commit94958cf2008-07-26 22:42:52 +00001317 logfile = open('testserver.log', 'w')
phajdan.jr@chromium.orgbf74e2b2010-08-17 20:07:11 +00001318 sys.stderr = FileMultiplexer(sys.stderr, logfile)
rsimha@chromium.orge77acad2011-02-01 19:56:46 +00001319 if options.log_to_console:
1320 sys.stdout = FileMultiplexer(sys.stdout, logfile)
1321 else:
1322 sys.stdout = logfile
initial.commit94958cf2008-07-26 22:42:52 +00001323
1324 port = options.port
1325
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001326 server_data = {}
1327
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001328 if options.server_type == SERVER_HTTP:
1329 if options.cert:
1330 # let's make sure the cert file exists.
1331 if not os.path.isfile(options.cert):
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001332 print 'specified server cert file not found: ' + options.cert + \
1333 ' exiting...'
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001334 return
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001335 for ca_cert in options.ssl_client_ca:
1336 if not os.path.isfile(ca_cert):
1337 print 'specified trusted client CA file not found: ' + ca_cert + \
1338 ' exiting...'
1339 return
davidben@chromium.org31282a12010-08-07 01:10:02 +00001340 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert,
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001341 options.ssl_client_auth, options.ssl_client_ca,
1342 options.ssl_bulk_cipher)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001343 print 'HTTPS server started on port %d...' % server.server_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001344 else:
phajdan.jr@chromium.orgfa49b5f2010-07-23 20:38:56 +00001345 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler)
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001346 print 'HTTP server started on port %d...' % server.server_port
erikkay@google.com70397b62008-12-30 21:49:21 +00001347
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001348 server.data_dir = MakeDataDir()
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001349 server.file_root_url = options.file_root_url
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001350 server_data['port'] = server.server_port
mnissler@chromium.org7c939802010-11-11 08:47:14 +00001351 server._device_management_handler = None
gfeher@chromium.orge0bddc12011-01-28 18:15:24 +00001352 server.policy_cert_chain = options.policy_cert_chain
akalin@chromium.org154bb132010-11-12 02:20:27 +00001353 elif options.server_type == SERVER_SYNC:
1354 server = SyncHTTPServer(('127.0.0.1', port), SyncPageHandler)
1355 print 'Sync HTTP server started on port %d...' % server.server_port
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001356 print 'Sync XMPP server started on port %d...' % server.xmpp_port
1357 server_data['port'] = server.server_port
1358 server_data['xmpp_port'] = server.xmpp_port
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001359 # means FTP Server
nsylvain@chromium.org8d5763b2008-12-30 23:44:27 +00001360 else:
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001361 my_data_dir = MakeDataDir()
1362
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001363 # Instantiate a dummy authorizer for managing 'virtual' users
1364 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1365
1366 # Define a new user having full r/w permissions and a read-only
1367 # anonymous user
1368 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1369
1370 authorizer.add_anonymous(my_data_dir)
1371
1372 # Instantiate FTP handler class
1373 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1374 ftp_handler.authorizer = authorizer
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001375
1376 # Define a customized banner (string returned when client connects)
maruel@google.come250a9b2009-03-10 17:39:46 +00001377 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1378 pyftpdlib.ftpserver.__ver__)
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001379
1380 # Instantiate FTP server class and listen to 127.0.0.1:port
1381 address = ('127.0.0.1', port)
1382 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler)
akalin@chromium.org4e4f3c92010-11-27 04:04:52 +00001383 server_data['port'] = server.socket.getsockname()[1]
1384 print 'FTP server started on port %d...' % server_data['port']
initial.commit94958cf2008-07-26 22:42:52 +00001385
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001386 # Notify the parent that we've started. (BaseServer subclasses
1387 # bind their sockets on construction.)
1388 if options.startup_pipe is not None:
akalin@chromium.org73b7b5c2010-11-26 19:42:26 +00001389 server_data_json = simplejson.dumps(server_data)
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001390 server_data_len = len(server_data_json)
1391 print 'sending server_data: %s (%d bytes)' % (
1392 server_data_json, server_data_len)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001393 if sys.platform == 'win32':
1394 fd = msvcrt.open_osfhandle(options.startup_pipe, 0)
1395 else:
1396 fd = options.startup_pipe
1397 startup_pipe = os.fdopen(fd, "w")
akalin@chromium.orgf8479c62010-11-27 01:52:52 +00001398 # First write the data length as an unsigned 4-byte value. This
1399 # is _not_ using network byte ordering since the other end of the
1400 # pipe is on the same machine.
1401 startup_pipe.write(struct.pack('=L', server_data_len))
1402 startup_pipe.write(server_data_json)
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001403 startup_pipe.close()
1404
initial.commit94958cf2008-07-26 22:42:52 +00001405 try:
1406 server.serve_forever()
1407 except KeyboardInterrupt:
1408 print 'shutting down server'
1409 server.stop = True
1410
1411if __name__ == '__main__':
1412 option_parser = optparse.OptionParser()
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001413 option_parser.add_option("-f", '--ftp', action='store_const',
1414 const=SERVER_FTP, default=SERVER_HTTP,
1415 dest='server_type',
akalin@chromium.org154bb132010-11-12 02:20:27 +00001416 help='start up an FTP server.')
1417 option_parser.add_option('', '--sync', action='store_const',
1418 const=SERVER_SYNC, default=SERVER_HTTP,
1419 dest='server_type',
1420 help='start up a sync server.')
rsimha@chromium.orge77acad2011-02-01 19:56:46 +00001421 option_parser.add_option('', '--log-to-console', action='store_const',
1422 const=True, default=False,
1423 dest='log_to_console',
1424 help='Enables or disables sys.stdout logging to '
1425 'the console.')
cbentzel@chromium.org0787bc72010-11-11 20:31:31 +00001426 option_parser.add_option('', '--port', default='0', type='int',
1427 help='Port used by the server. If unspecified, the '
1428 'server will listen on an ephemeral port.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001429 option_parser.add_option('', '--data-dir', dest='data_dir',
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001430 help='Directory from which to read the files.')
erikkay@google.comd5182ff2009-01-08 20:45:27 +00001431 option_parser.add_option('', '--https', dest='cert',
initial.commit94958cf2008-07-26 22:42:52 +00001432 help='Specify that https should be used, specify '
1433 'the path to the cert containing the private key '
robertshield@chromium.org5e231612010-01-20 18:23:53 +00001434 'the server should use.')
davidben@chromium.org31282a12010-08-07 01:10:02 +00001435 option_parser.add_option('', '--ssl-client-auth', action='store_true',
1436 help='Require SSL client auth on every connection.')
rsleevi@chromium.orgb2ecdab2010-08-21 04:02:44 +00001437 option_parser.add_option('', '--ssl-client-ca', action='append', default=[],
1438 help='Specify that the client certificate request '
rsleevi@chromium.org2124c812010-10-28 11:57:36 +00001439 'should include the CA named in the subject of '
1440 'the DER-encoded certificate contained in the '
1441 'specified file. This option may appear multiple '
1442 'times, indicating multiple CA names should be '
1443 'sent in the request.')
1444 option_parser.add_option('', '--ssl-bulk-cipher', action='append',
1445 help='Specify the bulk encryption algorithm(s)'
1446 'that will be accepted by the SSL server. Valid '
1447 'values are "aes256", "aes128", "3des", "rc4". If '
1448 'omitted, all algorithms will be used. This '
1449 'option may appear multiple times, indicating '
1450 'multiple algorithms should be enabled.');
ben@chromium.org0c7ac3a2009-04-10 02:37:22 +00001451 option_parser.add_option('', '--file-root-url', default='/files/',
1452 help='Specify a root URL for files served.')
davidben@chromium.org06fcf202010-09-22 18:15:23 +00001453 option_parser.add_option('', '--startup-pipe', type='int',
1454 dest='startup_pipe',
1455 help='File handle of pipe to parent process')
gfeher@chromium.orge0bddc12011-01-28 18:15:24 +00001456 option_parser.add_option('', '--policy-cert-chain', action='append',
1457 help='Specify a path to a certificate file to sign '
1458 'policy responses. This option may be used '
1459 'multiple times to define a certificate chain. '
1460 'The first element will be used for signing, '
1461 'the last element should be the root '
1462 'certificate.')
initial.commit94958cf2008-07-26 22:42:52 +00001463 options, args = option_parser.parse_args()
1464
1465 sys.exit(main(options, args))