Updating trunk VERSION from 2139.0 to 2140.0
[chromium-blink-merge.git] / net / tools / testserver / testserver.py
blob9babbb24eb43bc51c234a510a0cfdad93bdda8c0
1 #!/usr/bin/env python
2 # Copyright 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """This is a simple HTTP/FTP/TCP/UDP/BASIC_AUTH_PROXY/WEBSOCKET server used for
7 testing Chrome.
9 It supports several test URLs, as specified by the handlers in TestPageHandler.
10 By default, it listens on an ephemeral port and sends the port number back to
11 the originating process over a pipe. The originating process can specify an
12 explicit port if necessary.
13 It can use https if you specify the flag --https=CERT where CERT is the path
14 to a pem file containing the certificate and private key that should be used.
15 """
17 import base64
18 import BaseHTTPServer
19 import cgi
20 import hashlib
21 import logging
22 import minica
23 import os
24 import json
25 import random
26 import re
27 import select
28 import socket
29 import SocketServer
30 import ssl
31 import struct
32 import sys
33 import threading
34 import time
35 import urllib
36 import urlparse
37 import zlib
39 BASE_DIR = os.path.dirname(os.path.abspath(__file__))
40 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(BASE_DIR)))
42 # Temporary hack to deal with tlslite 0.3.8 -> 0.4.6 upgrade.
44 # TODO(davidben): Remove this when it has cycled through all the bots and
45 # developer checkouts or when http://crbug.com/356276 is resolved.
46 try:
47 os.remove(os.path.join(ROOT_DIR, 'third_party', 'tlslite',
48 'tlslite', 'utils', 'hmac.pyc'))
49 except Exception:
50 pass
52 # Append at the end of sys.path, it's fine to use the system library.
53 sys.path.append(os.path.join(ROOT_DIR, 'third_party', 'pyftpdlib', 'src'))
55 # Insert at the beginning of the path, we want to use our copies of the library
56 # unconditionally.
57 sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party', 'pywebsocket', 'src'))
58 sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party', 'tlslite'))
60 import mod_pywebsocket.standalone
61 from mod_pywebsocket.standalone import WebSocketServer
62 # import manually
63 mod_pywebsocket.standalone.ssl = ssl
65 import pyftpdlib.ftpserver
67 import tlslite
68 import tlslite.api
70 import echo_message
71 import testserver_base
73 SERVER_HTTP = 0
74 SERVER_FTP = 1
75 SERVER_TCP_ECHO = 2
76 SERVER_UDP_ECHO = 3
77 SERVER_BASIC_AUTH_PROXY = 4
78 SERVER_WEBSOCKET = 5
80 # Default request queue size for WebSocketServer.
81 _DEFAULT_REQUEST_QUEUE_SIZE = 128
83 class WebSocketOptions:
84 """Holds options for WebSocketServer."""
86 def __init__(self, host, port, data_dir):
87 self.request_queue_size = _DEFAULT_REQUEST_QUEUE_SIZE
88 self.server_host = host
89 self.port = port
90 self.websock_handlers = data_dir
91 self.scan_dir = None
92 self.allow_handlers_outside_root_dir = False
93 self.websock_handlers_map_file = None
94 self.cgi_directories = []
95 self.is_executable_method = None
96 self.allow_draft75 = False
97 self.strict = True
99 self.use_tls = False
100 self.private_key = None
101 self.certificate = None
102 self.tls_client_auth = False
103 self.tls_client_ca = None
104 self.tls_module = 'ssl'
105 self.use_basic_auth = False
106 self.basic_auth_credential = 'Basic ' + base64.b64encode('test:test')
109 class RecordingSSLSessionCache(object):
110 """RecordingSSLSessionCache acts as a TLS session cache and maintains a log of
111 lookups and inserts in order to test session cache behaviours."""
113 def __init__(self):
114 self.log = []
116 def __getitem__(self, sessionID):
117 self.log.append(('lookup', sessionID))
118 raise KeyError()
120 def __setitem__(self, sessionID, session):
121 self.log.append(('insert', sessionID))
124 class HTTPServer(testserver_base.ClientRestrictingServerMixIn,
125 testserver_base.BrokenPipeHandlerMixIn,
126 testserver_base.StoppableHTTPServer):
127 """This is a specialization of StoppableHTTPServer that adds client
128 verification."""
130 pass
132 class OCSPServer(testserver_base.ClientRestrictingServerMixIn,
133 testserver_base.BrokenPipeHandlerMixIn,
134 BaseHTTPServer.HTTPServer):
135 """This is a specialization of HTTPServer that serves an
136 OCSP response"""
138 def serve_forever_on_thread(self):
139 self.thread = threading.Thread(target = self.serve_forever,
140 name = "OCSPServerThread")
141 self.thread.start()
143 def stop_serving(self):
144 self.shutdown()
145 self.thread.join()
148 class HTTPSServer(tlslite.api.TLSSocketServerMixIn,
149 testserver_base.ClientRestrictingServerMixIn,
150 testserver_base.BrokenPipeHandlerMixIn,
151 testserver_base.StoppableHTTPServer):
152 """This is a specialization of StoppableHTTPServer that add https support and
153 client verification."""
155 def __init__(self, server_address, request_hander_class, pem_cert_and_key,
156 ssl_client_auth, ssl_client_cas, ssl_client_cert_types,
157 ssl_bulk_ciphers, ssl_key_exchanges, enable_npn,
158 record_resume_info, tls_intolerant,
159 tls_intolerance_type, signed_cert_timestamps,
160 fallback_scsv_enabled, ocsp_response, disable_session_cache):
161 self.cert_chain = tlslite.api.X509CertChain()
162 self.cert_chain.parsePemList(pem_cert_and_key)
163 # Force using only python implementation - otherwise behavior is different
164 # depending on whether m2crypto Python module is present (error is thrown
165 # when it is). m2crypto uses a C (based on OpenSSL) implementation under
166 # the hood.
167 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key,
168 private=True,
169 implementations=['python'])
170 self.ssl_client_auth = ssl_client_auth
171 self.ssl_client_cas = []
172 self.ssl_client_cert_types = []
173 if enable_npn:
174 self.next_protos = ['http/1.1']
175 else:
176 self.next_protos = None
177 self.signed_cert_timestamps = signed_cert_timestamps
178 self.fallback_scsv_enabled = fallback_scsv_enabled
179 self.ocsp_response = ocsp_response
181 if ssl_client_auth:
182 for ca_file in ssl_client_cas:
183 s = open(ca_file).read()
184 x509 = tlslite.api.X509()
185 x509.parse(s)
186 self.ssl_client_cas.append(x509.subject)
188 for cert_type in ssl_client_cert_types:
189 self.ssl_client_cert_types.append({
190 "rsa_sign": tlslite.api.ClientCertificateType.rsa_sign,
191 "dss_sign": tlslite.api.ClientCertificateType.dss_sign,
192 "ecdsa_sign": tlslite.api.ClientCertificateType.ecdsa_sign,
193 }[cert_type])
195 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
196 if ssl_bulk_ciphers is not None:
197 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
198 if ssl_key_exchanges is not None:
199 self.ssl_handshake_settings.keyExchangeNames = ssl_key_exchanges
200 if tls_intolerant != 0:
201 self.ssl_handshake_settings.tlsIntolerant = (3, tls_intolerant)
202 self.ssl_handshake_settings.tlsIntoleranceType = tls_intolerance_type
205 if disable_session_cache:
206 self.session_cache = None
207 elif record_resume_info:
208 # If record_resume_info is true then we'll replace the session cache with
209 # an object that records the lookups and inserts that it sees.
210 self.session_cache = RecordingSSLSessionCache()
211 else:
212 self.session_cache = tlslite.api.SessionCache()
213 testserver_base.StoppableHTTPServer.__init__(self,
214 server_address,
215 request_hander_class)
217 def handshake(self, tlsConnection):
218 """Creates the SSL connection."""
220 try:
221 self.tlsConnection = tlsConnection
222 tlsConnection.handshakeServer(certChain=self.cert_chain,
223 privateKey=self.private_key,
224 sessionCache=self.session_cache,
225 reqCert=self.ssl_client_auth,
226 settings=self.ssl_handshake_settings,
227 reqCAs=self.ssl_client_cas,
228 reqCertTypes=self.ssl_client_cert_types,
229 nextProtos=self.next_protos,
230 signedCertTimestamps=
231 self.signed_cert_timestamps,
232 fallbackSCSV=self.fallback_scsv_enabled,
233 ocspResponse = self.ocsp_response)
234 tlsConnection.ignoreAbruptClose = True
235 return True
236 except tlslite.api.TLSAbruptCloseError:
237 # Ignore abrupt close.
238 return True
239 except tlslite.api.TLSError, error:
240 print "Handshake failure:", str(error)
241 return False
244 class FTPServer(testserver_base.ClientRestrictingServerMixIn,
245 pyftpdlib.ftpserver.FTPServer):
246 """This is a specialization of FTPServer that adds client verification."""
248 pass
251 class TCPEchoServer(testserver_base.ClientRestrictingServerMixIn,
252 SocketServer.TCPServer):
253 """A TCP echo server that echoes back what it has received."""
255 def server_bind(self):
256 """Override server_bind to store the server name."""
258 SocketServer.TCPServer.server_bind(self)
259 host, port = self.socket.getsockname()[:2]
260 self.server_name = socket.getfqdn(host)
261 self.server_port = port
263 def serve_forever(self):
264 self.stop = False
265 self.nonce_time = None
266 while not self.stop:
267 self.handle_request()
268 self.socket.close()
271 class UDPEchoServer(testserver_base.ClientRestrictingServerMixIn,
272 SocketServer.UDPServer):
273 """A UDP echo server that echoes back what it has received."""
275 def server_bind(self):
276 """Override server_bind to store the server name."""
278 SocketServer.UDPServer.server_bind(self)
279 host, port = self.socket.getsockname()[:2]
280 self.server_name = socket.getfqdn(host)
281 self.server_port = port
283 def serve_forever(self):
284 self.stop = False
285 self.nonce_time = None
286 while not self.stop:
287 self.handle_request()
288 self.socket.close()
291 class TestPageHandler(testserver_base.BasePageHandler):
292 # Class variables to allow for persistence state between page handler
293 # invocations
294 rst_limits = {}
295 fail_precondition = {}
297 def __init__(self, request, client_address, socket_server):
298 connect_handlers = [
299 self.RedirectConnectHandler,
300 self.ServerAuthConnectHandler,
301 self.DefaultConnectResponseHandler]
302 get_handlers = [
303 self.NoCacheMaxAgeTimeHandler,
304 self.NoCacheTimeHandler,
305 self.CacheTimeHandler,
306 self.CacheExpiresHandler,
307 self.CacheProxyRevalidateHandler,
308 self.CachePrivateHandler,
309 self.CachePublicHandler,
310 self.CacheSMaxAgeHandler,
311 self.CacheMustRevalidateHandler,
312 self.CacheMustRevalidateMaxAgeHandler,
313 self.CacheNoStoreHandler,
314 self.CacheNoStoreMaxAgeHandler,
315 self.CacheNoTransformHandler,
316 self.DownloadHandler,
317 self.DownloadFinishHandler,
318 self.EchoHeader,
319 self.EchoHeaderCache,
320 self.EchoAllHandler,
321 self.ZipFileHandler,
322 self.FileHandler,
323 self.SetCookieHandler,
324 self.SetManyCookiesHandler,
325 self.ExpectAndSetCookieHandler,
326 self.SetHeaderHandler,
327 self.AuthBasicHandler,
328 self.AuthDigestHandler,
329 self.SlowServerHandler,
330 self.ChunkedServerHandler,
331 self.ContentTypeHandler,
332 self.NoContentHandler,
333 self.ServerRedirectHandler,
334 self.ClientRedirectHandler,
335 self.GetSSLSessionCacheHandler,
336 self.SSLManySmallRecords,
337 self.GetChannelID,
338 self.CloseSocketHandler,
339 self.RangeResetHandler,
340 self.DefaultResponseHandler]
341 post_handlers = [
342 self.EchoTitleHandler,
343 self.EchoHandler,
344 self.PostOnlyFileHandler,
345 self.EchoMultipartPostHandler] + get_handlers
346 put_handlers = [
347 self.EchoTitleHandler,
348 self.EchoHandler] + get_handlers
349 head_handlers = [
350 self.FileHandler,
351 self.DefaultResponseHandler]
353 self._mime_types = {
354 'crx' : 'application/x-chrome-extension',
355 'exe' : 'application/octet-stream',
356 'gif': 'image/gif',
357 'jpeg' : 'image/jpeg',
358 'jpg' : 'image/jpeg',
359 'json': 'application/json',
360 'pdf' : 'application/pdf',
361 'txt' : 'text/plain',
362 'wav' : 'audio/wav',
363 'xml' : 'text/xml'
365 self._default_mime_type = 'text/html'
367 testserver_base.BasePageHandler.__init__(self, request, client_address,
368 socket_server, connect_handlers,
369 get_handlers, head_handlers,
370 post_handlers, put_handlers)
372 def GetMIMETypeFromName(self, file_name):
373 """Returns the mime type for the specified file_name. So far it only looks
374 at the file extension."""
376 (_shortname, extension) = os.path.splitext(file_name.split("?")[0])
377 if len(extension) == 0:
378 # no extension.
379 return self._default_mime_type
381 # extension starts with a dot, so we need to remove it
382 return self._mime_types.get(extension[1:], self._default_mime_type)
384 def NoCacheMaxAgeTimeHandler(self):
385 """This request handler yields a page with the title set to the current
386 system time, and no caching requested."""
388 if not self._ShouldHandleRequest("/nocachetime/maxage"):
389 return False
391 self.send_response(200)
392 self.send_header('Cache-Control', 'max-age=0')
393 self.send_header('Content-Type', 'text/html')
394 self.end_headers()
396 self.wfile.write('<html><head><title>%s</title></head></html>' %
397 time.time())
399 return True
401 def NoCacheTimeHandler(self):
402 """This request handler yields a page with the title set to the current
403 system time, and no caching requested."""
405 if not self._ShouldHandleRequest("/nocachetime"):
406 return False
408 self.send_response(200)
409 self.send_header('Cache-Control', 'no-cache')
410 self.send_header('Content-Type', 'text/html')
411 self.end_headers()
413 self.wfile.write('<html><head><title>%s</title></head></html>' %
414 time.time())
416 return True
418 def CacheTimeHandler(self):
419 """This request handler yields a page with the title set to the current
420 system time, and allows caching for one minute."""
422 if not self._ShouldHandleRequest("/cachetime"):
423 return False
425 self.send_response(200)
426 self.send_header('Cache-Control', 'max-age=60')
427 self.send_header('Content-Type', 'text/html')
428 self.end_headers()
430 self.wfile.write('<html><head><title>%s</title></head></html>' %
431 time.time())
433 return True
435 def CacheExpiresHandler(self):
436 """This request handler yields a page with the title set to the current
437 system time, and set the page to expire on 1 Jan 2099."""
439 if not self._ShouldHandleRequest("/cache/expires"):
440 return False
442 self.send_response(200)
443 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
444 self.send_header('Content-Type', 'text/html')
445 self.end_headers()
447 self.wfile.write('<html><head><title>%s</title></head></html>' %
448 time.time())
450 return True
452 def CacheProxyRevalidateHandler(self):
453 """This request handler yields a page with the title set to the current
454 system time, and allows caching for 60 seconds"""
456 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
457 return False
459 self.send_response(200)
460 self.send_header('Content-Type', 'text/html')
461 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
462 self.end_headers()
464 self.wfile.write('<html><head><title>%s</title></head></html>' %
465 time.time())
467 return True
469 def CachePrivateHandler(self):
470 """This request handler yields a page with the title set to the current
471 system time, and allows caching for 5 seconds."""
473 if not self._ShouldHandleRequest("/cache/private"):
474 return False
476 self.send_response(200)
477 self.send_header('Content-Type', 'text/html')
478 self.send_header('Cache-Control', 'max-age=3, private')
479 self.end_headers()
481 self.wfile.write('<html><head><title>%s</title></head></html>' %
482 time.time())
484 return True
486 def CachePublicHandler(self):
487 """This request handler yields a page with the title set to the current
488 system time, and allows caching for 5 seconds."""
490 if not self._ShouldHandleRequest("/cache/public"):
491 return False
493 self.send_response(200)
494 self.send_header('Content-Type', 'text/html')
495 self.send_header('Cache-Control', 'max-age=3, public')
496 self.end_headers()
498 self.wfile.write('<html><head><title>%s</title></head></html>' %
499 time.time())
501 return True
503 def CacheSMaxAgeHandler(self):
504 """This request handler yields a page with the title set to the current
505 system time, and does not allow for caching."""
507 if not self._ShouldHandleRequest("/cache/s-maxage"):
508 return False
510 self.send_response(200)
511 self.send_header('Content-Type', 'text/html')
512 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
513 self.end_headers()
515 self.wfile.write('<html><head><title>%s</title></head></html>' %
516 time.time())
518 return True
520 def CacheMustRevalidateHandler(self):
521 """This request handler yields a page with the title set to the current
522 system time, and does not allow caching."""
524 if not self._ShouldHandleRequest("/cache/must-revalidate"):
525 return False
527 self.send_response(200)
528 self.send_header('Content-Type', 'text/html')
529 self.send_header('Cache-Control', 'must-revalidate')
530 self.end_headers()
532 self.wfile.write('<html><head><title>%s</title></head></html>' %
533 time.time())
535 return True
537 def CacheMustRevalidateMaxAgeHandler(self):
538 """This request handler yields a page with the title set to the current
539 system time, and does not allow caching event though max-age of 60
540 seconds is specified."""
542 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
543 return False
545 self.send_response(200)
546 self.send_header('Content-Type', 'text/html')
547 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
548 self.end_headers()
550 self.wfile.write('<html><head><title>%s</title></head></html>' %
551 time.time())
553 return True
555 def CacheNoStoreHandler(self):
556 """This request handler yields a page with the title set to the current
557 system time, and does not allow the page to be stored."""
559 if not self._ShouldHandleRequest("/cache/no-store"):
560 return False
562 self.send_response(200)
563 self.send_header('Content-Type', 'text/html')
564 self.send_header('Cache-Control', 'no-store')
565 self.end_headers()
567 self.wfile.write('<html><head><title>%s</title></head></html>' %
568 time.time())
570 return True
572 def CacheNoStoreMaxAgeHandler(self):
573 """This request handler yields a page with the title set to the current
574 system time, and does not allow the page to be stored even though max-age
575 of 60 seconds is specified."""
577 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
578 return False
580 self.send_response(200)
581 self.send_header('Content-Type', 'text/html')
582 self.send_header('Cache-Control', 'max-age=60, no-store')
583 self.end_headers()
585 self.wfile.write('<html><head><title>%s</title></head></html>' %
586 time.time())
588 return True
591 def CacheNoTransformHandler(self):
592 """This request handler yields a page with the title set to the current
593 system time, and does not allow the content to transformed during
594 user-agent caching"""
596 if not self._ShouldHandleRequest("/cache/no-transform"):
597 return False
599 self.send_response(200)
600 self.send_header('Content-Type', 'text/html')
601 self.send_header('Cache-Control', 'no-transform')
602 self.end_headers()
604 self.wfile.write('<html><head><title>%s</title></head></html>' %
605 time.time())
607 return True
609 def EchoHeader(self):
610 """This handler echoes back the value of a specific request header."""
612 return self.EchoHeaderHelper("/echoheader")
614 def EchoHeaderCache(self):
615 """This function echoes back the value of a specific request header while
616 allowing caching for 16 hours."""
618 return self.EchoHeaderHelper("/echoheadercache")
620 def EchoHeaderHelper(self, echo_header):
621 """This function echoes back the value of the request header passed in."""
623 if not self._ShouldHandleRequest(echo_header):
624 return False
626 query_char = self.path.find('?')
627 if query_char != -1:
628 header_name = self.path[query_char+1:]
630 self.send_response(200)
631 self.send_header('Content-Type', 'text/plain')
632 if echo_header == '/echoheadercache':
633 self.send_header('Cache-control', 'max-age=60000')
634 else:
635 self.send_header('Cache-control', 'no-cache')
636 # insert a vary header to properly indicate that the cachability of this
637 # request is subject to value of the request header being echoed.
638 if len(header_name) > 0:
639 self.send_header('Vary', header_name)
640 self.end_headers()
642 if len(header_name) > 0:
643 self.wfile.write(self.headers.getheader(header_name))
645 return True
647 def ReadRequestBody(self):
648 """This function reads the body of the current HTTP request, handling
649 both plain and chunked transfer encoded requests."""
651 if self.headers.getheader('transfer-encoding') != 'chunked':
652 length = int(self.headers.getheader('content-length'))
653 return self.rfile.read(length)
655 # Read the request body as chunks.
656 body = ""
657 while True:
658 line = self.rfile.readline()
659 length = int(line, 16)
660 if length == 0:
661 self.rfile.readline()
662 break
663 body += self.rfile.read(length)
664 self.rfile.read(2)
665 return body
667 def EchoHandler(self):
668 """This handler just echoes back the payload of the request, for testing
669 form submission."""
671 if not self._ShouldHandleRequest("/echo"):
672 return False
674 self.send_response(200)
675 self.send_header('Content-Type', 'text/html')
676 self.end_headers()
677 self.wfile.write(self.ReadRequestBody())
678 return True
680 def EchoTitleHandler(self):
681 """This handler is like Echo, but sets the page title to the request."""
683 if not self._ShouldHandleRequest("/echotitle"):
684 return False
686 self.send_response(200)
687 self.send_header('Content-Type', 'text/html')
688 self.end_headers()
689 request = self.ReadRequestBody()
690 self.wfile.write('<html><head><title>')
691 self.wfile.write(request)
692 self.wfile.write('</title></head></html>')
693 return True
695 def EchoAllHandler(self):
696 """This handler yields a (more) human-readable page listing information
697 about the request header & contents."""
699 if not self._ShouldHandleRequest("/echoall"):
700 return False
702 self.send_response(200)
703 self.send_header('Content-Type', 'text/html')
704 self.end_headers()
705 self.wfile.write('<html><head><style>'
706 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
707 '</style></head><body>'
708 '<div style="float: right">'
709 '<a href="/echo">back to referring page</a></div>'
710 '<h1>Request Body:</h1><pre>')
712 if self.command == 'POST' or self.command == 'PUT':
713 qs = self.ReadRequestBody()
714 params = cgi.parse_qs(qs, keep_blank_values=1)
716 for param in params:
717 self.wfile.write('%s=%s\n' % (param, params[param][0]))
719 self.wfile.write('</pre>')
721 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
723 self.wfile.write('</body></html>')
724 return True
726 def EchoMultipartPostHandler(self):
727 """This handler echoes received multipart post data as json format."""
729 if not (self._ShouldHandleRequest("/echomultipartpost") or
730 self._ShouldHandleRequest("/searchbyimage")):
731 return False
733 content_type, parameters = cgi.parse_header(
734 self.headers.getheader('content-type'))
735 if content_type == 'multipart/form-data':
736 post_multipart = cgi.parse_multipart(self.rfile, parameters)
737 elif content_type == 'application/x-www-form-urlencoded':
738 raise Exception('POST by application/x-www-form-urlencoded is '
739 'not implemented.')
740 else:
741 post_multipart = {}
743 # Since the data can be binary, we encode them by base64.
744 post_multipart_base64_encoded = {}
745 for field, values in post_multipart.items():
746 post_multipart_base64_encoded[field] = [base64.b64encode(value)
747 for value in values]
749 result = {'POST_multipart' : post_multipart_base64_encoded}
751 self.send_response(200)
752 self.send_header("Content-type", "text/plain")
753 self.end_headers()
754 self.wfile.write(json.dumps(result, indent=2, sort_keys=False))
755 return True
757 def DownloadHandler(self):
758 """This handler sends a downloadable file with or without reporting
759 the size (6K)."""
761 if self.path.startswith("/download-unknown-size"):
762 send_length = False
763 elif self.path.startswith("/download-known-size"):
764 send_length = True
765 else:
766 return False
769 # The test which uses this functionality is attempting to send
770 # small chunks of data to the client. Use a fairly large buffer
771 # so that we'll fill chrome's IO buffer enough to force it to
772 # actually write the data.
773 # See also the comments in the client-side of this test in
774 # download_uitest.cc
776 size_chunk1 = 35*1024
777 size_chunk2 = 10*1024
779 self.send_response(200)
780 self.send_header('Content-Type', 'application/octet-stream')
781 self.send_header('Cache-Control', 'max-age=0')
782 if send_length:
783 self.send_header('Content-Length', size_chunk1 + size_chunk2)
784 self.end_headers()
786 # First chunk of data:
787 self.wfile.write("*" * size_chunk1)
788 self.wfile.flush()
790 # handle requests until one of them clears this flag.
791 self.server.wait_for_download = True
792 while self.server.wait_for_download:
793 self.server.handle_request()
795 # Second chunk of data:
796 self.wfile.write("*" * size_chunk2)
797 return True
799 def DownloadFinishHandler(self):
800 """This handler just tells the server to finish the current download."""
802 if not self._ShouldHandleRequest("/download-finish"):
803 return False
805 self.server.wait_for_download = False
806 self.send_response(200)
807 self.send_header('Content-Type', 'text/html')
808 self.send_header('Cache-Control', 'max-age=0')
809 self.end_headers()
810 return True
812 def _ReplaceFileData(self, data, query_parameters):
813 """Replaces matching substrings in a file.
815 If the 'replace_text' URL query parameter is present, it is expected to be
816 of the form old_text:new_text, which indicates that any old_text strings in
817 the file are replaced with new_text. Multiple 'replace_text' parameters may
818 be specified.
820 If the parameters are not present, |data| is returned.
823 query_dict = cgi.parse_qs(query_parameters)
824 replace_text_values = query_dict.get('replace_text', [])
825 for replace_text_value in replace_text_values:
826 replace_text_args = replace_text_value.split(':')
827 if len(replace_text_args) != 2:
828 raise ValueError(
829 'replace_text must be of form old_text:new_text. Actual value: %s' %
830 replace_text_value)
831 old_text_b64, new_text_b64 = replace_text_args
832 old_text = base64.urlsafe_b64decode(old_text_b64)
833 new_text = base64.urlsafe_b64decode(new_text_b64)
834 data = data.replace(old_text, new_text)
835 return data
837 def ZipFileHandler(self):
838 """This handler sends the contents of the requested file in compressed form.
839 Can pass in a parameter that specifies that the content length be
840 C - the compressed size (OK),
841 U - the uncompressed size (Non-standard, but handled),
842 S - less than compressed (OK because we keep going),
843 M - larger than compressed but less than uncompressed (an error),
844 L - larger than uncompressed (an error)
845 Example: compressedfiles/Picture_1.doc?C
848 prefix = "/compressedfiles/"
849 if not self.path.startswith(prefix):
850 return False
852 # Consume a request body if present.
853 if self.command == 'POST' or self.command == 'PUT' :
854 self.ReadRequestBody()
856 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
858 if not query in ('C', 'U', 'S', 'M', 'L'):
859 return False
861 sub_path = url_path[len(prefix):]
862 entries = sub_path.split('/')
863 file_path = os.path.join(self.server.data_dir, *entries)
864 if os.path.isdir(file_path):
865 file_path = os.path.join(file_path, 'index.html')
867 if not os.path.isfile(file_path):
868 print "File not found " + sub_path + " full path:" + file_path
869 self.send_error(404)
870 return True
872 f = open(file_path, "rb")
873 data = f.read()
874 uncompressed_len = len(data)
875 f.close()
877 # Compress the data.
878 data = zlib.compress(data)
879 compressed_len = len(data)
881 content_length = compressed_len
882 if query == 'U':
883 content_length = uncompressed_len
884 elif query == 'S':
885 content_length = compressed_len / 2
886 elif query == 'M':
887 content_length = (compressed_len + uncompressed_len) / 2
888 elif query == 'L':
889 content_length = compressed_len + uncompressed_len
891 self.send_response(200)
892 self.send_header('Content-Type', 'application/msword')
893 self.send_header('Content-encoding', 'deflate')
894 self.send_header('Connection', 'close')
895 self.send_header('Content-Length', content_length)
896 self.send_header('ETag', '\'' + file_path + '\'')
897 self.end_headers()
899 self.wfile.write(data)
901 return True
903 def FileHandler(self):
904 """This handler sends the contents of the requested file. Wow, it's like
905 a real webserver!"""
907 prefix = self.server.file_root_url
908 if not self.path.startswith(prefix):
909 return False
910 return self._FileHandlerHelper(prefix)
912 def PostOnlyFileHandler(self):
913 """This handler sends the contents of the requested file on a POST."""
915 prefix = urlparse.urljoin(self.server.file_root_url, 'post/')
916 if not self.path.startswith(prefix):
917 return False
918 return self._FileHandlerHelper(prefix)
920 def _FileHandlerHelper(self, prefix):
921 request_body = ''
922 if self.command == 'POST' or self.command == 'PUT':
923 # Consume a request body if present.
924 request_body = self.ReadRequestBody()
926 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
927 query_dict = cgi.parse_qs(query)
929 expected_body = query_dict.get('expected_body', [])
930 if expected_body and request_body not in expected_body:
931 self.send_response(404)
932 self.end_headers()
933 self.wfile.write('')
934 return True
936 expected_headers = query_dict.get('expected_headers', [])
937 for expected_header in expected_headers:
938 header_name, expected_value = expected_header.split(':')
939 if self.headers.getheader(header_name) != expected_value:
940 self.send_response(404)
941 self.end_headers()
942 self.wfile.write('')
943 return True
945 sub_path = url_path[len(prefix):]
946 entries = sub_path.split('/')
947 file_path = os.path.join(self.server.data_dir, *entries)
948 if os.path.isdir(file_path):
949 file_path = os.path.join(file_path, 'index.html')
951 if not os.path.isfile(file_path):
952 print "File not found " + sub_path + " full path:" + file_path
953 self.send_error(404)
954 return True
956 f = open(file_path, "rb")
957 data = f.read()
958 f.close()
960 data = self._ReplaceFileData(data, query)
962 old_protocol_version = self.protocol_version
964 # If file.mock-http-headers exists, it contains the headers we
965 # should send. Read them in and parse them.
966 headers_path = file_path + '.mock-http-headers'
967 if os.path.isfile(headers_path):
968 f = open(headers_path, "r")
970 # "HTTP/1.1 200 OK"
971 response = f.readline()
972 http_major, http_minor, status_code = re.findall(
973 'HTTP/(\d+).(\d+) (\d+)', response)[0]
974 self.protocol_version = "HTTP/%s.%s" % (http_major, http_minor)
975 self.send_response(int(status_code))
977 for line in f:
978 header_values = re.findall('(\S+):\s*(.*)', line)
979 if len(header_values) > 0:
980 # "name: value"
981 name, value = header_values[0]
982 self.send_header(name, value)
983 f.close()
984 else:
985 # Could be more generic once we support mime-type sniffing, but for
986 # now we need to set it explicitly.
988 range_header = self.headers.get('Range')
989 if range_header and range_header.startswith('bytes='):
990 # Note this doesn't handle all valid byte range_header values (i.e.
991 # left open ended ones), just enough for what we needed so far.
992 range_header = range_header[6:].split('-')
993 start = int(range_header[0])
994 if range_header[1]:
995 end = int(range_header[1])
996 else:
997 end = len(data) - 1
999 self.send_response(206)
1000 content_range = ('bytes ' + str(start) + '-' + str(end) + '/' +
1001 str(len(data)))
1002 self.send_header('Content-Range', content_range)
1003 data = data[start: end + 1]
1004 else:
1005 self.send_response(200)
1007 self.send_header('Content-Type', self.GetMIMETypeFromName(file_path))
1008 self.send_header('Accept-Ranges', 'bytes')
1009 self.send_header('Content-Length', len(data))
1010 self.send_header('ETag', '\'' + file_path + '\'')
1011 self.end_headers()
1013 if (self.command != 'HEAD'):
1014 self.wfile.write(data)
1016 self.protocol_version = old_protocol_version
1017 return True
1019 def SetCookieHandler(self):
1020 """This handler just sets a cookie, for testing cookie handling."""
1022 if not self._ShouldHandleRequest("/set-cookie"):
1023 return False
1025 query_char = self.path.find('?')
1026 if query_char != -1:
1027 cookie_values = self.path[query_char + 1:].split('&')
1028 else:
1029 cookie_values = ("",)
1030 self.send_response(200)
1031 self.send_header('Content-Type', 'text/html')
1032 for cookie_value in cookie_values:
1033 self.send_header('Set-Cookie', '%s' % cookie_value)
1034 self.end_headers()
1035 for cookie_value in cookie_values:
1036 self.wfile.write('%s' % cookie_value)
1037 return True
1039 def SetManyCookiesHandler(self):
1040 """This handler just sets a given number of cookies, for testing handling
1041 of large numbers of cookies."""
1043 if not self._ShouldHandleRequest("/set-many-cookies"):
1044 return False
1046 query_char = self.path.find('?')
1047 if query_char != -1:
1048 num_cookies = int(self.path[query_char + 1:])
1049 else:
1050 num_cookies = 0
1051 self.send_response(200)
1052 self.send_header('', 'text/html')
1053 for _i in range(0, num_cookies):
1054 self.send_header('Set-Cookie', 'a=')
1055 self.end_headers()
1056 self.wfile.write('%d cookies were sent' % num_cookies)
1057 return True
1059 def ExpectAndSetCookieHandler(self):
1060 """Expects some cookies to be sent, and if they are, sets more cookies.
1062 The expect parameter specifies a required cookie. May be specified multiple
1063 times.
1064 The set parameter specifies a cookie to set if all required cookies are
1065 preset. May be specified multiple times.
1066 The data parameter specifies the response body data to be returned."""
1068 if not self._ShouldHandleRequest("/expect-and-set-cookie"):
1069 return False
1071 _, _, _, _, query, _ = urlparse.urlparse(self.path)
1072 query_dict = cgi.parse_qs(query)
1073 cookies = set()
1074 if 'Cookie' in self.headers:
1075 cookie_header = self.headers.getheader('Cookie')
1076 cookies.update([s.strip() for s in cookie_header.split(';')])
1077 got_all_expected_cookies = True
1078 for expected_cookie in query_dict.get('expect', []):
1079 if expected_cookie not in cookies:
1080 got_all_expected_cookies = False
1081 self.send_response(200)
1082 self.send_header('Content-Type', 'text/html')
1083 if got_all_expected_cookies:
1084 for cookie_value in query_dict.get('set', []):
1085 self.send_header('Set-Cookie', '%s' % cookie_value)
1086 self.end_headers()
1087 for data_value in query_dict.get('data', []):
1088 self.wfile.write(data_value)
1089 return True
1091 def SetHeaderHandler(self):
1092 """This handler sets a response header. Parameters are in the
1093 key%3A%20value&key2%3A%20value2 format."""
1095 if not self._ShouldHandleRequest("/set-header"):
1096 return False
1098 query_char = self.path.find('?')
1099 if query_char != -1:
1100 headers_values = self.path[query_char + 1:].split('&')
1101 else:
1102 headers_values = ("",)
1103 self.send_response(200)
1104 self.send_header('Content-Type', 'text/html')
1105 for header_value in headers_values:
1106 header_value = urllib.unquote(header_value)
1107 (key, value) = header_value.split(': ', 1)
1108 self.send_header(key, value)
1109 self.end_headers()
1110 for header_value in headers_values:
1111 self.wfile.write('%s' % header_value)
1112 return True
1114 def AuthBasicHandler(self):
1115 """This handler tests 'Basic' authentication. It just sends a page with
1116 title 'user/pass' if you succeed."""
1118 if not self._ShouldHandleRequest("/auth-basic"):
1119 return False
1121 username = userpass = password = b64str = ""
1122 expected_password = 'secret'
1123 realm = 'testrealm'
1124 set_cookie_if_challenged = False
1126 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1127 query_params = cgi.parse_qs(query, True)
1128 if 'set-cookie-if-challenged' in query_params:
1129 set_cookie_if_challenged = True
1130 if 'password' in query_params:
1131 expected_password = query_params['password'][0]
1132 if 'realm' in query_params:
1133 realm = query_params['realm'][0]
1135 auth = self.headers.getheader('authorization')
1136 try:
1137 if not auth:
1138 raise Exception('no auth')
1139 b64str = re.findall(r'Basic (\S+)', auth)[0]
1140 userpass = base64.b64decode(b64str)
1141 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
1142 if password != expected_password:
1143 raise Exception('wrong password')
1144 except Exception, e:
1145 # Authentication failed.
1146 self.send_response(401)
1147 self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
1148 self.send_header('Content-Type', 'text/html')
1149 if set_cookie_if_challenged:
1150 self.send_header('Set-Cookie', 'got_challenged=true')
1151 self.end_headers()
1152 self.wfile.write('<html><head>')
1153 self.wfile.write('<title>Denied: %s</title>' % e)
1154 self.wfile.write('</head><body>')
1155 self.wfile.write('auth=%s<p>' % auth)
1156 self.wfile.write('b64str=%s<p>' % b64str)
1157 self.wfile.write('username: %s<p>' % username)
1158 self.wfile.write('userpass: %s<p>' % userpass)
1159 self.wfile.write('password: %s<p>' % password)
1160 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1161 self.wfile.write('</body></html>')
1162 return True
1164 # Authentication successful. (Return a cachable response to allow for
1165 # testing cached pages that require authentication.)
1166 old_protocol_version = self.protocol_version
1167 self.protocol_version = "HTTP/1.1"
1169 if_none_match = self.headers.getheader('if-none-match')
1170 if if_none_match == "abc":
1171 self.send_response(304)
1172 self.end_headers()
1173 elif url_path.endswith(".gif"):
1174 # Using chrome/test/data/google/logo.gif as the test image
1175 test_image_path = ['google', 'logo.gif']
1176 gif_path = os.path.join(self.server.data_dir, *test_image_path)
1177 if not os.path.isfile(gif_path):
1178 self.send_error(404)
1179 self.protocol_version = old_protocol_version
1180 return True
1182 f = open(gif_path, "rb")
1183 data = f.read()
1184 f.close()
1186 self.send_response(200)
1187 self.send_header('Content-Type', 'image/gif')
1188 self.send_header('Cache-control', 'max-age=60000')
1189 self.send_header('Etag', 'abc')
1190 self.end_headers()
1191 self.wfile.write(data)
1192 else:
1193 self.send_response(200)
1194 self.send_header('Content-Type', 'text/html')
1195 self.send_header('Cache-control', 'max-age=60000')
1196 self.send_header('Etag', 'abc')
1197 self.end_headers()
1198 self.wfile.write('<html><head>')
1199 self.wfile.write('<title>%s/%s</title>' % (username, password))
1200 self.wfile.write('</head><body>')
1201 self.wfile.write('auth=%s<p>' % auth)
1202 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1203 self.wfile.write('</body></html>')
1205 self.protocol_version = old_protocol_version
1206 return True
1208 def GetNonce(self, force_reset=False):
1209 """Returns a nonce that's stable per request path for the server's lifetime.
1210 This is a fake implementation. A real implementation would only use a given
1211 nonce a single time (hence the name n-once). However, for the purposes of
1212 unittesting, we don't care about the security of the nonce.
1214 Args:
1215 force_reset: Iff set, the nonce will be changed. Useful for testing the
1216 "stale" response.
1219 if force_reset or not self.server.nonce_time:
1220 self.server.nonce_time = time.time()
1221 return hashlib.md5('privatekey%s%d' %
1222 (self.path, self.server.nonce_time)).hexdigest()
1224 def AuthDigestHandler(self):
1225 """This handler tests 'Digest' authentication.
1227 It just sends a page with title 'user/pass' if you succeed.
1229 A stale response is sent iff "stale" is present in the request path.
1232 if not self._ShouldHandleRequest("/auth-digest"):
1233 return False
1235 stale = 'stale' in self.path
1236 nonce = self.GetNonce(force_reset=stale)
1237 opaque = hashlib.md5('opaque').hexdigest()
1238 password = 'secret'
1239 realm = 'testrealm'
1241 auth = self.headers.getheader('authorization')
1242 pairs = {}
1243 try:
1244 if not auth:
1245 raise Exception('no auth')
1246 if not auth.startswith('Digest'):
1247 raise Exception('not digest')
1248 # Pull out all the name="value" pairs as a dictionary.
1249 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1251 # Make sure it's all valid.
1252 if pairs['nonce'] != nonce:
1253 raise Exception('wrong nonce')
1254 if pairs['opaque'] != opaque:
1255 raise Exception('wrong opaque')
1257 # Check the 'response' value and make sure it matches our magic hash.
1258 # See http://www.ietf.org/rfc/rfc2617.txt
1259 hash_a1 = hashlib.md5(
1260 ':'.join([pairs['username'], realm, password])).hexdigest()
1261 hash_a2 = hashlib.md5(':'.join([self.command, pairs['uri']])).hexdigest()
1262 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
1263 response = hashlib.md5(':'.join([hash_a1, nonce, pairs['nc'],
1264 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1265 else:
1266 response = hashlib.md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
1268 if pairs['response'] != response:
1269 raise Exception('wrong password')
1270 except Exception, e:
1271 # Authentication failed.
1272 self.send_response(401)
1273 hdr = ('Digest '
1274 'realm="%s", '
1275 'domain="/", '
1276 'qop="auth", '
1277 'algorithm=MD5, '
1278 'nonce="%s", '
1279 'opaque="%s"') % (realm, nonce, opaque)
1280 if stale:
1281 hdr += ', stale="TRUE"'
1282 self.send_header('WWW-Authenticate', hdr)
1283 self.send_header('Content-Type', 'text/html')
1284 self.end_headers()
1285 self.wfile.write('<html><head>')
1286 self.wfile.write('<title>Denied: %s</title>' % e)
1287 self.wfile.write('</head><body>')
1288 self.wfile.write('auth=%s<p>' % auth)
1289 self.wfile.write('pairs=%s<p>' % pairs)
1290 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1291 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1292 self.wfile.write('</body></html>')
1293 return True
1295 # Authentication successful.
1296 self.send_response(200)
1297 self.send_header('Content-Type', 'text/html')
1298 self.end_headers()
1299 self.wfile.write('<html><head>')
1300 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1301 self.wfile.write('</head><body>')
1302 self.wfile.write('auth=%s<p>' % auth)
1303 self.wfile.write('pairs=%s<p>' % pairs)
1304 self.wfile.write('</body></html>')
1306 return True
1308 def SlowServerHandler(self):
1309 """Wait for the user suggested time before responding. The syntax is
1310 /slow?0.5 to wait for half a second."""
1312 if not self._ShouldHandleRequest("/slow"):
1313 return False
1314 query_char = self.path.find('?')
1315 wait_sec = 1.0
1316 if query_char >= 0:
1317 try:
1318 wait_sec = int(self.path[query_char + 1:])
1319 except ValueError:
1320 pass
1321 time.sleep(wait_sec)
1322 self.send_response(200)
1323 self.send_header('Content-Type', 'text/plain')
1324 self.end_headers()
1325 self.wfile.write("waited %d seconds" % wait_sec)
1326 return True
1328 def ChunkedServerHandler(self):
1329 """Send chunked response. Allows to specify chunks parameters:
1330 - waitBeforeHeaders - ms to wait before sending headers
1331 - waitBetweenChunks - ms to wait between chunks
1332 - chunkSize - size of each chunk in bytes
1333 - chunksNumber - number of chunks
1334 Example: /chunked?waitBeforeHeaders=1000&chunkSize=5&chunksNumber=5
1335 waits one second, then sends headers and five chunks five bytes each."""
1337 if not self._ShouldHandleRequest("/chunked"):
1338 return False
1339 query_char = self.path.find('?')
1340 chunkedSettings = {'waitBeforeHeaders' : 0,
1341 'waitBetweenChunks' : 0,
1342 'chunkSize' : 5,
1343 'chunksNumber' : 5}
1344 if query_char >= 0:
1345 params = self.path[query_char + 1:].split('&')
1346 for param in params:
1347 keyValue = param.split('=')
1348 if len(keyValue) == 2:
1349 try:
1350 chunkedSettings[keyValue[0]] = int(keyValue[1])
1351 except ValueError:
1352 pass
1353 time.sleep(0.001 * chunkedSettings['waitBeforeHeaders'])
1354 self.protocol_version = 'HTTP/1.1' # Needed for chunked encoding
1355 self.send_response(200)
1356 self.send_header('Content-Type', 'text/plain')
1357 self.send_header('Connection', 'close')
1358 self.send_header('Transfer-Encoding', 'chunked')
1359 self.end_headers()
1360 # Chunked encoding: sending all chunks, then final zero-length chunk and
1361 # then final CRLF.
1362 for i in range(0, chunkedSettings['chunksNumber']):
1363 if i > 0:
1364 time.sleep(0.001 * chunkedSettings['waitBetweenChunks'])
1365 self.sendChunkHelp('*' * chunkedSettings['chunkSize'])
1366 self.wfile.flush() # Keep in mind that we start flushing only after 1kb.
1367 self.sendChunkHelp('')
1368 return True
1370 def ContentTypeHandler(self):
1371 """Returns a string of html with the given content type. E.g.,
1372 /contenttype?text/css returns an html file with the Content-Type
1373 header set to text/css."""
1375 if not self._ShouldHandleRequest("/contenttype"):
1376 return False
1377 query_char = self.path.find('?')
1378 content_type = self.path[query_char + 1:].strip()
1379 if not content_type:
1380 content_type = 'text/html'
1381 self.send_response(200)
1382 self.send_header('Content-Type', content_type)
1383 self.end_headers()
1384 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n")
1385 return True
1387 def NoContentHandler(self):
1388 """Returns a 204 No Content response."""
1390 if not self._ShouldHandleRequest("/nocontent"):
1391 return False
1392 self.send_response(204)
1393 self.end_headers()
1394 return True
1396 def ServerRedirectHandler(self):
1397 """Sends a server redirect to the given URL. The syntax is
1398 '/server-redirect?http://foo.bar/asdf' to redirect to
1399 'http://foo.bar/asdf'"""
1401 test_name = "/server-redirect"
1402 if not self._ShouldHandleRequest(test_name):
1403 return False
1405 query_char = self.path.find('?')
1406 if query_char < 0 or len(self.path) <= query_char + 1:
1407 self.sendRedirectHelp(test_name)
1408 return True
1409 dest = urllib.unquote(self.path[query_char + 1:])
1411 self.send_response(301) # moved permanently
1412 self.send_header('Location', dest)
1413 self.send_header('Content-Type', 'text/html')
1414 self.end_headers()
1415 self.wfile.write('<html><head>')
1416 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1418 return True
1420 def ClientRedirectHandler(self):
1421 """Sends a client redirect to the given URL. The syntax is
1422 '/client-redirect?http://foo.bar/asdf' to redirect to
1423 'http://foo.bar/asdf'"""
1425 test_name = "/client-redirect"
1426 if not self._ShouldHandleRequest(test_name):
1427 return False
1429 query_char = self.path.find('?')
1430 if query_char < 0 or len(self.path) <= query_char + 1:
1431 self.sendRedirectHelp(test_name)
1432 return True
1433 dest = urllib.unquote(self.path[query_char + 1:])
1435 self.send_response(200)
1436 self.send_header('Content-Type', 'text/html')
1437 self.end_headers()
1438 self.wfile.write('<html><head>')
1439 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1440 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1442 return True
1444 def GetSSLSessionCacheHandler(self):
1445 """Send a reply containing a log of the session cache operations."""
1447 if not self._ShouldHandleRequest('/ssl-session-cache'):
1448 return False
1450 self.send_response(200)
1451 self.send_header('Content-Type', 'text/plain')
1452 self.end_headers()
1453 try:
1454 log = self.server.session_cache.log
1455 except AttributeError:
1456 self.wfile.write('Pass --https-record-resume in order to use' +
1457 ' this request')
1458 return True
1460 for (action, sessionID) in log:
1461 self.wfile.write('%s\t%s\n' % (action, bytes(sessionID).encode('hex')))
1462 return True
1464 def SSLManySmallRecords(self):
1465 """Sends a reply consisting of a variety of small writes. These will be
1466 translated into a series of small SSL records when used over an HTTPS
1467 server."""
1469 if not self._ShouldHandleRequest('/ssl-many-small-records'):
1470 return False
1472 self.send_response(200)
1473 self.send_header('Content-Type', 'text/plain')
1474 self.end_headers()
1476 # Write ~26K of data, in 1350 byte chunks
1477 for i in xrange(20):
1478 self.wfile.write('*' * 1350)
1479 self.wfile.flush()
1480 return True
1482 def GetChannelID(self):
1483 """Send a reply containing the hashed ChannelID that the client provided."""
1485 if not self._ShouldHandleRequest('/channel-id'):
1486 return False
1488 self.send_response(200)
1489 self.send_header('Content-Type', 'text/plain')
1490 self.end_headers()
1491 channel_id = bytes(self.server.tlsConnection.channel_id)
1492 self.wfile.write(hashlib.sha256(channel_id).digest().encode('base64'))
1493 return True
1495 def CloseSocketHandler(self):
1496 """Closes the socket without sending anything."""
1498 if not self._ShouldHandleRequest('/close-socket'):
1499 return False
1501 self.wfile.close()
1502 return True
1504 def RangeResetHandler(self):
1505 """Send data broken up by connection resets every N (default 4K) bytes.
1506 Support range requests. If the data requested doesn't straddle a reset
1507 boundary, it will all be sent. Used for testing resuming downloads."""
1509 def DataForRange(start, end):
1510 """Data to be provided for a particular range of bytes."""
1511 # Offset and scale to avoid too obvious (and hence potentially
1512 # collidable) data.
1513 return ''.join([chr(y % 256)
1514 for y in range(start * 2 + 15, end * 2 + 15, 2)])
1516 if not self._ShouldHandleRequest('/rangereset'):
1517 return False
1519 # HTTP/1.1 is required for ETag and range support.
1520 self.protocol_version = 'HTTP/1.1'
1521 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1523 # Defaults
1524 size = 8000
1525 # Note that the rst is sent just before sending the rst_boundary byte.
1526 rst_boundary = 4000
1527 respond_to_range = True
1528 hold_for_signal = False
1529 rst_limit = -1
1530 token = 'DEFAULT'
1531 fail_precondition = 0
1532 send_verifiers = True
1534 # Parse the query
1535 qdict = urlparse.parse_qs(query, True)
1536 if 'size' in qdict:
1537 size = int(qdict['size'][0])
1538 if 'rst_boundary' in qdict:
1539 rst_boundary = int(qdict['rst_boundary'][0])
1540 if 'token' in qdict:
1541 # Identifying token for stateful tests.
1542 token = qdict['token'][0]
1543 if 'rst_limit' in qdict:
1544 # Max number of rsts for a given token.
1545 rst_limit = int(qdict['rst_limit'][0])
1546 if 'bounce_range' in qdict:
1547 respond_to_range = False
1548 if 'hold' in qdict:
1549 # Note that hold_for_signal will not work with null range requests;
1550 # see TODO below.
1551 hold_for_signal = True
1552 if 'no_verifiers' in qdict:
1553 send_verifiers = False
1554 if 'fail_precondition' in qdict:
1555 fail_precondition = int(qdict['fail_precondition'][0])
1557 # Record already set information, or set it.
1558 rst_limit = TestPageHandler.rst_limits.setdefault(token, rst_limit)
1559 if rst_limit != 0:
1560 TestPageHandler.rst_limits[token] -= 1
1561 fail_precondition = TestPageHandler.fail_precondition.setdefault(
1562 token, fail_precondition)
1563 if fail_precondition != 0:
1564 TestPageHandler.fail_precondition[token] -= 1
1566 first_byte = 0
1567 last_byte = size - 1
1569 # Does that define what we want to return, or do we need to apply
1570 # a range?
1571 range_response = False
1572 range_header = self.headers.getheader('range')
1573 if range_header and respond_to_range:
1574 mo = re.match("bytes=(\d*)-(\d*)", range_header)
1575 if mo.group(1):
1576 first_byte = int(mo.group(1))
1577 if mo.group(2):
1578 last_byte = int(mo.group(2))
1579 if last_byte > size - 1:
1580 last_byte = size - 1
1581 range_response = True
1582 if last_byte < first_byte:
1583 return False
1585 if (fail_precondition and
1586 (self.headers.getheader('If-Modified-Since') or
1587 self.headers.getheader('If-Match'))):
1588 self.send_response(412)
1589 self.end_headers()
1590 return True
1592 if range_response:
1593 self.send_response(206)
1594 self.send_header('Content-Range',
1595 'bytes %d-%d/%d' % (first_byte, last_byte, size))
1596 else:
1597 self.send_response(200)
1598 self.send_header('Content-Type', 'application/octet-stream')
1599 self.send_header('Content-Length', last_byte - first_byte + 1)
1600 if send_verifiers:
1601 # If fail_precondition is non-zero, then the ETag for each request will be
1602 # different.
1603 etag = "%s%d" % (token, fail_precondition)
1604 self.send_header('ETag', etag)
1605 self.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST')
1606 self.end_headers()
1608 if hold_for_signal:
1609 # TODO(rdsmith/phajdan.jr): http://crbug.com/169519: Without writing
1610 # a single byte, the self.server.handle_request() below hangs
1611 # without processing new incoming requests.
1612 self.wfile.write(DataForRange(first_byte, first_byte + 1))
1613 first_byte = first_byte + 1
1614 # handle requests until one of them clears this flag.
1615 self.server.wait_for_download = True
1616 while self.server.wait_for_download:
1617 self.server.handle_request()
1619 possible_rst = ((first_byte / rst_boundary) + 1) * rst_boundary
1620 if possible_rst >= last_byte or rst_limit == 0:
1621 # No RST has been requested in this range, so we don't need to
1622 # do anything fancy; just write the data and let the python
1623 # infrastructure close the connection.
1624 self.wfile.write(DataForRange(first_byte, last_byte + 1))
1625 self.wfile.flush()
1626 return True
1628 # We're resetting the connection part way in; go to the RST
1629 # boundary and then send an RST.
1630 # Because socket semantics do not guarantee that all the data will be
1631 # sent when using the linger semantics to hard close a socket,
1632 # we send the data and then wait for our peer to release us
1633 # before sending the reset.
1634 data = DataForRange(first_byte, possible_rst)
1635 self.wfile.write(data)
1636 self.wfile.flush()
1637 self.server.wait_for_download = True
1638 while self.server.wait_for_download:
1639 self.server.handle_request()
1640 l_onoff = 1 # Linger is active.
1641 l_linger = 0 # Seconds to linger for.
1642 self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
1643 struct.pack('ii', l_onoff, l_linger))
1645 # Close all duplicates of the underlying socket to force the RST.
1646 self.wfile.close()
1647 self.rfile.close()
1648 self.connection.close()
1650 return True
1652 def DefaultResponseHandler(self):
1653 """This is the catch-all response handler for requests that aren't handled
1654 by one of the special handlers above.
1655 Note that we specify the content-length as without it the https connection
1656 is not closed properly (and the browser keeps expecting data)."""
1658 contents = "Default response given for path: " + self.path
1659 self.send_response(200)
1660 self.send_header('Content-Type', 'text/html')
1661 self.send_header('Content-Length', len(contents))
1662 self.end_headers()
1663 if (self.command != 'HEAD'):
1664 self.wfile.write(contents)
1665 return True
1667 def RedirectConnectHandler(self):
1668 """Sends a redirect to the CONNECT request for www.redirect.com. This
1669 response is not specified by the RFC, so the browser should not follow
1670 the redirect."""
1672 if (self.path.find("www.redirect.com") < 0):
1673 return False
1675 dest = "http://www.destination.com/foo.js"
1677 self.send_response(302) # moved temporarily
1678 self.send_header('Location', dest)
1679 self.send_header('Connection', 'close')
1680 self.end_headers()
1681 return True
1683 def ServerAuthConnectHandler(self):
1684 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1685 response doesn't make sense because the proxy server cannot request
1686 server authentication."""
1688 if (self.path.find("www.server-auth.com") < 0):
1689 return False
1691 challenge = 'Basic realm="WallyWorld"'
1693 self.send_response(401) # unauthorized
1694 self.send_header('WWW-Authenticate', challenge)
1695 self.send_header('Connection', 'close')
1696 self.end_headers()
1697 return True
1699 def DefaultConnectResponseHandler(self):
1700 """This is the catch-all response handler for CONNECT requests that aren't
1701 handled by one of the special handlers above. Real Web servers respond
1702 with 400 to CONNECT requests."""
1704 contents = "Your client has issued a malformed or illegal request."
1705 self.send_response(400) # bad request
1706 self.send_header('Content-Type', 'text/html')
1707 self.send_header('Content-Length', len(contents))
1708 self.end_headers()
1709 self.wfile.write(contents)
1710 return True
1712 # called by the redirect handling function when there is no parameter
1713 def sendRedirectHelp(self, redirect_name):
1714 self.send_response(200)
1715 self.send_header('Content-Type', 'text/html')
1716 self.end_headers()
1717 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1718 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1719 self.wfile.write('</body></html>')
1721 # called by chunked handling function
1722 def sendChunkHelp(self, chunk):
1723 # Each chunk consists of: chunk size (hex), CRLF, chunk body, CRLF
1724 self.wfile.write('%X\r\n' % len(chunk))
1725 self.wfile.write(chunk)
1726 self.wfile.write('\r\n')
1729 class OCSPHandler(testserver_base.BasePageHandler):
1730 def __init__(self, request, client_address, socket_server):
1731 handlers = [self.OCSPResponse]
1732 self.ocsp_response = socket_server.ocsp_response
1733 testserver_base.BasePageHandler.__init__(self, request, client_address,
1734 socket_server, [], handlers, [],
1735 handlers, [])
1737 def OCSPResponse(self):
1738 self.send_response(200)
1739 self.send_header('Content-Type', 'application/ocsp-response')
1740 self.send_header('Content-Length', str(len(self.ocsp_response)))
1741 self.end_headers()
1743 self.wfile.write(self.ocsp_response)
1746 class TCPEchoHandler(SocketServer.BaseRequestHandler):
1747 """The RequestHandler class for TCP echo server.
1749 It is instantiated once per connection to the server, and overrides the
1750 handle() method to implement communication to the client.
1753 def handle(self):
1754 """Handles the request from the client and constructs a response."""
1756 data = self.request.recv(65536).strip()
1757 # Verify the "echo request" message received from the client. Send back
1758 # "echo response" message if "echo request" message is valid.
1759 try:
1760 return_data = echo_message.GetEchoResponseData(data)
1761 if not return_data:
1762 return
1763 except ValueError:
1764 return
1766 self.request.send(return_data)
1769 class UDPEchoHandler(SocketServer.BaseRequestHandler):
1770 """The RequestHandler class for UDP echo server.
1772 It is instantiated once per connection to the server, and overrides the
1773 handle() method to implement communication to the client.
1776 def handle(self):
1777 """Handles the request from the client and constructs a response."""
1779 data = self.request[0].strip()
1780 request_socket = self.request[1]
1781 # Verify the "echo request" message received from the client. Send back
1782 # "echo response" message if "echo request" message is valid.
1783 try:
1784 return_data = echo_message.GetEchoResponseData(data)
1785 if not return_data:
1786 return
1787 except ValueError:
1788 return
1789 request_socket.sendto(return_data, self.client_address)
1792 class BasicAuthProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
1793 """A request handler that behaves as a proxy server which requires
1794 basic authentication. Only CONNECT, GET and HEAD is supported for now.
1797 _AUTH_CREDENTIAL = 'Basic Zm9vOmJhcg==' # foo:bar
1799 def parse_request(self):
1800 """Overrides parse_request to check credential."""
1802 if not BaseHTTPServer.BaseHTTPRequestHandler.parse_request(self):
1803 return False
1805 auth = self.headers.getheader('Proxy-Authorization')
1806 if auth != self._AUTH_CREDENTIAL:
1807 self.send_response(407)
1808 self.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"')
1809 self.end_headers()
1810 return False
1812 return True
1814 def _start_read_write(self, sock):
1815 sock.setblocking(0)
1816 self.request.setblocking(0)
1817 rlist = [self.request, sock]
1818 while True:
1819 ready_sockets, _unused, errors = select.select(rlist, [], [])
1820 if errors:
1821 self.send_response(500)
1822 self.end_headers()
1823 return
1824 for s in ready_sockets:
1825 received = s.recv(1024)
1826 if len(received) == 0:
1827 return
1828 if s == self.request:
1829 other = sock
1830 else:
1831 other = self.request
1832 other.send(received)
1834 def _do_common_method(self):
1835 url = urlparse.urlparse(self.path)
1836 port = url.port
1837 if not port:
1838 if url.scheme == 'http':
1839 port = 80
1840 elif url.scheme == 'https':
1841 port = 443
1842 if not url.hostname or not port:
1843 self.send_response(400)
1844 self.end_headers()
1845 return
1847 if len(url.path) == 0:
1848 path = '/'
1849 else:
1850 path = url.path
1851 if len(url.query) > 0:
1852 path = '%s?%s' % (url.path, url.query)
1854 sock = None
1855 try:
1856 sock = socket.create_connection((url.hostname, port))
1857 sock.send('%s %s %s\r\n' % (
1858 self.command, path, self.protocol_version))
1859 for header in self.headers.headers:
1860 header = header.strip()
1861 if (header.lower().startswith('connection') or
1862 header.lower().startswith('proxy')):
1863 continue
1864 sock.send('%s\r\n' % header)
1865 sock.send('\r\n')
1866 self._start_read_write(sock)
1867 except Exception:
1868 self.send_response(500)
1869 self.end_headers()
1870 finally:
1871 if sock is not None:
1872 sock.close()
1874 def do_CONNECT(self):
1875 try:
1876 pos = self.path.rfind(':')
1877 host = self.path[:pos]
1878 port = int(self.path[pos+1:])
1879 except Exception:
1880 self.send_response(400)
1881 self.end_headers()
1883 try:
1884 sock = socket.create_connection((host, port))
1885 self.send_response(200, 'Connection established')
1886 self.end_headers()
1887 self._start_read_write(sock)
1888 except Exception:
1889 self.send_response(500)
1890 self.end_headers()
1891 finally:
1892 sock.close()
1894 def do_GET(self):
1895 self._do_common_method()
1897 def do_HEAD(self):
1898 self._do_common_method()
1901 class ServerRunner(testserver_base.TestServerRunner):
1902 """TestServerRunner for the net test servers."""
1904 def __init__(self):
1905 super(ServerRunner, self).__init__()
1906 self.__ocsp_server = None
1908 def __make_data_dir(self):
1909 if self.options.data_dir:
1910 if not os.path.isdir(self.options.data_dir):
1911 raise testserver_base.OptionError('specified data dir not found: ' +
1912 self.options.data_dir + ' exiting...')
1913 my_data_dir = self.options.data_dir
1914 else:
1915 # Create the default path to our data dir, relative to the exe dir.
1916 my_data_dir = os.path.join(BASE_DIR, "..", "..", "..", "..",
1917 "test", "data")
1919 #TODO(ibrar): Must use Find* funtion defined in google\tools
1920 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1922 return my_data_dir
1924 def create_server(self, server_data):
1925 port = self.options.port
1926 host = self.options.host
1928 if self.options.server_type == SERVER_HTTP:
1929 if self.options.https:
1930 pem_cert_and_key = None
1931 if self.options.cert_and_key_file:
1932 if not os.path.isfile(self.options.cert_and_key_file):
1933 raise testserver_base.OptionError(
1934 'specified server cert file not found: ' +
1935 self.options.cert_and_key_file + ' exiting...')
1936 pem_cert_and_key = file(self.options.cert_and_key_file, 'r').read()
1937 else:
1938 # generate a new certificate and run an OCSP server for it.
1939 self.__ocsp_server = OCSPServer((host, 0), OCSPHandler)
1940 print ('OCSP server started on %s:%d...' %
1941 (host, self.__ocsp_server.server_port))
1943 ocsp_der = None
1944 ocsp_state = None
1946 if self.options.ocsp == 'ok':
1947 ocsp_state = minica.OCSP_STATE_GOOD
1948 elif self.options.ocsp == 'revoked':
1949 ocsp_state = minica.OCSP_STATE_REVOKED
1950 elif self.options.ocsp == 'invalid':
1951 ocsp_state = minica.OCSP_STATE_INVALID
1952 elif self.options.ocsp == 'unauthorized':
1953 ocsp_state = minica.OCSP_STATE_UNAUTHORIZED
1954 elif self.options.ocsp == 'unknown':
1955 ocsp_state = minica.OCSP_STATE_UNKNOWN
1956 else:
1957 raise testserver_base.OptionError('unknown OCSP status: ' +
1958 self.options.ocsp_status)
1960 (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP(
1961 subject = "127.0.0.1",
1962 ocsp_url = ("http://%s:%d/ocsp" %
1963 (host, self.__ocsp_server.server_port)),
1964 ocsp_state = ocsp_state,
1965 serial = self.options.cert_serial)
1967 self.__ocsp_server.ocsp_response = ocsp_der
1969 for ca_cert in self.options.ssl_client_ca:
1970 if not os.path.isfile(ca_cert):
1971 raise testserver_base.OptionError(
1972 'specified trusted client CA file not found: ' + ca_cert +
1973 ' exiting...')
1975 stapled_ocsp_response = None
1976 if self.__ocsp_server and self.options.staple_ocsp_response:
1977 stapled_ocsp_response = self.__ocsp_server.ocsp_response
1979 server = HTTPSServer((host, port), TestPageHandler, pem_cert_and_key,
1980 self.options.ssl_client_auth,
1981 self.options.ssl_client_ca,
1982 self.options.ssl_client_cert_type,
1983 self.options.ssl_bulk_cipher,
1984 self.options.ssl_key_exchange,
1985 self.options.enable_npn,
1986 self.options.record_resume,
1987 self.options.tls_intolerant,
1988 self.options.tls_intolerance_type,
1989 self.options.signed_cert_timestamps_tls_ext.decode(
1990 "base64"),
1991 self.options.fallback_scsv,
1992 stapled_ocsp_response,
1993 self.options.disable_session_cache)
1994 print 'HTTPS server started on https://%s:%d...' % \
1995 (host, server.server_port)
1996 else:
1997 server = HTTPServer((host, port), TestPageHandler)
1998 print 'HTTP server started on http://%s:%d...' % \
1999 (host, server.server_port)
2001 server.data_dir = self.__make_data_dir()
2002 server.file_root_url = self.options.file_root_url
2003 server_data['port'] = server.server_port
2004 elif self.options.server_type == SERVER_WEBSOCKET:
2005 # Launch pywebsocket via WebSocketServer.
2006 logger = logging.getLogger()
2007 logger.addHandler(logging.StreamHandler())
2008 # TODO(toyoshim): Remove following os.chdir. Currently this operation
2009 # is required to work correctly. It should be fixed from pywebsocket side.
2010 os.chdir(self.__make_data_dir())
2011 websocket_options = WebSocketOptions(host, port, '.')
2012 scheme = "ws"
2013 if self.options.cert_and_key_file:
2014 scheme = "wss"
2015 websocket_options.use_tls = True
2016 websocket_options.private_key = self.options.cert_and_key_file
2017 websocket_options.certificate = self.options.cert_and_key_file
2018 if self.options.ssl_client_auth:
2019 websocket_options.tls_client_cert_optional = False
2020 websocket_options.tls_client_auth = True
2021 if len(self.options.ssl_client_ca) != 1:
2022 raise testserver_base.OptionError(
2023 'one trusted client CA file should be specified')
2024 if not os.path.isfile(self.options.ssl_client_ca[0]):
2025 raise testserver_base.OptionError(
2026 'specified trusted client CA file not found: ' +
2027 self.options.ssl_client_ca[0] + ' exiting...')
2028 websocket_options.tls_client_ca = self.options.ssl_client_ca[0]
2029 server = WebSocketServer(websocket_options)
2030 print 'WebSocket server started on %s://%s:%d...' % \
2031 (scheme, host, server.server_port)
2032 server_data['port'] = server.server_port
2033 websocket_options.use_basic_auth = self.options.ws_basic_auth
2034 elif self.options.server_type == SERVER_TCP_ECHO:
2035 # Used for generating the key (randomly) that encodes the "echo request"
2036 # message.
2037 random.seed()
2038 server = TCPEchoServer((host, port), TCPEchoHandler)
2039 print 'Echo TCP server started on port %d...' % server.server_port
2040 server_data['port'] = server.server_port
2041 elif self.options.server_type == SERVER_UDP_ECHO:
2042 # Used for generating the key (randomly) that encodes the "echo request"
2043 # message.
2044 random.seed()
2045 server = UDPEchoServer((host, port), UDPEchoHandler)
2046 print 'Echo UDP server started on port %d...' % server.server_port
2047 server_data['port'] = server.server_port
2048 elif self.options.server_type == SERVER_BASIC_AUTH_PROXY:
2049 server = HTTPServer((host, port), BasicAuthProxyRequestHandler)
2050 print 'BasicAuthProxy server started on port %d...' % server.server_port
2051 server_data['port'] = server.server_port
2052 elif self.options.server_type == SERVER_FTP:
2053 my_data_dir = self.__make_data_dir()
2055 # Instantiate a dummy authorizer for managing 'virtual' users
2056 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
2058 # Define a new user having full r/w permissions and a read-only
2059 # anonymous user
2060 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
2062 authorizer.add_anonymous(my_data_dir)
2064 # Instantiate FTP handler class
2065 ftp_handler = pyftpdlib.ftpserver.FTPHandler
2066 ftp_handler.authorizer = authorizer
2068 # Define a customized banner (string returned when client connects)
2069 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
2070 pyftpdlib.ftpserver.__ver__)
2072 # Instantiate FTP server class and listen to address:port
2073 server = pyftpdlib.ftpserver.FTPServer((host, port), ftp_handler)
2074 server_data['port'] = server.socket.getsockname()[1]
2075 print 'FTP server started on port %d...' % server_data['port']
2076 else:
2077 raise testserver_base.OptionError('unknown server type' +
2078 self.options.server_type)
2080 return server
2082 def run_server(self):
2083 if self.__ocsp_server:
2084 self.__ocsp_server.serve_forever_on_thread()
2086 testserver_base.TestServerRunner.run_server(self)
2088 if self.__ocsp_server:
2089 self.__ocsp_server.stop_serving()
2091 def add_options(self):
2092 testserver_base.TestServerRunner.add_options(self)
2093 self.option_parser.add_option('--disable-session-cache',
2094 action='store_true',
2095 dest='disable_session_cache',
2096 help='tells the server to disable the'
2097 'TLS session cache.')
2098 self.option_parser.add_option('-f', '--ftp', action='store_const',
2099 const=SERVER_FTP, default=SERVER_HTTP,
2100 dest='server_type',
2101 help='start up an FTP server.')
2102 self.option_parser.add_option('--tcp-echo', action='store_const',
2103 const=SERVER_TCP_ECHO, default=SERVER_HTTP,
2104 dest='server_type',
2105 help='start up a tcp echo server.')
2106 self.option_parser.add_option('--udp-echo', action='store_const',
2107 const=SERVER_UDP_ECHO, default=SERVER_HTTP,
2108 dest='server_type',
2109 help='start up a udp echo server.')
2110 self.option_parser.add_option('--basic-auth-proxy', action='store_const',
2111 const=SERVER_BASIC_AUTH_PROXY,
2112 default=SERVER_HTTP, dest='server_type',
2113 help='start up a proxy server which requires '
2114 'basic authentication.')
2115 self.option_parser.add_option('--websocket', action='store_const',
2116 const=SERVER_WEBSOCKET, default=SERVER_HTTP,
2117 dest='server_type',
2118 help='start up a WebSocket server.')
2119 self.option_parser.add_option('--https', action='store_true',
2120 dest='https', help='Specify that https '
2121 'should be used.')
2122 self.option_parser.add_option('--cert-and-key-file',
2123 dest='cert_and_key_file', help='specify the '
2124 'path to the file containing the certificate '
2125 'and private key for the server in PEM '
2126 'format')
2127 self.option_parser.add_option('--ocsp', dest='ocsp', default='ok',
2128 help='The type of OCSP response generated '
2129 'for the automatically generated '
2130 'certificate. One of [ok,revoked,invalid]')
2131 self.option_parser.add_option('--cert-serial', dest='cert_serial',
2132 default=0, type=int,
2133 help='If non-zero then the generated '
2134 'certificate will have this serial number')
2135 self.option_parser.add_option('--tls-intolerant', dest='tls_intolerant',
2136 default='0', type='int',
2137 help='If nonzero, certain TLS connections '
2138 'will be aborted in order to test version '
2139 'fallback. 1 means all TLS versions will be '
2140 'aborted. 2 means TLS 1.1 or higher will be '
2141 'aborted. 3 means TLS 1.2 or higher will be '
2142 'aborted.')
2143 self.option_parser.add_option('--tls-intolerance-type',
2144 dest='tls_intolerance_type',
2145 default="alert",
2146 help='Controls how the server reacts to a '
2147 'TLS version it is intolerant to. Valid '
2148 'values are "alert", "close", and "reset".')
2149 self.option_parser.add_option('--signed-cert-timestamps-tls-ext',
2150 dest='signed_cert_timestamps_tls_ext',
2151 default='',
2152 help='Base64 encoded SCT list. If set, '
2153 'server will respond with a '
2154 'signed_certificate_timestamp TLS extension '
2155 'whenever the client supports it.')
2156 self.option_parser.add_option('--fallback-scsv', dest='fallback_scsv',
2157 default=False, const=True,
2158 action='store_const',
2159 help='If given, TLS_FALLBACK_SCSV support '
2160 'will be enabled. This causes the server to '
2161 'reject fallback connections from compatible '
2162 'clients (e.g. Chrome).')
2163 self.option_parser.add_option('--staple-ocsp-response',
2164 dest='staple_ocsp_response',
2165 default=False, action='store_true',
2166 help='If set, server will staple the OCSP '
2167 'response whenever OCSP is on and the client '
2168 'supports OCSP stapling.')
2169 self.option_parser.add_option('--https-record-resume',
2170 dest='record_resume', const=True,
2171 default=False, action='store_const',
2172 help='Record resumption cache events rather '
2173 'than resuming as normal. Allows the use of '
2174 'the /ssl-session-cache request')
2175 self.option_parser.add_option('--ssl-client-auth', action='store_true',
2176 help='Require SSL client auth on every '
2177 'connection.')
2178 self.option_parser.add_option('--ssl-client-ca', action='append',
2179 default=[], help='Specify that the client '
2180 'certificate request should include the CA '
2181 'named in the subject of the DER-encoded '
2182 'certificate contained in the specified '
2183 'file. This option may appear multiple '
2184 'times, indicating multiple CA names should '
2185 'be sent in the request.')
2186 self.option_parser.add_option('--ssl-client-cert-type', action='append',
2187 default=[], help='Specify that the client '
2188 'certificate request should include the '
2189 'specified certificate_type value. This '
2190 'option may appear multiple times, '
2191 'indicating multiple values should be send '
2192 'in the request. Valid values are '
2193 '"rsa_sign", "dss_sign", and "ecdsa_sign". '
2194 'If omitted, "rsa_sign" will be used.')
2195 self.option_parser.add_option('--ssl-bulk-cipher', action='append',
2196 help='Specify the bulk encryption '
2197 'algorithm(s) that will be accepted by the '
2198 'SSL server. Valid values are "aes256", '
2199 '"aes128", "3des", "rc4". If omitted, all '
2200 'algorithms will be used. This option may '
2201 'appear multiple times, indicating '
2202 'multiple algorithms should be enabled.');
2203 self.option_parser.add_option('--ssl-key-exchange', action='append',
2204 help='Specify the key exchange algorithm(s)'
2205 'that will be accepted by the SSL server. '
2206 'Valid values are "rsa", "dhe_rsa". If '
2207 'omitted, all algorithms will be used. This '
2208 'option may appear multiple times, '
2209 'indicating multiple algorithms should be '
2210 'enabled.');
2211 # TODO(davidben): Add ALPN support to tlslite.
2212 self.option_parser.add_option('--enable-npn', dest='enable_npn',
2213 default=False, const=True,
2214 action='store_const',
2215 help='Enable server support for the NPN '
2216 'extension. The server will advertise '
2217 'support for exactly one protocol, http/1.1')
2218 self.option_parser.add_option('--file-root-url', default='/files/',
2219 help='Specify a root URL for files served.')
2220 # TODO(ricea): Generalize this to support basic auth for HTTP too.
2221 self.option_parser.add_option('--ws-basic-auth', action='store_true',
2222 dest='ws_basic_auth',
2223 help='Enable basic-auth for WebSocket')
2226 if __name__ == '__main__':
2227 sys.exit(ServerRunner().main())