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
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.
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.
47 os.remove(os.path.join(ROOT_DIR, 'third_party', 'tlslite',
48 'tlslite', 'utils', 'hmac.pyc'))
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
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
63 mod_pywebsocket.standalone.ssl = ssl
65 import pyftpdlib.ftpserver
71 import testserver_base
77 SERVER_BASIC_AUTH_PROXY = 4
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
90 self.websock_handlers = data_dir
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
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
108 class RecordingSSLSessionCache(object):
109 """RecordingSSLSessionCache acts as a TLS session cache and maintains a log of
110 lookups and inserts in order to test session cache behaviours."""
115 def __getitem__(self, sessionID):
116 self.log.append(('lookup', sessionID))
119 def __setitem__(self, sessionID, session):
120 self.log.append(('insert', sessionID))
123 class HTTPServer(testserver_base.ClientRestrictingServerMixIn,
124 testserver_base.BrokenPipeHandlerMixIn,
125 testserver_base.StoppableHTTPServer):
126 """This is a specialization of StoppableHTTPServer that adds client
131 class OCSPServer(testserver_base.ClientRestrictingServerMixIn,
132 testserver_base.BrokenPipeHandlerMixIn,
133 BaseHTTPServer.HTTPServer):
134 """This is a specialization of HTTPServer that serves an
137 def serve_forever_on_thread(self):
138 self.thread = threading.Thread(target = self.serve_forever,
139 name = "OCSPServerThread")
142 def stop_serving(self):
147 class HTTPSServer(tlslite.api.TLSSocketServerMixIn,
148 testserver_base.ClientRestrictingServerMixIn,
149 testserver_base.BrokenPipeHandlerMixIn,
150 testserver_base.StoppableHTTPServer):
151 """This is a specialization of StoppableHTTPServer that add https support and
152 client verification."""
154 def __init__(self, server_address, request_hander_class, pem_cert_and_key,
155 ssl_client_auth, ssl_client_cas,
156 ssl_bulk_ciphers, ssl_key_exchanges, enable_npn,
157 record_resume_info, tls_intolerant, signed_cert_timestamps,
158 fallback_scsv_enabled, ocsp_response):
159 self.cert_chain = tlslite.api.X509CertChain()
160 self.cert_chain.parsePemList(pem_cert_and_key)
161 # Force using only python implementation - otherwise behavior is different
162 # depending on whether m2crypto Python module is present (error is thrown
163 # when it is). m2crypto uses a C (based on OpenSSL) implementation under
165 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key,
167 implementations=['python'])
168 self.ssl_client_auth = ssl_client_auth
169 self.ssl_client_cas = []
171 self.next_protos = ['http/1.1']
173 self.next_protos = None
174 if tls_intolerant == 0:
175 self.tls_intolerant = None
177 self.tls_intolerant = (3, tls_intolerant)
178 self.signed_cert_timestamps = signed_cert_timestamps
179 self.fallback_scsv_enabled = fallback_scsv_enabled
180 self.ocsp_response = ocsp_response
182 for ca_file in ssl_client_cas:
183 s = open(ca_file).read()
184 x509 = tlslite.api.X509()
186 self.ssl_client_cas.append(x509.subject)
187 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
188 if ssl_bulk_ciphers is not None:
189 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
190 if ssl_key_exchanges is not None:
191 self.ssl_handshake_settings.keyExchangeNames = ssl_key_exchanges
193 if record_resume_info:
194 # If record_resume_info is true then we'll replace the session cache with
195 # an object that records the lookups and inserts that it sees.
196 self.session_cache = RecordingSSLSessionCache()
198 self.session_cache = tlslite.api.SessionCache()
199 testserver_base.StoppableHTTPServer.__init__(self,
201 request_hander_class)
203 def handshake(self, tlsConnection):
204 """Creates the SSL connection."""
207 self.tlsConnection = tlsConnection
208 tlsConnection.handshakeServer(certChain=self.cert_chain,
209 privateKey=self.private_key,
210 sessionCache=self.session_cache,
211 reqCert=self.ssl_client_auth,
212 settings=self.ssl_handshake_settings,
213 reqCAs=self.ssl_client_cas,
214 nextProtos=self.next_protos,
215 tlsIntolerant=self.tls_intolerant,
216 signedCertTimestamps=
217 self.signed_cert_timestamps,
218 fallbackSCSV=self.fallback_scsv_enabled,
219 ocspResponse = self.ocsp_response)
220 tlsConnection.ignoreAbruptClose = True
222 except tlslite.api.TLSAbruptCloseError:
223 # Ignore abrupt close.
225 except tlslite.api.TLSError, error:
226 print "Handshake failure:", str(error)
230 class FTPServer(testserver_base.ClientRestrictingServerMixIn,
231 pyftpdlib.ftpserver.FTPServer):
232 """This is a specialization of FTPServer that adds client verification."""
237 class TCPEchoServer(testserver_base.ClientRestrictingServerMixIn,
238 SocketServer.TCPServer):
239 """A TCP echo server that echoes back what it has received."""
241 def server_bind(self):
242 """Override server_bind to store the server name."""
244 SocketServer.TCPServer.server_bind(self)
245 host, port = self.socket.getsockname()[:2]
246 self.server_name = socket.getfqdn(host)
247 self.server_port = port
249 def serve_forever(self):
251 self.nonce_time = None
253 self.handle_request()
257 class UDPEchoServer(testserver_base.ClientRestrictingServerMixIn,
258 SocketServer.UDPServer):
259 """A UDP echo server that echoes back what it has received."""
261 def server_bind(self):
262 """Override server_bind to store the server name."""
264 SocketServer.UDPServer.server_bind(self)
265 host, port = self.socket.getsockname()[:2]
266 self.server_name = socket.getfqdn(host)
267 self.server_port = port
269 def serve_forever(self):
271 self.nonce_time = None
273 self.handle_request()
277 class TestPageHandler(testserver_base.BasePageHandler):
278 # Class variables to allow for persistence state between page handler
281 fail_precondition = {}
283 def __init__(self, request, client_address, socket_server):
285 self.RedirectConnectHandler,
286 self.ServerAuthConnectHandler,
287 self.DefaultConnectResponseHandler]
289 self.NoCacheMaxAgeTimeHandler,
290 self.NoCacheTimeHandler,
291 self.CacheTimeHandler,
292 self.CacheExpiresHandler,
293 self.CacheProxyRevalidateHandler,
294 self.CachePrivateHandler,
295 self.CachePublicHandler,
296 self.CacheSMaxAgeHandler,
297 self.CacheMustRevalidateHandler,
298 self.CacheMustRevalidateMaxAgeHandler,
299 self.CacheNoStoreHandler,
300 self.CacheNoStoreMaxAgeHandler,
301 self.CacheNoTransformHandler,
302 self.DownloadHandler,
303 self.DownloadFinishHandler,
305 self.EchoHeaderCache,
309 self.SetCookieHandler,
310 self.SetManyCookiesHandler,
311 self.ExpectAndSetCookieHandler,
312 self.SetHeaderHandler,
313 self.AuthBasicHandler,
314 self.AuthDigestHandler,
315 self.SlowServerHandler,
316 self.ChunkedServerHandler,
317 self.ContentTypeHandler,
318 self.NoContentHandler,
319 self.ServerRedirectHandler,
320 self.ClientRedirectHandler,
321 self.MultipartHandler,
322 self.GetSSLSessionCacheHandler,
323 self.SSLManySmallRecords,
325 self.CloseSocketHandler,
326 self.RangeResetHandler,
327 self.DefaultResponseHandler]
329 self.EchoTitleHandler,
331 self.PostOnlyFileHandler,
332 self.EchoMultipartPostHandler] + get_handlers
334 self.EchoTitleHandler,
335 self.EchoHandler] + get_handlers
338 self.DefaultResponseHandler]
341 'crx' : 'application/x-chrome-extension',
342 'exe' : 'application/octet-stream',
344 'jpeg' : 'image/jpeg',
345 'jpg' : 'image/jpeg',
346 'json': 'application/json',
347 'pdf' : 'application/pdf',
348 'txt' : 'text/plain',
352 self._default_mime_type = 'text/html'
354 testserver_base.BasePageHandler.__init__(self, request, client_address,
355 socket_server, connect_handlers,
356 get_handlers, head_handlers,
357 post_handlers, put_handlers)
359 def GetMIMETypeFromName(self, file_name):
360 """Returns the mime type for the specified file_name. So far it only looks
361 at the file extension."""
363 (_shortname, extension) = os.path.splitext(file_name.split("?")[0])
364 if len(extension) == 0:
366 return self._default_mime_type
368 # extension starts with a dot, so we need to remove it
369 return self._mime_types.get(extension[1:], self._default_mime_type)
371 def NoCacheMaxAgeTimeHandler(self):
372 """This request handler yields a page with the title set to the current
373 system time, and no caching requested."""
375 if not self._ShouldHandleRequest("/nocachetime/maxage"):
378 self.send_response(200)
379 self.send_header('Cache-Control', 'max-age=0')
380 self.send_header('Content-Type', 'text/html')
383 self.wfile.write('<html><head><title>%s</title></head></html>' %
388 def NoCacheTimeHandler(self):
389 """This request handler yields a page with the title set to the current
390 system time, and no caching requested."""
392 if not self._ShouldHandleRequest("/nocachetime"):
395 self.send_response(200)
396 self.send_header('Cache-Control', 'no-cache')
397 self.send_header('Content-Type', 'text/html')
400 self.wfile.write('<html><head><title>%s</title></head></html>' %
405 def CacheTimeHandler(self):
406 """This request handler yields a page with the title set to the current
407 system time, and allows caching for one minute."""
409 if not self._ShouldHandleRequest("/cachetime"):
412 self.send_response(200)
413 self.send_header('Cache-Control', 'max-age=60')
414 self.send_header('Content-Type', 'text/html')
417 self.wfile.write('<html><head><title>%s</title></head></html>' %
422 def CacheExpiresHandler(self):
423 """This request handler yields a page with the title set to the current
424 system time, and set the page to expire on 1 Jan 2099."""
426 if not self._ShouldHandleRequest("/cache/expires"):
429 self.send_response(200)
430 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
431 self.send_header('Content-Type', 'text/html')
434 self.wfile.write('<html><head><title>%s</title></head></html>' %
439 def CacheProxyRevalidateHandler(self):
440 """This request handler yields a page with the title set to the current
441 system time, and allows caching for 60 seconds"""
443 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
446 self.send_response(200)
447 self.send_header('Content-Type', 'text/html')
448 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
451 self.wfile.write('<html><head><title>%s</title></head></html>' %
456 def CachePrivateHandler(self):
457 """This request handler yields a page with the title set to the current
458 system time, and allows caching for 5 seconds."""
460 if not self._ShouldHandleRequest("/cache/private"):
463 self.send_response(200)
464 self.send_header('Content-Type', 'text/html')
465 self.send_header('Cache-Control', 'max-age=3, private')
468 self.wfile.write('<html><head><title>%s</title></head></html>' %
473 def CachePublicHandler(self):
474 """This request handler yields a page with the title set to the current
475 system time, and allows caching for 5 seconds."""
477 if not self._ShouldHandleRequest("/cache/public"):
480 self.send_response(200)
481 self.send_header('Content-Type', 'text/html')
482 self.send_header('Cache-Control', 'max-age=3, public')
485 self.wfile.write('<html><head><title>%s</title></head></html>' %
490 def CacheSMaxAgeHandler(self):
491 """This request handler yields a page with the title set to the current
492 system time, and does not allow for caching."""
494 if not self._ShouldHandleRequest("/cache/s-maxage"):
497 self.send_response(200)
498 self.send_header('Content-Type', 'text/html')
499 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
502 self.wfile.write('<html><head><title>%s</title></head></html>' %
507 def CacheMustRevalidateHandler(self):
508 """This request handler yields a page with the title set to the current
509 system time, and does not allow caching."""
511 if not self._ShouldHandleRequest("/cache/must-revalidate"):
514 self.send_response(200)
515 self.send_header('Content-Type', 'text/html')
516 self.send_header('Cache-Control', 'must-revalidate')
519 self.wfile.write('<html><head><title>%s</title></head></html>' %
524 def CacheMustRevalidateMaxAgeHandler(self):
525 """This request handler yields a page with the title set to the current
526 system time, and does not allow caching event though max-age of 60
527 seconds is specified."""
529 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
532 self.send_response(200)
533 self.send_header('Content-Type', 'text/html')
534 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
537 self.wfile.write('<html><head><title>%s</title></head></html>' %
542 def CacheNoStoreHandler(self):
543 """This request handler yields a page with the title set to the current
544 system time, and does not allow the page to be stored."""
546 if not self._ShouldHandleRequest("/cache/no-store"):
549 self.send_response(200)
550 self.send_header('Content-Type', 'text/html')
551 self.send_header('Cache-Control', 'no-store')
554 self.wfile.write('<html><head><title>%s</title></head></html>' %
559 def CacheNoStoreMaxAgeHandler(self):
560 """This request handler yields a page with the title set to the current
561 system time, and does not allow the page to be stored even though max-age
562 of 60 seconds is specified."""
564 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
567 self.send_response(200)
568 self.send_header('Content-Type', 'text/html')
569 self.send_header('Cache-Control', 'max-age=60, no-store')
572 self.wfile.write('<html><head><title>%s</title></head></html>' %
578 def CacheNoTransformHandler(self):
579 """This request handler yields a page with the title set to the current
580 system time, and does not allow the content to transformed during
581 user-agent caching"""
583 if not self._ShouldHandleRequest("/cache/no-transform"):
586 self.send_response(200)
587 self.send_header('Content-Type', 'text/html')
588 self.send_header('Cache-Control', 'no-transform')
591 self.wfile.write('<html><head><title>%s</title></head></html>' %
596 def EchoHeader(self):
597 """This handler echoes back the value of a specific request header."""
599 return self.EchoHeaderHelper("/echoheader")
601 def EchoHeaderCache(self):
602 """This function echoes back the value of a specific request header while
603 allowing caching for 16 hours."""
605 return self.EchoHeaderHelper("/echoheadercache")
607 def EchoHeaderHelper(self, echo_header):
608 """This function echoes back the value of the request header passed in."""
610 if not self._ShouldHandleRequest(echo_header):
613 query_char = self.path.find('?')
615 header_name = self.path[query_char+1:]
617 self.send_response(200)
618 self.send_header('Content-Type', 'text/plain')
619 if echo_header == '/echoheadercache':
620 self.send_header('Cache-control', 'max-age=60000')
622 self.send_header('Cache-control', 'no-cache')
623 # insert a vary header to properly indicate that the cachability of this
624 # request is subject to value of the request header being echoed.
625 if len(header_name) > 0:
626 self.send_header('Vary', header_name)
629 if len(header_name) > 0:
630 self.wfile.write(self.headers.getheader(header_name))
634 def ReadRequestBody(self):
635 """This function reads the body of the current HTTP request, handling
636 both plain and chunked transfer encoded requests."""
638 if self.headers.getheader('transfer-encoding') != 'chunked':
639 length = int(self.headers.getheader('content-length'))
640 return self.rfile.read(length)
642 # Read the request body as chunks.
645 line = self.rfile.readline()
646 length = int(line, 16)
648 self.rfile.readline()
650 body += self.rfile.read(length)
654 def EchoHandler(self):
655 """This handler just echoes back the payload of the request, for testing
658 if not self._ShouldHandleRequest("/echo"):
661 self.send_response(200)
662 self.send_header('Content-Type', 'text/html')
664 self.wfile.write(self.ReadRequestBody())
667 def EchoTitleHandler(self):
668 """This handler is like Echo, but sets the page title to the request."""
670 if not self._ShouldHandleRequest("/echotitle"):
673 self.send_response(200)
674 self.send_header('Content-Type', 'text/html')
676 request = self.ReadRequestBody()
677 self.wfile.write('<html><head><title>')
678 self.wfile.write(request)
679 self.wfile.write('</title></head></html>')
682 def EchoAllHandler(self):
683 """This handler yields a (more) human-readable page listing information
684 about the request header & contents."""
686 if not self._ShouldHandleRequest("/echoall"):
689 self.send_response(200)
690 self.send_header('Content-Type', 'text/html')
692 self.wfile.write('<html><head><style>'
693 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
694 '</style></head><body>'
695 '<div style="float: right">'
696 '<a href="/echo">back to referring page</a></div>'
697 '<h1>Request Body:</h1><pre>')
699 if self.command == 'POST' or self.command == 'PUT':
700 qs = self.ReadRequestBody()
701 params = cgi.parse_qs(qs, keep_blank_values=1)
704 self.wfile.write('%s=%s\n' % (param, params[param][0]))
706 self.wfile.write('</pre>')
708 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
710 self.wfile.write('</body></html>')
713 def EchoMultipartPostHandler(self):
714 """This handler echoes received multipart post data as json format."""
716 if not (self._ShouldHandleRequest("/echomultipartpost") or
717 self._ShouldHandleRequest("/searchbyimage")):
720 content_type, parameters = cgi.parse_header(
721 self.headers.getheader('content-type'))
722 if content_type == 'multipart/form-data':
723 post_multipart = cgi.parse_multipart(self.rfile, parameters)
724 elif content_type == 'application/x-www-form-urlencoded':
725 raise Exception('POST by application/x-www-form-urlencoded is '
730 # Since the data can be binary, we encode them by base64.
731 post_multipart_base64_encoded = {}
732 for field, values in post_multipart.items():
733 post_multipart_base64_encoded[field] = [base64.b64encode(value)
736 result = {'POST_multipart' : post_multipart_base64_encoded}
738 self.send_response(200)
739 self.send_header("Content-type", "text/plain")
741 self.wfile.write(json.dumps(result, indent=2, sort_keys=False))
744 def DownloadHandler(self):
745 """This handler sends a downloadable file with or without reporting
748 if self.path.startswith("/download-unknown-size"):
750 elif self.path.startswith("/download-known-size"):
756 # The test which uses this functionality is attempting to send
757 # small chunks of data to the client. Use a fairly large buffer
758 # so that we'll fill chrome's IO buffer enough to force it to
759 # actually write the data.
760 # See also the comments in the client-side of this test in
763 size_chunk1 = 35*1024
764 size_chunk2 = 10*1024
766 self.send_response(200)
767 self.send_header('Content-Type', 'application/octet-stream')
768 self.send_header('Cache-Control', 'max-age=0')
770 self.send_header('Content-Length', size_chunk1 + size_chunk2)
773 # First chunk of data:
774 self.wfile.write("*" * size_chunk1)
777 # handle requests until one of them clears this flag.
778 self.server.wait_for_download = True
779 while self.server.wait_for_download:
780 self.server.handle_request()
782 # Second chunk of data:
783 self.wfile.write("*" * size_chunk2)
786 def DownloadFinishHandler(self):
787 """This handler just tells the server to finish the current download."""
789 if not self._ShouldHandleRequest("/download-finish"):
792 self.server.wait_for_download = False
793 self.send_response(200)
794 self.send_header('Content-Type', 'text/html')
795 self.send_header('Cache-Control', 'max-age=0')
799 def _ReplaceFileData(self, data, query_parameters):
800 """Replaces matching substrings in a file.
802 If the 'replace_text' URL query parameter is present, it is expected to be
803 of the form old_text:new_text, which indicates that any old_text strings in
804 the file are replaced with new_text. Multiple 'replace_text' parameters may
807 If the parameters are not present, |data| is returned.
810 query_dict = cgi.parse_qs(query_parameters)
811 replace_text_values = query_dict.get('replace_text', [])
812 for replace_text_value in replace_text_values:
813 replace_text_args = replace_text_value.split(':')
814 if len(replace_text_args) != 2:
816 'replace_text must be of form old_text:new_text. Actual value: %s' %
818 old_text_b64, new_text_b64 = replace_text_args
819 old_text = base64.urlsafe_b64decode(old_text_b64)
820 new_text = base64.urlsafe_b64decode(new_text_b64)
821 data = data.replace(old_text, new_text)
824 def ZipFileHandler(self):
825 """This handler sends the contents of the requested file in compressed form.
826 Can pass in a parameter that specifies that the content length be
827 C - the compressed size (OK),
828 U - the uncompressed size (Non-standard, but handled),
829 S - less than compressed (OK because we keep going),
830 M - larger than compressed but less than uncompressed (an error),
831 L - larger than uncompressed (an error)
832 Example: compressedfiles/Picture_1.doc?C
835 prefix = "/compressedfiles/"
836 if not self.path.startswith(prefix):
839 # Consume a request body if present.
840 if self.command == 'POST' or self.command == 'PUT' :
841 self.ReadRequestBody()
843 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
845 if not query in ('C', 'U', 'S', 'M', 'L'):
848 sub_path = url_path[len(prefix):]
849 entries = sub_path.split('/')
850 file_path = os.path.join(self.server.data_dir, *entries)
851 if os.path.isdir(file_path):
852 file_path = os.path.join(file_path, 'index.html')
854 if not os.path.isfile(file_path):
855 print "File not found " + sub_path + " full path:" + file_path
859 f = open(file_path, "rb")
861 uncompressed_len = len(data)
865 data = zlib.compress(data)
866 compressed_len = len(data)
868 content_length = compressed_len
870 content_length = uncompressed_len
872 content_length = compressed_len / 2
874 content_length = (compressed_len + uncompressed_len) / 2
876 content_length = compressed_len + uncompressed_len
878 self.send_response(200)
879 self.send_header('Content-Type', 'application/msword')
880 self.send_header('Content-encoding', 'deflate')
881 self.send_header('Connection', 'close')
882 self.send_header('Content-Length', content_length)
883 self.send_header('ETag', '\'' + file_path + '\'')
886 self.wfile.write(data)
890 def FileHandler(self):
891 """This handler sends the contents of the requested file. Wow, it's like
894 prefix = self.server.file_root_url
895 if not self.path.startswith(prefix):
897 return self._FileHandlerHelper(prefix)
899 def PostOnlyFileHandler(self):
900 """This handler sends the contents of the requested file on a POST."""
902 prefix = urlparse.urljoin(self.server.file_root_url, 'post/')
903 if not self.path.startswith(prefix):
905 return self._FileHandlerHelper(prefix)
907 def _FileHandlerHelper(self, prefix):
909 if self.command == 'POST' or self.command == 'PUT':
910 # Consume a request body if present.
911 request_body = self.ReadRequestBody()
913 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
914 query_dict = cgi.parse_qs(query)
916 expected_body = query_dict.get('expected_body', [])
917 if expected_body and request_body not in expected_body:
918 self.send_response(404)
923 expected_headers = query_dict.get('expected_headers', [])
924 for expected_header in expected_headers:
925 header_name, expected_value = expected_header.split(':')
926 if self.headers.getheader(header_name) != expected_value:
927 self.send_response(404)
932 sub_path = url_path[len(prefix):]
933 entries = sub_path.split('/')
934 file_path = os.path.join(self.server.data_dir, *entries)
935 if os.path.isdir(file_path):
936 file_path = os.path.join(file_path, 'index.html')
938 if not os.path.isfile(file_path):
939 print "File not found " + sub_path + " full path:" + file_path
943 f = open(file_path, "rb")
947 data = self._ReplaceFileData(data, query)
949 old_protocol_version = self.protocol_version
951 # If file.mock-http-headers exists, it contains the headers we
952 # should send. Read them in and parse them.
953 headers_path = file_path + '.mock-http-headers'
954 if os.path.isfile(headers_path):
955 f = open(headers_path, "r")
958 response = f.readline()
959 http_major, http_minor, status_code = re.findall(
960 'HTTP/(\d+).(\d+) (\d+)', response)[0]
961 self.protocol_version = "HTTP/%s.%s" % (http_major, http_minor)
962 self.send_response(int(status_code))
965 header_values = re.findall('(\S+):\s*(.*)', line)
966 if len(header_values) > 0:
968 name, value = header_values[0]
969 self.send_header(name, value)
972 # Could be more generic once we support mime-type sniffing, but for
973 # now we need to set it explicitly.
975 range_header = self.headers.get('Range')
976 if range_header and range_header.startswith('bytes='):
977 # Note this doesn't handle all valid byte range_header values (i.e.
978 # left open ended ones), just enough for what we needed so far.
979 range_header = range_header[6:].split('-')
980 start = int(range_header[0])
982 end = int(range_header[1])
986 self.send_response(206)
987 content_range = ('bytes ' + str(start) + '-' + str(end) + '/' +
989 self.send_header('Content-Range', content_range)
990 data = data[start: end + 1]
992 self.send_response(200)
994 self.send_header('Content-Type', self.GetMIMETypeFromName(file_path))
995 self.send_header('Accept-Ranges', 'bytes')
996 self.send_header('Content-Length', len(data))
997 self.send_header('ETag', '\'' + file_path + '\'')
1000 if (self.command != 'HEAD'):
1001 self.wfile.write(data)
1003 self.protocol_version = old_protocol_version
1006 def SetCookieHandler(self):
1007 """This handler just sets a cookie, for testing cookie handling."""
1009 if not self._ShouldHandleRequest("/set-cookie"):
1012 query_char = self.path.find('?')
1013 if query_char != -1:
1014 cookie_values = self.path[query_char + 1:].split('&')
1016 cookie_values = ("",)
1017 self.send_response(200)
1018 self.send_header('Content-Type', 'text/html')
1019 for cookie_value in cookie_values:
1020 self.send_header('Set-Cookie', '%s' % cookie_value)
1022 for cookie_value in cookie_values:
1023 self.wfile.write('%s' % cookie_value)
1026 def SetManyCookiesHandler(self):
1027 """This handler just sets a given number of cookies, for testing handling
1028 of large numbers of cookies."""
1030 if not self._ShouldHandleRequest("/set-many-cookies"):
1033 query_char = self.path.find('?')
1034 if query_char != -1:
1035 num_cookies = int(self.path[query_char + 1:])
1038 self.send_response(200)
1039 self.send_header('', 'text/html')
1040 for _i in range(0, num_cookies):
1041 self.send_header('Set-Cookie', 'a=')
1043 self.wfile.write('%d cookies were sent' % num_cookies)
1046 def ExpectAndSetCookieHandler(self):
1047 """Expects some cookies to be sent, and if they are, sets more cookies.
1049 The expect parameter specifies a required cookie. May be specified multiple
1051 The set parameter specifies a cookie to set if all required cookies are
1052 preset. May be specified multiple times.
1053 The data parameter specifies the response body data to be returned."""
1055 if not self._ShouldHandleRequest("/expect-and-set-cookie"):
1058 _, _, _, _, query, _ = urlparse.urlparse(self.path)
1059 query_dict = cgi.parse_qs(query)
1061 if 'Cookie' in self.headers:
1062 cookie_header = self.headers.getheader('Cookie')
1063 cookies.update([s.strip() for s in cookie_header.split(';')])
1064 got_all_expected_cookies = True
1065 for expected_cookie in query_dict.get('expect', []):
1066 if expected_cookie not in cookies:
1067 got_all_expected_cookies = False
1068 self.send_response(200)
1069 self.send_header('Content-Type', 'text/html')
1070 if got_all_expected_cookies:
1071 for cookie_value in query_dict.get('set', []):
1072 self.send_header('Set-Cookie', '%s' % cookie_value)
1074 for data_value in query_dict.get('data', []):
1075 self.wfile.write(data_value)
1078 def SetHeaderHandler(self):
1079 """This handler sets a response header. Parameters are in the
1080 key%3A%20value&key2%3A%20value2 format."""
1082 if not self._ShouldHandleRequest("/set-header"):
1085 query_char = self.path.find('?')
1086 if query_char != -1:
1087 headers_values = self.path[query_char + 1:].split('&')
1089 headers_values = ("",)
1090 self.send_response(200)
1091 self.send_header('Content-Type', 'text/html')
1092 for header_value in headers_values:
1093 header_value = urllib.unquote(header_value)
1094 (key, value) = header_value.split(': ', 1)
1095 self.send_header(key, value)
1097 for header_value in headers_values:
1098 self.wfile.write('%s' % header_value)
1101 def AuthBasicHandler(self):
1102 """This handler tests 'Basic' authentication. It just sends a page with
1103 title 'user/pass' if you succeed."""
1105 if not self._ShouldHandleRequest("/auth-basic"):
1108 username = userpass = password = b64str = ""
1109 expected_password = 'secret'
1111 set_cookie_if_challenged = False
1113 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1114 query_params = cgi.parse_qs(query, True)
1115 if 'set-cookie-if-challenged' in query_params:
1116 set_cookie_if_challenged = True
1117 if 'password' in query_params:
1118 expected_password = query_params['password'][0]
1119 if 'realm' in query_params:
1120 realm = query_params['realm'][0]
1122 auth = self.headers.getheader('authorization')
1125 raise Exception('no auth')
1126 b64str = re.findall(r'Basic (\S+)', auth)[0]
1127 userpass = base64.b64decode(b64str)
1128 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
1129 if password != expected_password:
1130 raise Exception('wrong password')
1131 except Exception, e:
1132 # Authentication failed.
1133 self.send_response(401)
1134 self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
1135 self.send_header('Content-Type', 'text/html')
1136 if set_cookie_if_challenged:
1137 self.send_header('Set-Cookie', 'got_challenged=true')
1139 self.wfile.write('<html><head>')
1140 self.wfile.write('<title>Denied: %s</title>' % e)
1141 self.wfile.write('</head><body>')
1142 self.wfile.write('auth=%s<p>' % auth)
1143 self.wfile.write('b64str=%s<p>' % b64str)
1144 self.wfile.write('username: %s<p>' % username)
1145 self.wfile.write('userpass: %s<p>' % userpass)
1146 self.wfile.write('password: %s<p>' % password)
1147 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1148 self.wfile.write('</body></html>')
1151 # Authentication successful. (Return a cachable response to allow for
1152 # testing cached pages that require authentication.)
1153 old_protocol_version = self.protocol_version
1154 self.protocol_version = "HTTP/1.1"
1156 if_none_match = self.headers.getheader('if-none-match')
1157 if if_none_match == "abc":
1158 self.send_response(304)
1160 elif url_path.endswith(".gif"):
1161 # Using chrome/test/data/google/logo.gif as the test image
1162 test_image_path = ['google', 'logo.gif']
1163 gif_path = os.path.join(self.server.data_dir, *test_image_path)
1164 if not os.path.isfile(gif_path):
1165 self.send_error(404)
1166 self.protocol_version = old_protocol_version
1169 f = open(gif_path, "rb")
1173 self.send_response(200)
1174 self.send_header('Content-Type', 'image/gif')
1175 self.send_header('Cache-control', 'max-age=60000')
1176 self.send_header('Etag', 'abc')
1178 self.wfile.write(data)
1180 self.send_response(200)
1181 self.send_header('Content-Type', 'text/html')
1182 self.send_header('Cache-control', 'max-age=60000')
1183 self.send_header('Etag', 'abc')
1185 self.wfile.write('<html><head>')
1186 self.wfile.write('<title>%s/%s</title>' % (username, password))
1187 self.wfile.write('</head><body>')
1188 self.wfile.write('auth=%s<p>' % auth)
1189 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1190 self.wfile.write('</body></html>')
1192 self.protocol_version = old_protocol_version
1195 def GetNonce(self, force_reset=False):
1196 """Returns a nonce that's stable per request path for the server's lifetime.
1197 This is a fake implementation. A real implementation would only use a given
1198 nonce a single time (hence the name n-once). However, for the purposes of
1199 unittesting, we don't care about the security of the nonce.
1202 force_reset: Iff set, the nonce will be changed. Useful for testing the
1206 if force_reset or not self.server.nonce_time:
1207 self.server.nonce_time = time.time()
1208 return hashlib.md5('privatekey%s%d' %
1209 (self.path, self.server.nonce_time)).hexdigest()
1211 def AuthDigestHandler(self):
1212 """This handler tests 'Digest' authentication.
1214 It just sends a page with title 'user/pass' if you succeed.
1216 A stale response is sent iff "stale" is present in the request path.
1219 if not self._ShouldHandleRequest("/auth-digest"):
1222 stale = 'stale' in self.path
1223 nonce = self.GetNonce(force_reset=stale)
1224 opaque = hashlib.md5('opaque').hexdigest()
1228 auth = self.headers.getheader('authorization')
1232 raise Exception('no auth')
1233 if not auth.startswith('Digest'):
1234 raise Exception('not digest')
1235 # Pull out all the name="value" pairs as a dictionary.
1236 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1238 # Make sure it's all valid.
1239 if pairs['nonce'] != nonce:
1240 raise Exception('wrong nonce')
1241 if pairs['opaque'] != opaque:
1242 raise Exception('wrong opaque')
1244 # Check the 'response' value and make sure it matches our magic hash.
1245 # See http://www.ietf.org/rfc/rfc2617.txt
1246 hash_a1 = hashlib.md5(
1247 ':'.join([pairs['username'], realm, password])).hexdigest()
1248 hash_a2 = hashlib.md5(':'.join([self.command, pairs['uri']])).hexdigest()
1249 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
1250 response = hashlib.md5(':'.join([hash_a1, nonce, pairs['nc'],
1251 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1253 response = hashlib.md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
1255 if pairs['response'] != response:
1256 raise Exception('wrong password')
1257 except Exception, e:
1258 # Authentication failed.
1259 self.send_response(401)
1266 'opaque="%s"') % (realm, nonce, opaque)
1268 hdr += ', stale="TRUE"'
1269 self.send_header('WWW-Authenticate', hdr)
1270 self.send_header('Content-Type', 'text/html')
1272 self.wfile.write('<html><head>')
1273 self.wfile.write('<title>Denied: %s</title>' % e)
1274 self.wfile.write('</head><body>')
1275 self.wfile.write('auth=%s<p>' % auth)
1276 self.wfile.write('pairs=%s<p>' % pairs)
1277 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1278 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1279 self.wfile.write('</body></html>')
1282 # Authentication successful.
1283 self.send_response(200)
1284 self.send_header('Content-Type', 'text/html')
1286 self.wfile.write('<html><head>')
1287 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1288 self.wfile.write('</head><body>')
1289 self.wfile.write('auth=%s<p>' % auth)
1290 self.wfile.write('pairs=%s<p>' % pairs)
1291 self.wfile.write('</body></html>')
1295 def SlowServerHandler(self):
1296 """Wait for the user suggested time before responding. The syntax is
1297 /slow?0.5 to wait for half a second."""
1299 if not self._ShouldHandleRequest("/slow"):
1301 query_char = self.path.find('?')
1305 wait_sec = int(self.path[query_char + 1:])
1308 time.sleep(wait_sec)
1309 self.send_response(200)
1310 self.send_header('Content-Type', 'text/plain')
1312 self.wfile.write("waited %d seconds" % wait_sec)
1315 def ChunkedServerHandler(self):
1316 """Send chunked response. Allows to specify chunks parameters:
1317 - waitBeforeHeaders - ms to wait before sending headers
1318 - waitBetweenChunks - ms to wait between chunks
1319 - chunkSize - size of each chunk in bytes
1320 - chunksNumber - number of chunks
1321 Example: /chunked?waitBeforeHeaders=1000&chunkSize=5&chunksNumber=5
1322 waits one second, then sends headers and five chunks five bytes each."""
1324 if not self._ShouldHandleRequest("/chunked"):
1326 query_char = self.path.find('?')
1327 chunkedSettings = {'waitBeforeHeaders' : 0,
1328 'waitBetweenChunks' : 0,
1332 params = self.path[query_char + 1:].split('&')
1333 for param in params:
1334 keyValue = param.split('=')
1335 if len(keyValue) == 2:
1337 chunkedSettings[keyValue[0]] = int(keyValue[1])
1340 time.sleep(0.001 * chunkedSettings['waitBeforeHeaders'])
1341 self.protocol_version = 'HTTP/1.1' # Needed for chunked encoding
1342 self.send_response(200)
1343 self.send_header('Content-Type', 'text/plain')
1344 self.send_header('Connection', 'close')
1345 self.send_header('Transfer-Encoding', 'chunked')
1347 # Chunked encoding: sending all chunks, then final zero-length chunk and
1349 for i in range(0, chunkedSettings['chunksNumber']):
1351 time.sleep(0.001 * chunkedSettings['waitBetweenChunks'])
1352 self.sendChunkHelp('*' * chunkedSettings['chunkSize'])
1353 self.wfile.flush() # Keep in mind that we start flushing only after 1kb.
1354 self.sendChunkHelp('')
1357 def ContentTypeHandler(self):
1358 """Returns a string of html with the given content type. E.g.,
1359 /contenttype?text/css returns an html file with the Content-Type
1360 header set to text/css."""
1362 if not self._ShouldHandleRequest("/contenttype"):
1364 query_char = self.path.find('?')
1365 content_type = self.path[query_char + 1:].strip()
1366 if not content_type:
1367 content_type = 'text/html'
1368 self.send_response(200)
1369 self.send_header('Content-Type', content_type)
1371 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n")
1374 def NoContentHandler(self):
1375 """Returns a 204 No Content response."""
1377 if not self._ShouldHandleRequest("/nocontent"):
1379 self.send_response(204)
1383 def ServerRedirectHandler(self):
1384 """Sends a server redirect to the given URL. The syntax is
1385 '/server-redirect?http://foo.bar/asdf' to redirect to
1386 'http://foo.bar/asdf'"""
1388 test_name = "/server-redirect"
1389 if not self._ShouldHandleRequest(test_name):
1392 query_char = self.path.find('?')
1393 if query_char < 0 or len(self.path) <= query_char + 1:
1394 self.sendRedirectHelp(test_name)
1396 dest = urllib.unquote(self.path[query_char + 1:])
1398 self.send_response(301) # moved permanently
1399 self.send_header('Location', dest)
1400 self.send_header('Content-Type', 'text/html')
1402 self.wfile.write('<html><head>')
1403 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1407 def ClientRedirectHandler(self):
1408 """Sends a client redirect to the given URL. The syntax is
1409 '/client-redirect?http://foo.bar/asdf' to redirect to
1410 'http://foo.bar/asdf'"""
1412 test_name = "/client-redirect"
1413 if not self._ShouldHandleRequest(test_name):
1416 query_char = self.path.find('?')
1417 if query_char < 0 or len(self.path) <= query_char + 1:
1418 self.sendRedirectHelp(test_name)
1420 dest = urllib.unquote(self.path[query_char + 1:])
1422 self.send_response(200)
1423 self.send_header('Content-Type', 'text/html')
1425 self.wfile.write('<html><head>')
1426 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1427 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1431 def MultipartHandler(self):
1432 """Send a multipart response (10 text/html pages)."""
1434 test_name = '/multipart'
1435 if not self._ShouldHandleRequest(test_name):
1440 self.send_response(200)
1441 self.send_header('Content-Type',
1442 'multipart/x-mixed-replace;boundary=' + bound)
1445 for i in xrange(num_frames):
1446 self.wfile.write('--' + bound + '\r\n')
1447 self.wfile.write('Content-Type: text/html\r\n\r\n')
1448 self.wfile.write('<title>page ' + str(i) + '</title>')
1449 self.wfile.write('page ' + str(i))
1451 self.wfile.write('--' + bound + '--')
1454 def GetSSLSessionCacheHandler(self):
1455 """Send a reply containing a log of the session cache operations."""
1457 if not self._ShouldHandleRequest('/ssl-session-cache'):
1460 self.send_response(200)
1461 self.send_header('Content-Type', 'text/plain')
1464 log = self.server.session_cache.log
1465 except AttributeError:
1466 self.wfile.write('Pass --https-record-resume in order to use' +
1470 for (action, sessionID) in log:
1471 self.wfile.write('%s\t%s\n' % (action, bytes(sessionID).encode('hex')))
1474 def SSLManySmallRecords(self):
1475 """Sends a reply consisting of a variety of small writes. These will be
1476 translated into a series of small SSL records when used over an HTTPS
1479 if not self._ShouldHandleRequest('/ssl-many-small-records'):
1482 self.send_response(200)
1483 self.send_header('Content-Type', 'text/plain')
1486 # Write ~26K of data, in 1350 byte chunks
1487 for i in xrange(20):
1488 self.wfile.write('*' * 1350)
1492 def GetChannelID(self):
1493 """Send a reply containing the hashed ChannelID that the client provided."""
1495 if not self._ShouldHandleRequest('/channel-id'):
1498 self.send_response(200)
1499 self.send_header('Content-Type', 'text/plain')
1501 channel_id = bytes(self.server.tlsConnection.channel_id)
1502 self.wfile.write(hashlib.sha256(channel_id).digest().encode('base64'))
1505 def CloseSocketHandler(self):
1506 """Closes the socket without sending anything."""
1508 if not self._ShouldHandleRequest('/close-socket'):
1514 def RangeResetHandler(self):
1515 """Send data broken up by connection resets every N (default 4K) bytes.
1516 Support range requests. If the data requested doesn't straddle a reset
1517 boundary, it will all be sent. Used for testing resuming downloads."""
1519 def DataForRange(start, end):
1520 """Data to be provided for a particular range of bytes."""
1521 # Offset and scale to avoid too obvious (and hence potentially
1523 return ''.join([chr(y % 256)
1524 for y in range(start * 2 + 15, end * 2 + 15, 2)])
1526 if not self._ShouldHandleRequest('/rangereset'):
1529 # HTTP/1.1 is required for ETag and range support.
1530 self.protocol_version = 'HTTP/1.1'
1531 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1535 # Note that the rst is sent just before sending the rst_boundary byte.
1537 respond_to_range = True
1538 hold_for_signal = False
1541 fail_precondition = 0
1542 send_verifiers = True
1545 qdict = urlparse.parse_qs(query, True)
1547 size = int(qdict['size'][0])
1548 if 'rst_boundary' in qdict:
1549 rst_boundary = int(qdict['rst_boundary'][0])
1550 if 'token' in qdict:
1551 # Identifying token for stateful tests.
1552 token = qdict['token'][0]
1553 if 'rst_limit' in qdict:
1554 # Max number of rsts for a given token.
1555 rst_limit = int(qdict['rst_limit'][0])
1556 if 'bounce_range' in qdict:
1557 respond_to_range = False
1559 # Note that hold_for_signal will not work with null range requests;
1561 hold_for_signal = True
1562 if 'no_verifiers' in qdict:
1563 send_verifiers = False
1564 if 'fail_precondition' in qdict:
1565 fail_precondition = int(qdict['fail_precondition'][0])
1567 # Record already set information, or set it.
1568 rst_limit = TestPageHandler.rst_limits.setdefault(token, rst_limit)
1570 TestPageHandler.rst_limits[token] -= 1
1571 fail_precondition = TestPageHandler.fail_precondition.setdefault(
1572 token, fail_precondition)
1573 if fail_precondition != 0:
1574 TestPageHandler.fail_precondition[token] -= 1
1577 last_byte = size - 1
1579 # Does that define what we want to return, or do we need to apply
1581 range_response = False
1582 range_header = self.headers.getheader('range')
1583 if range_header and respond_to_range:
1584 mo = re.match("bytes=(\d*)-(\d*)", range_header)
1586 first_byte = int(mo.group(1))
1588 last_byte = int(mo.group(2))
1589 if last_byte > size - 1:
1590 last_byte = size - 1
1591 range_response = True
1592 if last_byte < first_byte:
1595 if (fail_precondition and
1596 (self.headers.getheader('If-Modified-Since') or
1597 self.headers.getheader('If-Match'))):
1598 self.send_response(412)
1603 self.send_response(206)
1604 self.send_header('Content-Range',
1605 'bytes %d-%d/%d' % (first_byte, last_byte, size))
1607 self.send_response(200)
1608 self.send_header('Content-Type', 'application/octet-stream')
1609 self.send_header('Content-Length', last_byte - first_byte + 1)
1611 # If fail_precondition is non-zero, then the ETag for each request will be
1613 etag = "%s%d" % (token, fail_precondition)
1614 self.send_header('ETag', etag)
1615 self.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST')
1619 # TODO(rdsmith/phajdan.jr): http://crbug.com/169519: Without writing
1620 # a single byte, the self.server.handle_request() below hangs
1621 # without processing new incoming requests.
1622 self.wfile.write(DataForRange(first_byte, first_byte + 1))
1623 first_byte = first_byte + 1
1624 # handle requests until one of them clears this flag.
1625 self.server.wait_for_download = True
1626 while self.server.wait_for_download:
1627 self.server.handle_request()
1629 possible_rst = ((first_byte / rst_boundary) + 1) * rst_boundary
1630 if possible_rst >= last_byte or rst_limit == 0:
1631 # No RST has been requested in this range, so we don't need to
1632 # do anything fancy; just write the data and let the python
1633 # infrastructure close the connection.
1634 self.wfile.write(DataForRange(first_byte, last_byte + 1))
1638 # We're resetting the connection part way in; go to the RST
1639 # boundary and then send an RST.
1640 # Because socket semantics do not guarantee that all the data will be
1641 # sent when using the linger semantics to hard close a socket,
1642 # we send the data and then wait for our peer to release us
1643 # before sending the reset.
1644 data = DataForRange(first_byte, possible_rst)
1645 self.wfile.write(data)
1647 self.server.wait_for_download = True
1648 while self.server.wait_for_download:
1649 self.server.handle_request()
1650 l_onoff = 1 # Linger is active.
1651 l_linger = 0 # Seconds to linger for.
1652 self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
1653 struct.pack('ii', l_onoff, l_linger))
1655 # Close all duplicates of the underlying socket to force the RST.
1658 self.connection.close()
1662 def DefaultResponseHandler(self):
1663 """This is the catch-all response handler for requests that aren't handled
1664 by one of the special handlers above.
1665 Note that we specify the content-length as without it the https connection
1666 is not closed properly (and the browser keeps expecting data)."""
1668 contents = "Default response given for path: " + self.path
1669 self.send_response(200)
1670 self.send_header('Content-Type', 'text/html')
1671 self.send_header('Content-Length', len(contents))
1673 if (self.command != 'HEAD'):
1674 self.wfile.write(contents)
1677 def RedirectConnectHandler(self):
1678 """Sends a redirect to the CONNECT request for www.redirect.com. This
1679 response is not specified by the RFC, so the browser should not follow
1682 if (self.path.find("www.redirect.com") < 0):
1685 dest = "http://www.destination.com/foo.js"
1687 self.send_response(302) # moved temporarily
1688 self.send_header('Location', dest)
1689 self.send_header('Connection', 'close')
1693 def ServerAuthConnectHandler(self):
1694 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1695 response doesn't make sense because the proxy server cannot request
1696 server authentication."""
1698 if (self.path.find("www.server-auth.com") < 0):
1701 challenge = 'Basic realm="WallyWorld"'
1703 self.send_response(401) # unauthorized
1704 self.send_header('WWW-Authenticate', challenge)
1705 self.send_header('Connection', 'close')
1709 def DefaultConnectResponseHandler(self):
1710 """This is the catch-all response handler for CONNECT requests that aren't
1711 handled by one of the special handlers above. Real Web servers respond
1712 with 400 to CONNECT requests."""
1714 contents = "Your client has issued a malformed or illegal request."
1715 self.send_response(400) # bad request
1716 self.send_header('Content-Type', 'text/html')
1717 self.send_header('Content-Length', len(contents))
1719 self.wfile.write(contents)
1722 # called by the redirect handling function when there is no parameter
1723 def sendRedirectHelp(self, redirect_name):
1724 self.send_response(200)
1725 self.send_header('Content-Type', 'text/html')
1727 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1728 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1729 self.wfile.write('</body></html>')
1731 # called by chunked handling function
1732 def sendChunkHelp(self, chunk):
1733 # Each chunk consists of: chunk size (hex), CRLF, chunk body, CRLF
1734 self.wfile.write('%X\r\n' % len(chunk))
1735 self.wfile.write(chunk)
1736 self.wfile.write('\r\n')
1739 class OCSPHandler(testserver_base.BasePageHandler):
1740 def __init__(self, request, client_address, socket_server):
1741 handlers = [self.OCSPResponse]
1742 self.ocsp_response = socket_server.ocsp_response
1743 testserver_base.BasePageHandler.__init__(self, request, client_address,
1744 socket_server, [], handlers, [],
1747 def OCSPResponse(self):
1748 self.send_response(200)
1749 self.send_header('Content-Type', 'application/ocsp-response')
1750 self.send_header('Content-Length', str(len(self.ocsp_response)))
1753 self.wfile.write(self.ocsp_response)
1756 class TCPEchoHandler(SocketServer.BaseRequestHandler):
1757 """The RequestHandler class for TCP echo server.
1759 It is instantiated once per connection to the server, and overrides the
1760 handle() method to implement communication to the client.
1764 """Handles the request from the client and constructs a response."""
1766 data = self.request.recv(65536).strip()
1767 # Verify the "echo request" message received from the client. Send back
1768 # "echo response" message if "echo request" message is valid.
1770 return_data = echo_message.GetEchoResponseData(data)
1776 self.request.send(return_data)
1779 class UDPEchoHandler(SocketServer.BaseRequestHandler):
1780 """The RequestHandler class for UDP echo server.
1782 It is instantiated once per connection to the server, and overrides the
1783 handle() method to implement communication to the client.
1787 """Handles the request from the client and constructs a response."""
1789 data = self.request[0].strip()
1790 request_socket = self.request[1]
1791 # Verify the "echo request" message received from the client. Send back
1792 # "echo response" message if "echo request" message is valid.
1794 return_data = echo_message.GetEchoResponseData(data)
1799 request_socket.sendto(return_data, self.client_address)
1802 class BasicAuthProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
1803 """A request handler that behaves as a proxy server which requires
1804 basic authentication. Only CONNECT, GET and HEAD is supported for now.
1807 _AUTH_CREDENTIAL = 'Basic Zm9vOmJhcg==' # foo:bar
1809 def parse_request(self):
1810 """Overrides parse_request to check credential."""
1812 if not BaseHTTPServer.BaseHTTPRequestHandler.parse_request(self):
1815 auth = self.headers.getheader('Proxy-Authorization')
1816 if auth != self._AUTH_CREDENTIAL:
1817 self.send_response(407)
1818 self.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"')
1824 def _start_read_write(self, sock):
1826 self.request.setblocking(0)
1827 rlist = [self.request, sock]
1829 ready_sockets, _unused, errors = select.select(rlist, [], [])
1831 self.send_response(500)
1834 for s in ready_sockets:
1835 received = s.recv(1024)
1836 if len(received) == 0:
1838 if s == self.request:
1841 other = self.request
1842 other.send(received)
1844 def _do_common_method(self):
1845 url = urlparse.urlparse(self.path)
1848 if url.scheme == 'http':
1850 elif url.scheme == 'https':
1852 if not url.hostname or not port:
1853 self.send_response(400)
1857 if len(url.path) == 0:
1861 if len(url.query) > 0:
1862 path = '%s?%s' % (url.path, url.query)
1866 sock = socket.create_connection((url.hostname, port))
1867 sock.send('%s %s %s\r\n' % (
1868 self.command, path, self.protocol_version))
1869 for header in self.headers.headers:
1870 header = header.strip()
1871 if (header.lower().startswith('connection') or
1872 header.lower().startswith('proxy')):
1874 sock.send('%s\r\n' % header)
1876 self._start_read_write(sock)
1878 self.send_response(500)
1881 if sock is not None:
1884 def do_CONNECT(self):
1886 pos = self.path.rfind(':')
1887 host = self.path[:pos]
1888 port = int(self.path[pos+1:])
1890 self.send_response(400)
1894 sock = socket.create_connection((host, port))
1895 self.send_response(200, 'Connection established')
1897 self._start_read_write(sock)
1899 self.send_response(500)
1905 self._do_common_method()
1908 self._do_common_method()
1911 class ServerRunner(testserver_base.TestServerRunner):
1912 """TestServerRunner for the net test servers."""
1915 super(ServerRunner, self).__init__()
1916 self.__ocsp_server = None
1918 def __make_data_dir(self):
1919 if self.options.data_dir:
1920 if not os.path.isdir(self.options.data_dir):
1921 raise testserver_base.OptionError('specified data dir not found: ' +
1922 self.options.data_dir + ' exiting...')
1923 my_data_dir = self.options.data_dir
1925 # Create the default path to our data dir, relative to the exe dir.
1926 my_data_dir = os.path.join(BASE_DIR, "..", "..", "..", "..",
1929 #TODO(ibrar): Must use Find* funtion defined in google\tools
1930 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1934 def create_server(self, server_data):
1935 port = self.options.port
1936 host = self.options.host
1938 if self.options.server_type == SERVER_HTTP:
1939 if self.options.https:
1940 pem_cert_and_key = None
1941 if self.options.cert_and_key_file:
1942 if not os.path.isfile(self.options.cert_and_key_file):
1943 raise testserver_base.OptionError(
1944 'specified server cert file not found: ' +
1945 self.options.cert_and_key_file + ' exiting...')
1946 pem_cert_and_key = file(self.options.cert_and_key_file, 'r').read()
1948 # generate a new certificate and run an OCSP server for it.
1949 self.__ocsp_server = OCSPServer((host, 0), OCSPHandler)
1950 print ('OCSP server started on %s:%d...' %
1951 (host, self.__ocsp_server.server_port))
1956 if self.options.ocsp == 'ok':
1957 ocsp_state = minica.OCSP_STATE_GOOD
1958 elif self.options.ocsp == 'revoked':
1959 ocsp_state = minica.OCSP_STATE_REVOKED
1960 elif self.options.ocsp == 'invalid':
1961 ocsp_state = minica.OCSP_STATE_INVALID
1962 elif self.options.ocsp == 'unauthorized':
1963 ocsp_state = minica.OCSP_STATE_UNAUTHORIZED
1964 elif self.options.ocsp == 'unknown':
1965 ocsp_state = minica.OCSP_STATE_UNKNOWN
1967 raise testserver_base.OptionError('unknown OCSP status: ' +
1968 self.options.ocsp_status)
1970 (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP(
1971 subject = "127.0.0.1",
1972 ocsp_url = ("http://%s:%d/ocsp" %
1973 (host, self.__ocsp_server.server_port)),
1974 ocsp_state = ocsp_state,
1975 serial = self.options.cert_serial)
1977 self.__ocsp_server.ocsp_response = ocsp_der
1979 for ca_cert in self.options.ssl_client_ca:
1980 if not os.path.isfile(ca_cert):
1981 raise testserver_base.OptionError(
1982 'specified trusted client CA file not found: ' + ca_cert +
1985 stapled_ocsp_response = None
1986 if self.__ocsp_server and self.options.staple_ocsp_response:
1987 stapled_ocsp_response = self.__ocsp_server.ocsp_response
1989 server = HTTPSServer((host, port), TestPageHandler, pem_cert_and_key,
1990 self.options.ssl_client_auth,
1991 self.options.ssl_client_ca,
1992 self.options.ssl_bulk_cipher,
1993 self.options.ssl_key_exchange,
1994 self.options.enable_npn,
1995 self.options.record_resume,
1996 self.options.tls_intolerant,
1997 self.options.signed_cert_timestamps_tls_ext.decode(
1999 self.options.fallback_scsv,
2000 stapled_ocsp_response)
2001 print 'HTTPS server started on %s:%d...' % (host, server.server_port)
2003 server = HTTPServer((host, port), TestPageHandler)
2004 print 'HTTP server started on %s:%d...' % (host, server.server_port)
2006 server.data_dir = self.__make_data_dir()
2007 server.file_root_url = self.options.file_root_url
2008 server_data['port'] = server.server_port
2009 elif self.options.server_type == SERVER_WEBSOCKET:
2010 # Launch pywebsocket via WebSocketServer.
2011 logger = logging.getLogger()
2012 logger.addHandler(logging.StreamHandler())
2013 # TODO(toyoshim): Remove following os.chdir. Currently this operation
2014 # is required to work correctly. It should be fixed from pywebsocket side.
2015 os.chdir(self.__make_data_dir())
2016 websocket_options = WebSocketOptions(host, port, '.')
2017 if self.options.cert_and_key_file:
2018 websocket_options.use_tls = True
2019 websocket_options.private_key = self.options.cert_and_key_file
2020 websocket_options.certificate = self.options.cert_and_key_file
2021 if self.options.ssl_client_auth:
2022 websocket_options.tls_client_auth = True
2023 if len(self.options.ssl_client_ca) != 1:
2024 raise testserver_base.OptionError(
2025 'one trusted client CA file should be specified')
2026 if not os.path.isfile(self.options.ssl_client_ca[0]):
2027 raise testserver_base.OptionError(
2028 'specified trusted client CA file not found: ' +
2029 self.options.ssl_client_ca[0] + ' exiting...')
2030 websocket_options.tls_client_ca = self.options.ssl_client_ca[0]
2031 server = WebSocketServer(websocket_options)
2032 print 'WebSocket server started on %s:%d...' % (host, server.server_port)
2033 server_data['port'] = server.server_port
2034 elif self.options.server_type == SERVER_TCP_ECHO:
2035 # Used for generating the key (randomly) that encodes the "echo request"
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"
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
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']
2077 raise testserver_base.OptionError('unknown server type' +
2078 self.options.server_type)
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('-f', '--ftp', action='store_const',
2094 const=SERVER_FTP, default=SERVER_HTTP,
2096 help='start up an FTP server.')
2097 self.option_parser.add_option('--tcp-echo', action='store_const',
2098 const=SERVER_TCP_ECHO, default=SERVER_HTTP,
2100 help='start up a tcp echo server.')
2101 self.option_parser.add_option('--udp-echo', action='store_const',
2102 const=SERVER_UDP_ECHO, default=SERVER_HTTP,
2104 help='start up a udp echo server.')
2105 self.option_parser.add_option('--basic-auth-proxy', action='store_const',
2106 const=SERVER_BASIC_AUTH_PROXY,
2107 default=SERVER_HTTP, dest='server_type',
2108 help='start up a proxy server which requires '
2109 'basic authentication.')
2110 self.option_parser.add_option('--websocket', action='store_const',
2111 const=SERVER_WEBSOCKET, default=SERVER_HTTP,
2113 help='start up a WebSocket server.')
2114 self.option_parser.add_option('--https', action='store_true',
2115 dest='https', help='Specify that https '
2117 self.option_parser.add_option('--cert-and-key-file',
2118 dest='cert_and_key_file', help='specify the '
2119 'path to the file containing the certificate '
2120 'and private key for the server in PEM '
2122 self.option_parser.add_option('--ocsp', dest='ocsp', default='ok',
2123 help='The type of OCSP response generated '
2124 'for the automatically generated '
2125 'certificate. One of [ok,revoked,invalid]')
2126 self.option_parser.add_option('--cert-serial', dest='cert_serial',
2127 default=0, type=int,
2128 help='If non-zero then the generated '
2129 'certificate will have this serial number')
2130 self.option_parser.add_option('--tls-intolerant', dest='tls_intolerant',
2131 default='0', type='int',
2132 help='If nonzero, certain TLS connections '
2133 'will be aborted in order to test version '
2134 'fallback. 1 means all TLS versions will be '
2135 'aborted. 2 means TLS 1.1 or higher will be '
2136 'aborted. 3 means TLS 1.2 or higher will be '
2138 self.option_parser.add_option('--signed-cert-timestamps-tls-ext',
2139 dest='signed_cert_timestamps_tls_ext',
2141 help='Base64 encoded SCT list. If set, '
2142 'server will respond with a '
2143 'signed_certificate_timestamp TLS extension '
2144 'whenever the client supports it.')
2145 self.option_parser.add_option('--fallback-scsv', dest='fallback_scsv',
2146 default=False, const=True,
2147 action='store_const',
2148 help='If given, TLS_FALLBACK_SCSV support '
2149 'will be enabled. This causes the server to '
2150 'reject fallback connections from compatible '
2151 'clients (e.g. Chrome).')
2152 self.option_parser.add_option('--staple-ocsp-response',
2153 dest='staple_ocsp_response',
2154 default=False, action='store_true',
2155 help='If set, server will staple the OCSP '
2156 'response whenever OCSP is on and the client '
2157 'supports OCSP stapling.')
2158 self.option_parser.add_option('--https-record-resume',
2159 dest='record_resume', const=True,
2160 default=False, action='store_const',
2161 help='Record resumption cache events rather '
2162 'than resuming as normal. Allows the use of '
2163 'the /ssl-session-cache request')
2164 self.option_parser.add_option('--ssl-client-auth', action='store_true',
2165 help='Require SSL client auth on every '
2167 self.option_parser.add_option('--ssl-client-ca', action='append',
2168 default=[], help='Specify that the client '
2169 'certificate request should include the CA '
2170 'named in the subject of the DER-encoded '
2171 'certificate contained in the specified '
2172 'file. This option may appear multiple '
2173 'times, indicating multiple CA names should '
2174 'be sent in the request.')
2175 self.option_parser.add_option('--ssl-bulk-cipher', action='append',
2176 help='Specify the bulk encryption '
2177 'algorithm(s) that will be accepted by the '
2178 'SSL server. Valid values are "aes256", '
2179 '"aes128", "3des", "rc4". If omitted, all '
2180 'algorithms will be used. This option may '
2181 'appear multiple times, indicating '
2182 'multiple algorithms should be enabled.');
2183 self.option_parser.add_option('--ssl-key-exchange', action='append',
2184 help='Specify the key exchange algorithm(s)'
2185 'that will be accepted by the SSL server. '
2186 'Valid values are "rsa", "dhe_rsa". If '
2187 'omitted, all algorithms will be used. This '
2188 'option may appear multiple times, '
2189 'indicating multiple algorithms should be '
2191 # TODO(davidben): Add ALPN support to tlslite.
2192 self.option_parser.add_option('--enable-npn', dest='enable_npn',
2193 default=False, const=True,
2194 action='store_const',
2195 help='Enable server support for the NPN '
2196 'extension. The server will advertise '
2197 'support for exactly one protocol, http/1.1')
2198 self.option_parser.add_option('--file-root-url', default='/files/',
2199 help='Specify a root URL for files served.')
2202 if __name__ == '__main__':
2203 sys.exit(ServerRunner().main())