Windows should animate when they are about to get docked at screen edges.
[chromium-blink-merge.git] / net / tools / testserver / testserver.py
blob77a314263001a60d083f5ed0f23e9c563cd42c76
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 random
25 import re
26 import select
27 import socket
28 import SocketServer
29 import struct
30 import sys
31 import threading
32 import time
33 import urllib
34 import urlparse
35 import zlib
37 import echo_message
38 import pyftpdlib.ftpserver
39 import testserver_base
40 import tlslite
41 import tlslite.api
43 BASE_DIR = os.path.dirname(os.path.abspath(__file__))
44 sys.path.insert(
45 0, os.path.join(BASE_DIR, '..', '..', '..', 'third_party/pywebsocket/src'))
46 from mod_pywebsocket.standalone import WebSocketServer
48 SERVER_HTTP = 0
49 SERVER_FTP = 1
50 SERVER_TCP_ECHO = 2
51 SERVER_UDP_ECHO = 3
52 SERVER_BASIC_AUTH_PROXY = 4
53 SERVER_WEBSOCKET = 5
55 # Default request queue size for WebSocketServer.
56 _DEFAULT_REQUEST_QUEUE_SIZE = 128
58 class WebSocketOptions:
59 """Holds options for WebSocketServer."""
61 def __init__(self, host, port, data_dir):
62 self.request_queue_size = _DEFAULT_REQUEST_QUEUE_SIZE
63 self.server_host = host
64 self.port = port
65 self.websock_handlers = data_dir
66 self.scan_dir = None
67 self.allow_handlers_outside_root_dir = False
68 self.websock_handlers_map_file = None
69 self.cgi_directories = []
70 self.is_executable_method = None
71 self.allow_draft75 = False
72 self.strict = True
74 self.use_tls = False
75 self.private_key = None
76 self.certificate = None
77 self.tls_client_auth = False
78 self.tls_client_ca = None
79 self.use_basic_auth = False
82 class RecordingSSLSessionCache(object):
83 """RecordingSSLSessionCache acts as a TLS session cache and maintains a log of
84 lookups and inserts in order to test session cache behaviours."""
86 def __init__(self):
87 self.log = []
89 def __getitem__(self, sessionID):
90 self.log.append(('lookup', sessionID))
91 raise KeyError()
93 def __setitem__(self, sessionID, session):
94 self.log.append(('insert', sessionID))
97 class HTTPServer(testserver_base.ClientRestrictingServerMixIn,
98 testserver_base.BrokenPipeHandlerMixIn,
99 testserver_base.StoppableHTTPServer):
100 """This is a specialization of StoppableHTTPServer that adds client
101 verification."""
103 pass
105 class OCSPServer(testserver_base.ClientRestrictingServerMixIn,
106 testserver_base.BrokenPipeHandlerMixIn,
107 BaseHTTPServer.HTTPServer):
108 """This is a specialization of HTTPServer that serves an
109 OCSP response"""
111 def serve_forever_on_thread(self):
112 self.thread = threading.Thread(target = self.serve_forever,
113 name = "OCSPServerThread")
114 self.thread.start()
116 def stop_serving(self):
117 self.shutdown()
118 self.thread.join()
121 class HTTPSServer(tlslite.api.TLSSocketServerMixIn,
122 testserver_base.ClientRestrictingServerMixIn,
123 testserver_base.BrokenPipeHandlerMixIn,
124 testserver_base.StoppableHTTPServer):
125 """This is a specialization of StoppableHTTPServer that add https support and
126 client verification."""
128 def __init__(self, server_address, request_hander_class, pem_cert_and_key,
129 ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers,
130 record_resume_info, tls_intolerant):
131 self.cert_chain = tlslite.api.X509CertChain().parseChain(pem_cert_and_key)
132 # Force using only python implementation - otherwise behavior is different
133 # depending on whether m2crypto Python module is present (error is thrown
134 # when it is). m2crypto uses a C (based on OpenSSL) implementation under
135 # the hood.
136 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key,
137 private=True,
138 implementations=['python'])
139 self.ssl_client_auth = ssl_client_auth
140 self.ssl_client_cas = []
141 self.tls_intolerant = tls_intolerant
143 for ca_file in ssl_client_cas:
144 s = open(ca_file).read()
145 x509 = tlslite.api.X509()
146 x509.parse(s)
147 self.ssl_client_cas.append(x509.subject)
148 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
149 if ssl_bulk_ciphers is not None:
150 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
152 if record_resume_info:
153 # If record_resume_info is true then we'll replace the session cache with
154 # an object that records the lookups and inserts that it sees.
155 self.session_cache = RecordingSSLSessionCache()
156 else:
157 self.session_cache = tlslite.api.SessionCache()
158 testserver_base.StoppableHTTPServer.__init__(self,
159 server_address,
160 request_hander_class)
162 def handshake(self, tlsConnection):
163 """Creates the SSL connection."""
165 try:
166 self.tlsConnection = tlsConnection
167 tlsConnection.handshakeServer(certChain=self.cert_chain,
168 privateKey=self.private_key,
169 sessionCache=self.session_cache,
170 reqCert=self.ssl_client_auth,
171 settings=self.ssl_handshake_settings,
172 reqCAs=self.ssl_client_cas,
173 tlsIntolerant=self.tls_intolerant)
174 tlsConnection.ignoreAbruptClose = True
175 return True
176 except tlslite.api.TLSAbruptCloseError:
177 # Ignore abrupt close.
178 return True
179 except tlslite.api.TLSError, error:
180 print "Handshake failure:", str(error)
181 return False
184 class FTPServer(testserver_base.ClientRestrictingServerMixIn,
185 pyftpdlib.ftpserver.FTPServer):
186 """This is a specialization of FTPServer that adds client verification."""
188 pass
191 class TCPEchoServer(testserver_base.ClientRestrictingServerMixIn,
192 SocketServer.TCPServer):
193 """A TCP echo server that echoes back what it has received."""
195 def server_bind(self):
196 """Override server_bind to store the server name."""
198 SocketServer.TCPServer.server_bind(self)
199 host, port = self.socket.getsockname()[:2]
200 self.server_name = socket.getfqdn(host)
201 self.server_port = port
203 def serve_forever(self):
204 self.stop = False
205 self.nonce_time = None
206 while not self.stop:
207 self.handle_request()
208 self.socket.close()
211 class UDPEchoServer(testserver_base.ClientRestrictingServerMixIn,
212 SocketServer.UDPServer):
213 """A UDP echo server that echoes back what it has received."""
215 def server_bind(self):
216 """Override server_bind to store the server name."""
218 SocketServer.UDPServer.server_bind(self)
219 host, port = self.socket.getsockname()[:2]
220 self.server_name = socket.getfqdn(host)
221 self.server_port = port
223 def serve_forever(self):
224 self.stop = False
225 self.nonce_time = None
226 while not self.stop:
227 self.handle_request()
228 self.socket.close()
231 class TestPageHandler(testserver_base.BasePageHandler):
232 # Class variables to allow for persistence state between page handler
233 # invocations
234 rst_limits = {}
235 fail_precondition = {}
237 def __init__(self, request, client_address, socket_server):
238 connect_handlers = [
239 self.RedirectConnectHandler,
240 self.ServerAuthConnectHandler,
241 self.DefaultConnectResponseHandler]
242 get_handlers = [
243 self.NoCacheMaxAgeTimeHandler,
244 self.NoCacheTimeHandler,
245 self.CacheTimeHandler,
246 self.CacheExpiresHandler,
247 self.CacheProxyRevalidateHandler,
248 self.CachePrivateHandler,
249 self.CachePublicHandler,
250 self.CacheSMaxAgeHandler,
251 self.CacheMustRevalidateHandler,
252 self.CacheMustRevalidateMaxAgeHandler,
253 self.CacheNoStoreHandler,
254 self.CacheNoStoreMaxAgeHandler,
255 self.CacheNoTransformHandler,
256 self.DownloadHandler,
257 self.DownloadFinishHandler,
258 self.EchoHeader,
259 self.EchoHeaderCache,
260 self.EchoAllHandler,
261 self.ZipFileHandler,
262 self.FileHandler,
263 self.SetCookieHandler,
264 self.SetManyCookiesHandler,
265 self.ExpectAndSetCookieHandler,
266 self.SetHeaderHandler,
267 self.AuthBasicHandler,
268 self.AuthDigestHandler,
269 self.SlowServerHandler,
270 self.ChunkedServerHandler,
271 self.ContentTypeHandler,
272 self.NoContentHandler,
273 self.ServerRedirectHandler,
274 self.ClientRedirectHandler,
275 self.MultipartHandler,
276 self.GetSSLSessionCacheHandler,
277 self.SSLManySmallRecords,
278 self.GetChannelID,
279 self.CloseSocketHandler,
280 self.RangeResetHandler,
281 self.DefaultResponseHandler]
282 post_handlers = [
283 self.EchoTitleHandler,
284 self.EchoHandler,
285 self.PostOnlyFileHandler] + get_handlers
286 put_handlers = [
287 self.EchoTitleHandler,
288 self.EchoHandler] + get_handlers
289 head_handlers = [
290 self.FileHandler,
291 self.DefaultResponseHandler]
293 self._mime_types = {
294 'crx' : 'application/x-chrome-extension',
295 'exe' : 'application/octet-stream',
296 'gif': 'image/gif',
297 'jpeg' : 'image/jpeg',
298 'jpg' : 'image/jpeg',
299 'json': 'application/json',
300 'pdf' : 'application/pdf',
301 'wav' : 'audio/wav',
302 'xml' : 'text/xml'
304 self._default_mime_type = 'text/html'
306 testserver_base.BasePageHandler.__init__(self, request, client_address,
307 socket_server, connect_handlers,
308 get_handlers, head_handlers,
309 post_handlers, put_handlers)
311 def GetMIMETypeFromName(self, file_name):
312 """Returns the mime type for the specified file_name. So far it only looks
313 at the file extension."""
315 (_shortname, extension) = os.path.splitext(file_name.split("?")[0])
316 if len(extension) == 0:
317 # no extension.
318 return self._default_mime_type
320 # extension starts with a dot, so we need to remove it
321 return self._mime_types.get(extension[1:], self._default_mime_type)
323 def NoCacheMaxAgeTimeHandler(self):
324 """This request handler yields a page with the title set to the current
325 system time, and no caching requested."""
327 if not self._ShouldHandleRequest("/nocachetime/maxage"):
328 return False
330 self.send_response(200)
331 self.send_header('Cache-Control', 'max-age=0')
332 self.send_header('Content-Type', 'text/html')
333 self.end_headers()
335 self.wfile.write('<html><head><title>%s</title></head></html>' %
336 time.time())
338 return True
340 def NoCacheTimeHandler(self):
341 """This request handler yields a page with the title set to the current
342 system time, and no caching requested."""
344 if not self._ShouldHandleRequest("/nocachetime"):
345 return False
347 self.send_response(200)
348 self.send_header('Cache-Control', 'no-cache')
349 self.send_header('Content-Type', 'text/html')
350 self.end_headers()
352 self.wfile.write('<html><head><title>%s</title></head></html>' %
353 time.time())
355 return True
357 def CacheTimeHandler(self):
358 """This request handler yields a page with the title set to the current
359 system time, and allows caching for one minute."""
361 if not self._ShouldHandleRequest("/cachetime"):
362 return False
364 self.send_response(200)
365 self.send_header('Cache-Control', 'max-age=60')
366 self.send_header('Content-Type', 'text/html')
367 self.end_headers()
369 self.wfile.write('<html><head><title>%s</title></head></html>' %
370 time.time())
372 return True
374 def CacheExpiresHandler(self):
375 """This request handler yields a page with the title set to the current
376 system time, and set the page to expire on 1 Jan 2099."""
378 if not self._ShouldHandleRequest("/cache/expires"):
379 return False
381 self.send_response(200)
382 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
383 self.send_header('Content-Type', 'text/html')
384 self.end_headers()
386 self.wfile.write('<html><head><title>%s</title></head></html>' %
387 time.time())
389 return True
391 def CacheProxyRevalidateHandler(self):
392 """This request handler yields a page with the title set to the current
393 system time, and allows caching for 60 seconds"""
395 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
396 return False
398 self.send_response(200)
399 self.send_header('Content-Type', 'text/html')
400 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
401 self.end_headers()
403 self.wfile.write('<html><head><title>%s</title></head></html>' %
404 time.time())
406 return True
408 def CachePrivateHandler(self):
409 """This request handler yields a page with the title set to the current
410 system time, and allows caching for 5 seconds."""
412 if not self._ShouldHandleRequest("/cache/private"):
413 return False
415 self.send_response(200)
416 self.send_header('Content-Type', 'text/html')
417 self.send_header('Cache-Control', 'max-age=3, private')
418 self.end_headers()
420 self.wfile.write('<html><head><title>%s</title></head></html>' %
421 time.time())
423 return True
425 def CachePublicHandler(self):
426 """This request handler yields a page with the title set to the current
427 system time, and allows caching for 5 seconds."""
429 if not self._ShouldHandleRequest("/cache/public"):
430 return False
432 self.send_response(200)
433 self.send_header('Content-Type', 'text/html')
434 self.send_header('Cache-Control', 'max-age=3, public')
435 self.end_headers()
437 self.wfile.write('<html><head><title>%s</title></head></html>' %
438 time.time())
440 return True
442 def CacheSMaxAgeHandler(self):
443 """This request handler yields a page with the title set to the current
444 system time, and does not allow for caching."""
446 if not self._ShouldHandleRequest("/cache/s-maxage"):
447 return False
449 self.send_response(200)
450 self.send_header('Content-Type', 'text/html')
451 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
452 self.end_headers()
454 self.wfile.write('<html><head><title>%s</title></head></html>' %
455 time.time())
457 return True
459 def CacheMustRevalidateHandler(self):
460 """This request handler yields a page with the title set to the current
461 system time, and does not allow caching."""
463 if not self._ShouldHandleRequest("/cache/must-revalidate"):
464 return False
466 self.send_response(200)
467 self.send_header('Content-Type', 'text/html')
468 self.send_header('Cache-Control', 'must-revalidate')
469 self.end_headers()
471 self.wfile.write('<html><head><title>%s</title></head></html>' %
472 time.time())
474 return True
476 def CacheMustRevalidateMaxAgeHandler(self):
477 """This request handler yields a page with the title set to the current
478 system time, and does not allow caching event though max-age of 60
479 seconds is specified."""
481 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
482 return False
484 self.send_response(200)
485 self.send_header('Content-Type', 'text/html')
486 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
487 self.end_headers()
489 self.wfile.write('<html><head><title>%s</title></head></html>' %
490 time.time())
492 return True
494 def CacheNoStoreHandler(self):
495 """This request handler yields a page with the title set to the current
496 system time, and does not allow the page to be stored."""
498 if not self._ShouldHandleRequest("/cache/no-store"):
499 return False
501 self.send_response(200)
502 self.send_header('Content-Type', 'text/html')
503 self.send_header('Cache-Control', 'no-store')
504 self.end_headers()
506 self.wfile.write('<html><head><title>%s</title></head></html>' %
507 time.time())
509 return True
511 def CacheNoStoreMaxAgeHandler(self):
512 """This request handler yields a page with the title set to the current
513 system time, and does not allow the page to be stored even though max-age
514 of 60 seconds is specified."""
516 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
517 return False
519 self.send_response(200)
520 self.send_header('Content-Type', 'text/html')
521 self.send_header('Cache-Control', 'max-age=60, no-store')
522 self.end_headers()
524 self.wfile.write('<html><head><title>%s</title></head></html>' %
525 time.time())
527 return True
530 def CacheNoTransformHandler(self):
531 """This request handler yields a page with the title set to the current
532 system time, and does not allow the content to transformed during
533 user-agent caching"""
535 if not self._ShouldHandleRequest("/cache/no-transform"):
536 return False
538 self.send_response(200)
539 self.send_header('Content-Type', 'text/html')
540 self.send_header('Cache-Control', 'no-transform')
541 self.end_headers()
543 self.wfile.write('<html><head><title>%s</title></head></html>' %
544 time.time())
546 return True
548 def EchoHeader(self):
549 """This handler echoes back the value of a specific request header."""
551 return self.EchoHeaderHelper("/echoheader")
553 def EchoHeaderCache(self):
554 """This function echoes back the value of a specific request header while
555 allowing caching for 16 hours."""
557 return self.EchoHeaderHelper("/echoheadercache")
559 def EchoHeaderHelper(self, echo_header):
560 """This function echoes back the value of the request header passed in."""
562 if not self._ShouldHandleRequest(echo_header):
563 return False
565 query_char = self.path.find('?')
566 if query_char != -1:
567 header_name = self.path[query_char+1:]
569 self.send_response(200)
570 self.send_header('Content-Type', 'text/plain')
571 if echo_header == '/echoheadercache':
572 self.send_header('Cache-control', 'max-age=60000')
573 else:
574 self.send_header('Cache-control', 'no-cache')
575 # insert a vary header to properly indicate that the cachability of this
576 # request is subject to value of the request header being echoed.
577 if len(header_name) > 0:
578 self.send_header('Vary', header_name)
579 self.end_headers()
581 if len(header_name) > 0:
582 self.wfile.write(self.headers.getheader(header_name))
584 return True
586 def ReadRequestBody(self):
587 """This function reads the body of the current HTTP request, handling
588 both plain and chunked transfer encoded requests."""
590 if self.headers.getheader('transfer-encoding') != 'chunked':
591 length = int(self.headers.getheader('content-length'))
592 return self.rfile.read(length)
594 # Read the request body as chunks.
595 body = ""
596 while True:
597 line = self.rfile.readline()
598 length = int(line, 16)
599 if length == 0:
600 self.rfile.readline()
601 break
602 body += self.rfile.read(length)
603 self.rfile.read(2)
604 return body
606 def EchoHandler(self):
607 """This handler just echoes back the payload of the request, for testing
608 form submission."""
610 if not self._ShouldHandleRequest("/echo"):
611 return False
613 self.send_response(200)
614 self.send_header('Content-Type', 'text/html')
615 self.end_headers()
616 self.wfile.write(self.ReadRequestBody())
617 return True
619 def EchoTitleHandler(self):
620 """This handler is like Echo, but sets the page title to the request."""
622 if not self._ShouldHandleRequest("/echotitle"):
623 return False
625 self.send_response(200)
626 self.send_header('Content-Type', 'text/html')
627 self.end_headers()
628 request = self.ReadRequestBody()
629 self.wfile.write('<html><head><title>')
630 self.wfile.write(request)
631 self.wfile.write('</title></head></html>')
632 return True
634 def EchoAllHandler(self):
635 """This handler yields a (more) human-readable page listing information
636 about the request header & contents."""
638 if not self._ShouldHandleRequest("/echoall"):
639 return False
641 self.send_response(200)
642 self.send_header('Content-Type', 'text/html')
643 self.end_headers()
644 self.wfile.write('<html><head><style>'
645 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
646 '</style></head><body>'
647 '<div style="float: right">'
648 '<a href="/echo">back to referring page</a></div>'
649 '<h1>Request Body:</h1><pre>')
651 if self.command == 'POST' or self.command == 'PUT':
652 qs = self.ReadRequestBody()
653 params = cgi.parse_qs(qs, keep_blank_values=1)
655 for param in params:
656 self.wfile.write('%s=%s\n' % (param, params[param][0]))
658 self.wfile.write('</pre>')
660 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
662 self.wfile.write('</body></html>')
663 return True
665 def DownloadHandler(self):
666 """This handler sends a downloadable file with or without reporting
667 the size (6K)."""
669 if self.path.startswith("/download-unknown-size"):
670 send_length = False
671 elif self.path.startswith("/download-known-size"):
672 send_length = True
673 else:
674 return False
677 # The test which uses this functionality is attempting to send
678 # small chunks of data to the client. Use a fairly large buffer
679 # so that we'll fill chrome's IO buffer enough to force it to
680 # actually write the data.
681 # See also the comments in the client-side of this test in
682 # download_uitest.cc
684 size_chunk1 = 35*1024
685 size_chunk2 = 10*1024
687 self.send_response(200)
688 self.send_header('Content-Type', 'application/octet-stream')
689 self.send_header('Cache-Control', 'max-age=0')
690 if send_length:
691 self.send_header('Content-Length', size_chunk1 + size_chunk2)
692 self.end_headers()
694 # First chunk of data:
695 self.wfile.write("*" * size_chunk1)
696 self.wfile.flush()
698 # handle requests until one of them clears this flag.
699 self.server.wait_for_download = True
700 while self.server.wait_for_download:
701 self.server.handle_request()
703 # Second chunk of data:
704 self.wfile.write("*" * size_chunk2)
705 return True
707 def DownloadFinishHandler(self):
708 """This handler just tells the server to finish the current download."""
710 if not self._ShouldHandleRequest("/download-finish"):
711 return False
713 self.server.wait_for_download = False
714 self.send_response(200)
715 self.send_header('Content-Type', 'text/html')
716 self.send_header('Cache-Control', 'max-age=0')
717 self.end_headers()
718 return True
720 def _ReplaceFileData(self, data, query_parameters):
721 """Replaces matching substrings in a file.
723 If the 'replace_text' URL query parameter is present, it is expected to be
724 of the form old_text:new_text, which indicates that any old_text strings in
725 the file are replaced with new_text. Multiple 'replace_text' parameters may
726 be specified.
728 If the parameters are not present, |data| is returned.
731 query_dict = cgi.parse_qs(query_parameters)
732 replace_text_values = query_dict.get('replace_text', [])
733 for replace_text_value in replace_text_values:
734 replace_text_args = replace_text_value.split(':')
735 if len(replace_text_args) != 2:
736 raise ValueError(
737 'replace_text must be of form old_text:new_text. Actual value: %s' %
738 replace_text_value)
739 old_text_b64, new_text_b64 = replace_text_args
740 old_text = base64.urlsafe_b64decode(old_text_b64)
741 new_text = base64.urlsafe_b64decode(new_text_b64)
742 data = data.replace(old_text, new_text)
743 return data
745 def ZipFileHandler(self):
746 """This handler sends the contents of the requested file in compressed form.
747 Can pass in a parameter that specifies that the content length be
748 C - the compressed size (OK),
749 U - the uncompressed size (Non-standard, but handled),
750 S - less than compressed (OK because we keep going),
751 M - larger than compressed but less than uncompressed (an error),
752 L - larger than uncompressed (an error)
753 Example: compressedfiles/Picture_1.doc?C
756 prefix = "/compressedfiles/"
757 if not self.path.startswith(prefix):
758 return False
760 # Consume a request body if present.
761 if self.command == 'POST' or self.command == 'PUT' :
762 self.ReadRequestBody()
764 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
766 if not query in ('C', 'U', 'S', 'M', 'L'):
767 return False
769 sub_path = url_path[len(prefix):]
770 entries = sub_path.split('/')
771 file_path = os.path.join(self.server.data_dir, *entries)
772 if os.path.isdir(file_path):
773 file_path = os.path.join(file_path, 'index.html')
775 if not os.path.isfile(file_path):
776 print "File not found " + sub_path + " full path:" + file_path
777 self.send_error(404)
778 return True
780 f = open(file_path, "rb")
781 data = f.read()
782 uncompressed_len = len(data)
783 f.close()
785 # Compress the data.
786 data = zlib.compress(data)
787 compressed_len = len(data)
789 content_length = compressed_len
790 if query == 'U':
791 content_length = uncompressed_len
792 elif query == 'S':
793 content_length = compressed_len / 2
794 elif query == 'M':
795 content_length = (compressed_len + uncompressed_len) / 2
796 elif query == 'L':
797 content_length = compressed_len + uncompressed_len
799 self.send_response(200)
800 self.send_header('Content-Type', 'application/msword')
801 self.send_header('Content-encoding', 'deflate')
802 self.send_header('Connection', 'close')
803 self.send_header('Content-Length', content_length)
804 self.send_header('ETag', '\'' + file_path + '\'')
805 self.end_headers()
807 self.wfile.write(data)
809 return True
811 def FileHandler(self):
812 """This handler sends the contents of the requested file. Wow, it's like
813 a real webserver!"""
815 prefix = self.server.file_root_url
816 if not self.path.startswith(prefix):
817 return False
818 return self._FileHandlerHelper(prefix)
820 def PostOnlyFileHandler(self):
821 """This handler sends the contents of the requested file on a POST."""
823 prefix = urlparse.urljoin(self.server.file_root_url, 'post/')
824 if not self.path.startswith(prefix):
825 return False
826 return self._FileHandlerHelper(prefix)
828 def _FileHandlerHelper(self, prefix):
829 request_body = ''
830 if self.command == 'POST' or self.command == 'PUT':
831 # Consume a request body if present.
832 request_body = self.ReadRequestBody()
834 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
835 query_dict = cgi.parse_qs(query)
837 expected_body = query_dict.get('expected_body', [])
838 if expected_body and request_body not in expected_body:
839 self.send_response(404)
840 self.end_headers()
841 self.wfile.write('')
842 return True
844 expected_headers = query_dict.get('expected_headers', [])
845 for expected_header in expected_headers:
846 header_name, expected_value = expected_header.split(':')
847 if self.headers.getheader(header_name) != expected_value:
848 self.send_response(404)
849 self.end_headers()
850 self.wfile.write('')
851 return True
853 sub_path = url_path[len(prefix):]
854 entries = sub_path.split('/')
855 file_path = os.path.join(self.server.data_dir, *entries)
856 if os.path.isdir(file_path):
857 file_path = os.path.join(file_path, 'index.html')
859 if not os.path.isfile(file_path):
860 print "File not found " + sub_path + " full path:" + file_path
861 self.send_error(404)
862 return True
864 f = open(file_path, "rb")
865 data = f.read()
866 f.close()
868 data = self._ReplaceFileData(data, query)
870 old_protocol_version = self.protocol_version
872 # If file.mock-http-headers exists, it contains the headers we
873 # should send. Read them in and parse them.
874 headers_path = file_path + '.mock-http-headers'
875 if os.path.isfile(headers_path):
876 f = open(headers_path, "r")
878 # "HTTP/1.1 200 OK"
879 response = f.readline()
880 http_major, http_minor, status_code = re.findall(
881 'HTTP/(\d+).(\d+) (\d+)', response)[0]
882 self.protocol_version = "HTTP/%s.%s" % (http_major, http_minor)
883 self.send_response(int(status_code))
885 for line in f:
886 header_values = re.findall('(\S+):\s*(.*)', line)
887 if len(header_values) > 0:
888 # "name: value"
889 name, value = header_values[0]
890 self.send_header(name, value)
891 f.close()
892 else:
893 # Could be more generic once we support mime-type sniffing, but for
894 # now we need to set it explicitly.
896 range_header = self.headers.get('Range')
897 if range_header and range_header.startswith('bytes='):
898 # Note this doesn't handle all valid byte range_header values (i.e.
899 # left open ended ones), just enough for what we needed so far.
900 range_header = range_header[6:].split('-')
901 start = int(range_header[0])
902 if range_header[1]:
903 end = int(range_header[1])
904 else:
905 end = len(data) - 1
907 self.send_response(206)
908 content_range = ('bytes ' + str(start) + '-' + str(end) + '/' +
909 str(len(data)))
910 self.send_header('Content-Range', content_range)
911 data = data[start: end + 1]
912 else:
913 self.send_response(200)
915 self.send_header('Content-Type', self.GetMIMETypeFromName(file_path))
916 self.send_header('Accept-Ranges', 'bytes')
917 self.send_header('Content-Length', len(data))
918 self.send_header('ETag', '\'' + file_path + '\'')
919 self.end_headers()
921 if (self.command != 'HEAD'):
922 self.wfile.write(data)
924 self.protocol_version = old_protocol_version
925 return True
927 def SetCookieHandler(self):
928 """This handler just sets a cookie, for testing cookie handling."""
930 if not self._ShouldHandleRequest("/set-cookie"):
931 return False
933 query_char = self.path.find('?')
934 if query_char != -1:
935 cookie_values = self.path[query_char + 1:].split('&')
936 else:
937 cookie_values = ("",)
938 self.send_response(200)
939 self.send_header('Content-Type', 'text/html')
940 for cookie_value in cookie_values:
941 self.send_header('Set-Cookie', '%s' % cookie_value)
942 self.end_headers()
943 for cookie_value in cookie_values:
944 self.wfile.write('%s' % cookie_value)
945 return True
947 def SetManyCookiesHandler(self):
948 """This handler just sets a given number of cookies, for testing handling
949 of large numbers of cookies."""
951 if not self._ShouldHandleRequest("/set-many-cookies"):
952 return False
954 query_char = self.path.find('?')
955 if query_char != -1:
956 num_cookies = int(self.path[query_char + 1:])
957 else:
958 num_cookies = 0
959 self.send_response(200)
960 self.send_header('', 'text/html')
961 for _i in range(0, num_cookies):
962 self.send_header('Set-Cookie', 'a=')
963 self.end_headers()
964 self.wfile.write('%d cookies were sent' % num_cookies)
965 return True
967 def ExpectAndSetCookieHandler(self):
968 """Expects some cookies to be sent, and if they are, sets more cookies.
970 The expect parameter specifies a required cookie. May be specified multiple
971 times.
972 The set parameter specifies a cookie to set if all required cookies are
973 preset. May be specified multiple times.
974 The data parameter specifies the response body data to be returned."""
976 if not self._ShouldHandleRequest("/expect-and-set-cookie"):
977 return False
979 _, _, _, _, query, _ = urlparse.urlparse(self.path)
980 query_dict = cgi.parse_qs(query)
981 cookies = set()
982 if 'Cookie' in self.headers:
983 cookie_header = self.headers.getheader('Cookie')
984 cookies.update([s.strip() for s in cookie_header.split(';')])
985 got_all_expected_cookies = True
986 for expected_cookie in query_dict.get('expect', []):
987 if expected_cookie not in cookies:
988 got_all_expected_cookies = False
989 self.send_response(200)
990 self.send_header('Content-Type', 'text/html')
991 if got_all_expected_cookies:
992 for cookie_value in query_dict.get('set', []):
993 self.send_header('Set-Cookie', '%s' % cookie_value)
994 self.end_headers()
995 for data_value in query_dict.get('data', []):
996 self.wfile.write(data_value)
997 return True
999 def SetHeaderHandler(self):
1000 """This handler sets a response header. Parameters are in the
1001 key%3A%20value&key2%3A%20value2 format."""
1003 if not self._ShouldHandleRequest("/set-header"):
1004 return False
1006 query_char = self.path.find('?')
1007 if query_char != -1:
1008 headers_values = self.path[query_char + 1:].split('&')
1009 else:
1010 headers_values = ("",)
1011 self.send_response(200)
1012 self.send_header('Content-Type', 'text/html')
1013 for header_value in headers_values:
1014 header_value = urllib.unquote(header_value)
1015 (key, value) = header_value.split(': ', 1)
1016 self.send_header(key, value)
1017 self.end_headers()
1018 for header_value in headers_values:
1019 self.wfile.write('%s' % header_value)
1020 return True
1022 def AuthBasicHandler(self):
1023 """This handler tests 'Basic' authentication. It just sends a page with
1024 title 'user/pass' if you succeed."""
1026 if not self._ShouldHandleRequest("/auth-basic"):
1027 return False
1029 username = userpass = password = b64str = ""
1030 expected_password = 'secret'
1031 realm = 'testrealm'
1032 set_cookie_if_challenged = False
1034 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1035 query_params = cgi.parse_qs(query, True)
1036 if 'set-cookie-if-challenged' in query_params:
1037 set_cookie_if_challenged = True
1038 if 'password' in query_params:
1039 expected_password = query_params['password'][0]
1040 if 'realm' in query_params:
1041 realm = query_params['realm'][0]
1043 auth = self.headers.getheader('authorization')
1044 try:
1045 if not auth:
1046 raise Exception('no auth')
1047 b64str = re.findall(r'Basic (\S+)', auth)[0]
1048 userpass = base64.b64decode(b64str)
1049 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
1050 if password != expected_password:
1051 raise Exception('wrong password')
1052 except Exception, e:
1053 # Authentication failed.
1054 self.send_response(401)
1055 self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
1056 self.send_header('Content-Type', 'text/html')
1057 if set_cookie_if_challenged:
1058 self.send_header('Set-Cookie', 'got_challenged=true')
1059 self.end_headers()
1060 self.wfile.write('<html><head>')
1061 self.wfile.write('<title>Denied: %s</title>' % e)
1062 self.wfile.write('</head><body>')
1063 self.wfile.write('auth=%s<p>' % auth)
1064 self.wfile.write('b64str=%s<p>' % b64str)
1065 self.wfile.write('username: %s<p>' % username)
1066 self.wfile.write('userpass: %s<p>' % userpass)
1067 self.wfile.write('password: %s<p>' % password)
1068 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1069 self.wfile.write('</body></html>')
1070 return True
1072 # Authentication successful. (Return a cachable response to allow for
1073 # testing cached pages that require authentication.)
1074 old_protocol_version = self.protocol_version
1075 self.protocol_version = "HTTP/1.1"
1077 if_none_match = self.headers.getheader('if-none-match')
1078 if if_none_match == "abc":
1079 self.send_response(304)
1080 self.end_headers()
1081 elif url_path.endswith(".gif"):
1082 # Using chrome/test/data/google/logo.gif as the test image
1083 test_image_path = ['google', 'logo.gif']
1084 gif_path = os.path.join(self.server.data_dir, *test_image_path)
1085 if not os.path.isfile(gif_path):
1086 self.send_error(404)
1087 self.protocol_version = old_protocol_version
1088 return True
1090 f = open(gif_path, "rb")
1091 data = f.read()
1092 f.close()
1094 self.send_response(200)
1095 self.send_header('Content-Type', 'image/gif')
1096 self.send_header('Cache-control', 'max-age=60000')
1097 self.send_header('Etag', 'abc')
1098 self.end_headers()
1099 self.wfile.write(data)
1100 else:
1101 self.send_response(200)
1102 self.send_header('Content-Type', 'text/html')
1103 self.send_header('Cache-control', 'max-age=60000')
1104 self.send_header('Etag', 'abc')
1105 self.end_headers()
1106 self.wfile.write('<html><head>')
1107 self.wfile.write('<title>%s/%s</title>' % (username, password))
1108 self.wfile.write('</head><body>')
1109 self.wfile.write('auth=%s<p>' % auth)
1110 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1111 self.wfile.write('</body></html>')
1113 self.protocol_version = old_protocol_version
1114 return True
1116 def GetNonce(self, force_reset=False):
1117 """Returns a nonce that's stable per request path for the server's lifetime.
1118 This is a fake implementation. A real implementation would only use a given
1119 nonce a single time (hence the name n-once). However, for the purposes of
1120 unittesting, we don't care about the security of the nonce.
1122 Args:
1123 force_reset: Iff set, the nonce will be changed. Useful for testing the
1124 "stale" response.
1127 if force_reset or not self.server.nonce_time:
1128 self.server.nonce_time = time.time()
1129 return hashlib.md5('privatekey%s%d' %
1130 (self.path, self.server.nonce_time)).hexdigest()
1132 def AuthDigestHandler(self):
1133 """This handler tests 'Digest' authentication.
1135 It just sends a page with title 'user/pass' if you succeed.
1137 A stale response is sent iff "stale" is present in the request path.
1140 if not self._ShouldHandleRequest("/auth-digest"):
1141 return False
1143 stale = 'stale' in self.path
1144 nonce = self.GetNonce(force_reset=stale)
1145 opaque = hashlib.md5('opaque').hexdigest()
1146 password = 'secret'
1147 realm = 'testrealm'
1149 auth = self.headers.getheader('authorization')
1150 pairs = {}
1151 try:
1152 if not auth:
1153 raise Exception('no auth')
1154 if not auth.startswith('Digest'):
1155 raise Exception('not digest')
1156 # Pull out all the name="value" pairs as a dictionary.
1157 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1159 # Make sure it's all valid.
1160 if pairs['nonce'] != nonce:
1161 raise Exception('wrong nonce')
1162 if pairs['opaque'] != opaque:
1163 raise Exception('wrong opaque')
1165 # Check the 'response' value and make sure it matches our magic hash.
1166 # See http://www.ietf.org/rfc/rfc2617.txt
1167 hash_a1 = hashlib.md5(
1168 ':'.join([pairs['username'], realm, password])).hexdigest()
1169 hash_a2 = hashlib.md5(':'.join([self.command, pairs['uri']])).hexdigest()
1170 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
1171 response = hashlib.md5(':'.join([hash_a1, nonce, pairs['nc'],
1172 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1173 else:
1174 response = hashlib.md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
1176 if pairs['response'] != response:
1177 raise Exception('wrong password')
1178 except Exception, e:
1179 # Authentication failed.
1180 self.send_response(401)
1181 hdr = ('Digest '
1182 'realm="%s", '
1183 'domain="/", '
1184 'qop="auth", '
1185 'algorithm=MD5, '
1186 'nonce="%s", '
1187 'opaque="%s"') % (realm, nonce, opaque)
1188 if stale:
1189 hdr += ', stale="TRUE"'
1190 self.send_header('WWW-Authenticate', hdr)
1191 self.send_header('Content-Type', 'text/html')
1192 self.end_headers()
1193 self.wfile.write('<html><head>')
1194 self.wfile.write('<title>Denied: %s</title>' % e)
1195 self.wfile.write('</head><body>')
1196 self.wfile.write('auth=%s<p>' % auth)
1197 self.wfile.write('pairs=%s<p>' % pairs)
1198 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1199 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1200 self.wfile.write('</body></html>')
1201 return True
1203 # Authentication successful.
1204 self.send_response(200)
1205 self.send_header('Content-Type', 'text/html')
1206 self.end_headers()
1207 self.wfile.write('<html><head>')
1208 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1209 self.wfile.write('</head><body>')
1210 self.wfile.write('auth=%s<p>' % auth)
1211 self.wfile.write('pairs=%s<p>' % pairs)
1212 self.wfile.write('</body></html>')
1214 return True
1216 def SlowServerHandler(self):
1217 """Wait for the user suggested time before responding. The syntax is
1218 /slow?0.5 to wait for half a second."""
1220 if not self._ShouldHandleRequest("/slow"):
1221 return False
1222 query_char = self.path.find('?')
1223 wait_sec = 1.0
1224 if query_char >= 0:
1225 try:
1226 wait_sec = int(self.path[query_char + 1:])
1227 except ValueError:
1228 pass
1229 time.sleep(wait_sec)
1230 self.send_response(200)
1231 self.send_header('Content-Type', 'text/plain')
1232 self.end_headers()
1233 self.wfile.write("waited %d seconds" % wait_sec)
1234 return True
1236 def ChunkedServerHandler(self):
1237 """Send chunked response. Allows to specify chunks parameters:
1238 - waitBeforeHeaders - ms to wait before sending headers
1239 - waitBetweenChunks - ms to wait between chunks
1240 - chunkSize - size of each chunk in bytes
1241 - chunksNumber - number of chunks
1242 Example: /chunked?waitBeforeHeaders=1000&chunkSize=5&chunksNumber=5
1243 waits one second, then sends headers and five chunks five bytes each."""
1245 if not self._ShouldHandleRequest("/chunked"):
1246 return False
1247 query_char = self.path.find('?')
1248 chunkedSettings = {'waitBeforeHeaders' : 0,
1249 'waitBetweenChunks' : 0,
1250 'chunkSize' : 5,
1251 'chunksNumber' : 5}
1252 if query_char >= 0:
1253 params = self.path[query_char + 1:].split('&')
1254 for param in params:
1255 keyValue = param.split('=')
1256 if len(keyValue) == 2:
1257 try:
1258 chunkedSettings[keyValue[0]] = int(keyValue[1])
1259 except ValueError:
1260 pass
1261 time.sleep(0.001 * chunkedSettings['waitBeforeHeaders'])
1262 self.protocol_version = 'HTTP/1.1' # Needed for chunked encoding
1263 self.send_response(200)
1264 self.send_header('Content-Type', 'text/plain')
1265 self.send_header('Connection', 'close')
1266 self.send_header('Transfer-Encoding', 'chunked')
1267 self.end_headers()
1268 # Chunked encoding: sending all chunks, then final zero-length chunk and
1269 # then final CRLF.
1270 for i in range(0, chunkedSettings['chunksNumber']):
1271 if i > 0:
1272 time.sleep(0.001 * chunkedSettings['waitBetweenChunks'])
1273 self.sendChunkHelp('*' * chunkedSettings['chunkSize'])
1274 self.wfile.flush() # Keep in mind that we start flushing only after 1kb.
1275 self.sendChunkHelp('')
1276 return True
1278 def ContentTypeHandler(self):
1279 """Returns a string of html with the given content type. E.g.,
1280 /contenttype?text/css returns an html file with the Content-Type
1281 header set to text/css."""
1283 if not self._ShouldHandleRequest("/contenttype"):
1284 return False
1285 query_char = self.path.find('?')
1286 content_type = self.path[query_char + 1:].strip()
1287 if not content_type:
1288 content_type = 'text/html'
1289 self.send_response(200)
1290 self.send_header('Content-Type', content_type)
1291 self.end_headers()
1292 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n")
1293 return True
1295 def NoContentHandler(self):
1296 """Returns a 204 No Content response."""
1298 if not self._ShouldHandleRequest("/nocontent"):
1299 return False
1300 self.send_response(204)
1301 self.end_headers()
1302 return True
1304 def ServerRedirectHandler(self):
1305 """Sends a server redirect to the given URL. The syntax is
1306 '/server-redirect?http://foo.bar/asdf' to redirect to
1307 'http://foo.bar/asdf'"""
1309 test_name = "/server-redirect"
1310 if not self._ShouldHandleRequest(test_name):
1311 return False
1313 query_char = self.path.find('?')
1314 if query_char < 0 or len(self.path) <= query_char + 1:
1315 self.sendRedirectHelp(test_name)
1316 return True
1317 dest = self.path[query_char + 1:]
1319 self.send_response(301) # moved permanently
1320 self.send_header('Location', dest)
1321 self.send_header('Content-Type', 'text/html')
1322 self.end_headers()
1323 self.wfile.write('<html><head>')
1324 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1326 return True
1328 def ClientRedirectHandler(self):
1329 """Sends a client redirect to the given URL. The syntax is
1330 '/client-redirect?http://foo.bar/asdf' to redirect to
1331 'http://foo.bar/asdf'"""
1333 test_name = "/client-redirect"
1334 if not self._ShouldHandleRequest(test_name):
1335 return False
1337 query_char = self.path.find('?')
1338 if query_char < 0 or len(self.path) <= query_char + 1:
1339 self.sendRedirectHelp(test_name)
1340 return True
1341 dest = self.path[query_char + 1:]
1343 self.send_response(200)
1344 self.send_header('Content-Type', 'text/html')
1345 self.end_headers()
1346 self.wfile.write('<html><head>')
1347 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1348 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1350 return True
1352 def MultipartHandler(self):
1353 """Send a multipart response (10 text/html pages)."""
1355 test_name = '/multipart'
1356 if not self._ShouldHandleRequest(test_name):
1357 return False
1359 num_frames = 10
1360 bound = '12345'
1361 self.send_response(200)
1362 self.send_header('Content-Type',
1363 'multipart/x-mixed-replace;boundary=' + bound)
1364 self.end_headers()
1366 for i in xrange(num_frames):
1367 self.wfile.write('--' + bound + '\r\n')
1368 self.wfile.write('Content-Type: text/html\r\n\r\n')
1369 self.wfile.write('<title>page ' + str(i) + '</title>')
1370 self.wfile.write('page ' + str(i))
1372 self.wfile.write('--' + bound + '--')
1373 return True
1375 def GetSSLSessionCacheHandler(self):
1376 """Send a reply containing a log of the session cache operations."""
1378 if not self._ShouldHandleRequest('/ssl-session-cache'):
1379 return False
1381 self.send_response(200)
1382 self.send_header('Content-Type', 'text/plain')
1383 self.end_headers()
1384 try:
1385 for (action, sessionID) in self.server.session_cache.log:
1386 self.wfile.write('%s\t%s\n' % (action, sessionID.encode('hex')))
1387 except AttributeError:
1388 self.wfile.write('Pass --https-record-resume in order to use' +
1389 ' this request')
1390 return True
1392 def SSLManySmallRecords(self):
1393 """Sends a reply consisting of a variety of small writes. These will be
1394 translated into a series of small SSL records when used over an HTTPS
1395 server."""
1397 if not self._ShouldHandleRequest('/ssl-many-small-records'):
1398 return False
1400 self.send_response(200)
1401 self.send_header('Content-Type', 'text/plain')
1402 self.end_headers()
1404 # Write ~26K of data, in 1350 byte chunks
1405 for i in xrange(20):
1406 self.wfile.write('*' * 1350)
1407 self.wfile.flush()
1408 return True
1410 def GetChannelID(self):
1411 """Send a reply containing the hashed ChannelID that the client provided."""
1413 if not self._ShouldHandleRequest('/channel-id'):
1414 return False
1416 self.send_response(200)
1417 self.send_header('Content-Type', 'text/plain')
1418 self.end_headers()
1419 channel_id = self.server.tlsConnection.channel_id.tostring()
1420 self.wfile.write(hashlib.sha256(channel_id).digest().encode('base64'))
1421 return True
1423 def CloseSocketHandler(self):
1424 """Closes the socket without sending anything."""
1426 if not self._ShouldHandleRequest('/close-socket'):
1427 return False
1429 self.wfile.close()
1430 return True
1432 def RangeResetHandler(self):
1433 """Send data broken up by connection resets every N (default 4K) bytes.
1434 Support range requests. If the data requested doesn't straddle a reset
1435 boundary, it will all be sent. Used for testing resuming downloads."""
1437 def DataForRange(start, end):
1438 """Data to be provided for a particular range of bytes."""
1439 # Offset and scale to avoid too obvious (and hence potentially
1440 # collidable) data.
1441 return ''.join([chr(y % 256)
1442 for y in range(start * 2 + 15, end * 2 + 15, 2)])
1444 if not self._ShouldHandleRequest('/rangereset'):
1445 return False
1447 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1449 # Defaults
1450 size = 8000
1451 # Note that the rst is sent just before sending the rst_boundary byte.
1452 rst_boundary = 4000
1453 respond_to_range = True
1454 hold_for_signal = False
1455 rst_limit = -1
1456 token = 'DEFAULT'
1457 fail_precondition = 0
1458 send_verifiers = True
1460 # Parse the query
1461 qdict = urlparse.parse_qs(query, True)
1462 if 'size' in qdict:
1463 size = int(qdict['size'][0])
1464 if 'rst_boundary' in qdict:
1465 rst_boundary = int(qdict['rst_boundary'][0])
1466 if 'token' in qdict:
1467 # Identifying token for stateful tests.
1468 token = qdict['token'][0]
1469 if 'rst_limit' in qdict:
1470 # Max number of rsts for a given token.
1471 rst_limit = int(qdict['rst_limit'][0])
1472 if 'bounce_range' in qdict:
1473 respond_to_range = False
1474 if 'hold' in qdict:
1475 # Note that hold_for_signal will not work with null range requests;
1476 # see TODO below.
1477 hold_for_signal = True
1478 if 'no_verifiers' in qdict:
1479 send_verifiers = False
1480 if 'fail_precondition' in qdict:
1481 fail_precondition = int(qdict['fail_precondition'][0])
1483 # Record already set information, or set it.
1484 rst_limit = TestPageHandler.rst_limits.setdefault(token, rst_limit)
1485 if rst_limit != 0:
1486 TestPageHandler.rst_limits[token] -= 1
1487 fail_precondition = TestPageHandler.fail_precondition.setdefault(
1488 token, fail_precondition)
1489 if fail_precondition != 0:
1490 TestPageHandler.fail_precondition[token] -= 1
1492 first_byte = 0
1493 last_byte = size - 1
1495 # Does that define what we want to return, or do we need to apply
1496 # a range?
1497 range_response = False
1498 range_header = self.headers.getheader('range')
1499 if range_header and respond_to_range:
1500 mo = re.match("bytes=(\d*)-(\d*)", range_header)
1501 if mo.group(1):
1502 first_byte = int(mo.group(1))
1503 if mo.group(2):
1504 last_byte = int(mo.group(2))
1505 if last_byte > size - 1:
1506 last_byte = size - 1
1507 range_response = True
1508 if last_byte < first_byte:
1509 return False
1511 if (fail_precondition and
1512 (self.headers.getheader('If-Modified-Since') or
1513 self.headers.getheader('If-Match'))):
1514 self.send_response(412)
1515 self.end_headers()
1516 return True
1518 if range_response:
1519 self.send_response(206)
1520 self.send_header('Content-Range',
1521 'bytes %d-%d/%d' % (first_byte, last_byte, size))
1522 else:
1523 self.send_response(200)
1524 self.send_header('Content-Type', 'application/octet-stream')
1525 self.send_header('Content-Length', last_byte - first_byte + 1)
1526 if send_verifiers:
1527 self.send_header('Etag', '"XYZZY"')
1528 self.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST')
1529 self.end_headers()
1531 if hold_for_signal:
1532 # TODO(rdsmith/phajdan.jr): http://crbug.com/169519: Without writing
1533 # a single byte, the self.server.handle_request() below hangs
1534 # without processing new incoming requests.
1535 self.wfile.write(DataForRange(first_byte, first_byte + 1))
1536 first_byte = first_byte + 1
1537 # handle requests until one of them clears this flag.
1538 self.server.wait_for_download = True
1539 while self.server.wait_for_download:
1540 self.server.handle_request()
1542 possible_rst = ((first_byte / rst_boundary) + 1) * rst_boundary
1543 if possible_rst >= last_byte or rst_limit == 0:
1544 # No RST has been requested in this range, so we don't need to
1545 # do anything fancy; just write the data and let the python
1546 # infrastructure close the connection.
1547 self.wfile.write(DataForRange(first_byte, last_byte + 1))
1548 self.wfile.flush()
1549 return True
1551 # We're resetting the connection part way in; go to the RST
1552 # boundary and then send an RST.
1553 # Because socket semantics do not guarantee that all the data will be
1554 # sent when using the linger semantics to hard close a socket,
1555 # we send the data and then wait for our peer to release us
1556 # before sending the reset.
1557 data = DataForRange(first_byte, possible_rst)
1558 self.wfile.write(data)
1559 self.wfile.flush()
1560 self.server.wait_for_download = True
1561 while self.server.wait_for_download:
1562 self.server.handle_request()
1563 l_onoff = 1 # Linger is active.
1564 l_linger = 0 # Seconds to linger for.
1565 self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
1566 struct.pack('ii', l_onoff, l_linger))
1568 # Close all duplicates of the underlying socket to force the RST.
1569 self.wfile.close()
1570 self.rfile.close()
1571 self.connection.close()
1573 return True
1575 def DefaultResponseHandler(self):
1576 """This is the catch-all response handler for requests that aren't handled
1577 by one of the special handlers above.
1578 Note that we specify the content-length as without it the https connection
1579 is not closed properly (and the browser keeps expecting data)."""
1581 contents = "Default response given for path: " + self.path
1582 self.send_response(200)
1583 self.send_header('Content-Type', 'text/html')
1584 self.send_header('Content-Length', len(contents))
1585 self.end_headers()
1586 if (self.command != 'HEAD'):
1587 self.wfile.write(contents)
1588 return True
1590 def RedirectConnectHandler(self):
1591 """Sends a redirect to the CONNECT request for www.redirect.com. This
1592 response is not specified by the RFC, so the browser should not follow
1593 the redirect."""
1595 if (self.path.find("www.redirect.com") < 0):
1596 return False
1598 dest = "http://www.destination.com/foo.js"
1600 self.send_response(302) # moved temporarily
1601 self.send_header('Location', dest)
1602 self.send_header('Connection', 'close')
1603 self.end_headers()
1604 return True
1606 def ServerAuthConnectHandler(self):
1607 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1608 response doesn't make sense because the proxy server cannot request
1609 server authentication."""
1611 if (self.path.find("www.server-auth.com") < 0):
1612 return False
1614 challenge = 'Basic realm="WallyWorld"'
1616 self.send_response(401) # unauthorized
1617 self.send_header('WWW-Authenticate', challenge)
1618 self.send_header('Connection', 'close')
1619 self.end_headers()
1620 return True
1622 def DefaultConnectResponseHandler(self):
1623 """This is the catch-all response handler for CONNECT requests that aren't
1624 handled by one of the special handlers above. Real Web servers respond
1625 with 400 to CONNECT requests."""
1627 contents = "Your client has issued a malformed or illegal request."
1628 self.send_response(400) # bad request
1629 self.send_header('Content-Type', 'text/html')
1630 self.send_header('Content-Length', len(contents))
1631 self.end_headers()
1632 self.wfile.write(contents)
1633 return True
1635 # called by the redirect handling function when there is no parameter
1636 def sendRedirectHelp(self, redirect_name):
1637 self.send_response(200)
1638 self.send_header('Content-Type', 'text/html')
1639 self.end_headers()
1640 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1641 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1642 self.wfile.write('</body></html>')
1644 # called by chunked handling function
1645 def sendChunkHelp(self, chunk):
1646 # Each chunk consists of: chunk size (hex), CRLF, chunk body, CRLF
1647 self.wfile.write('%X\r\n' % len(chunk))
1648 self.wfile.write(chunk)
1649 self.wfile.write('\r\n')
1652 class OCSPHandler(testserver_base.BasePageHandler):
1653 def __init__(self, request, client_address, socket_server):
1654 handlers = [self.OCSPResponse]
1655 self.ocsp_response = socket_server.ocsp_response
1656 testserver_base.BasePageHandler.__init__(self, request, client_address,
1657 socket_server, [], handlers, [],
1658 handlers, [])
1660 def OCSPResponse(self):
1661 self.send_response(200)
1662 self.send_header('Content-Type', 'application/ocsp-response')
1663 self.send_header('Content-Length', str(len(self.ocsp_response)))
1664 self.end_headers()
1666 self.wfile.write(self.ocsp_response)
1669 class TCPEchoHandler(SocketServer.BaseRequestHandler):
1670 """The RequestHandler class for TCP echo server.
1672 It is instantiated once per connection to the server, and overrides the
1673 handle() method to implement communication to the client.
1676 def handle(self):
1677 """Handles the request from the client and constructs a response."""
1679 data = self.request.recv(65536).strip()
1680 # Verify the "echo request" message received from the client. Send back
1681 # "echo response" message if "echo request" message is valid.
1682 try:
1683 return_data = echo_message.GetEchoResponseData(data)
1684 if not return_data:
1685 return
1686 except ValueError:
1687 return
1689 self.request.send(return_data)
1692 class UDPEchoHandler(SocketServer.BaseRequestHandler):
1693 """The RequestHandler class for UDP echo server.
1695 It is instantiated once per connection to the server, and overrides the
1696 handle() method to implement communication to the client.
1699 def handle(self):
1700 """Handles the request from the client and constructs a response."""
1702 data = self.request[0].strip()
1703 request_socket = self.request[1]
1704 # Verify the "echo request" message received from the client. Send back
1705 # "echo response" message if "echo request" message is valid.
1706 try:
1707 return_data = echo_message.GetEchoResponseData(data)
1708 if not return_data:
1709 return
1710 except ValueError:
1711 return
1712 request_socket.sendto(return_data, self.client_address)
1715 class BasicAuthProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
1716 """A request handler that behaves as a proxy server which requires
1717 basic authentication. Only CONNECT, GET and HEAD is supported for now.
1720 _AUTH_CREDENTIAL = 'Basic Zm9vOmJhcg==' # foo:bar
1722 def parse_request(self):
1723 """Overrides parse_request to check credential."""
1725 if not BaseHTTPServer.BaseHTTPRequestHandler.parse_request(self):
1726 return False
1728 auth = self.headers.getheader('Proxy-Authorization')
1729 if auth != self._AUTH_CREDENTIAL:
1730 self.send_response(407)
1731 self.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"')
1732 self.end_headers()
1733 return False
1735 return True
1737 def _start_read_write(self, sock):
1738 sock.setblocking(0)
1739 self.request.setblocking(0)
1740 rlist = [self.request, sock]
1741 while True:
1742 ready_sockets, _unused, errors = select.select(rlist, [], [])
1743 if errors:
1744 self.send_response(500)
1745 self.end_headers()
1746 return
1747 for s in ready_sockets:
1748 received = s.recv(1024)
1749 if len(received) == 0:
1750 return
1751 if s == self.request:
1752 other = sock
1753 else:
1754 other = self.request
1755 other.send(received)
1757 def _do_common_method(self):
1758 url = urlparse.urlparse(self.path)
1759 port = url.port
1760 if not port:
1761 if url.scheme == 'http':
1762 port = 80
1763 elif url.scheme == 'https':
1764 port = 443
1765 if not url.hostname or not port:
1766 self.send_response(400)
1767 self.end_headers()
1768 return
1770 if len(url.path) == 0:
1771 path = '/'
1772 else:
1773 path = url.path
1774 if len(url.query) > 0:
1775 path = '%s?%s' % (url.path, url.query)
1777 sock = None
1778 try:
1779 sock = socket.create_connection((url.hostname, port))
1780 sock.send('%s %s %s\r\n' % (
1781 self.command, path, self.protocol_version))
1782 for header in self.headers.headers:
1783 header = header.strip()
1784 if (header.lower().startswith('connection') or
1785 header.lower().startswith('proxy')):
1786 continue
1787 sock.send('%s\r\n' % header)
1788 sock.send('\r\n')
1789 self._start_read_write(sock)
1790 except Exception:
1791 self.send_response(500)
1792 self.end_headers()
1793 finally:
1794 if sock is not None:
1795 sock.close()
1797 def do_CONNECT(self):
1798 try:
1799 pos = self.path.rfind(':')
1800 host = self.path[:pos]
1801 port = int(self.path[pos+1:])
1802 except Exception:
1803 self.send_response(400)
1804 self.end_headers()
1806 try:
1807 sock = socket.create_connection((host, port))
1808 self.send_response(200, 'Connection established')
1809 self.end_headers()
1810 self._start_read_write(sock)
1811 except Exception:
1812 self.send_response(500)
1813 self.end_headers()
1814 finally:
1815 sock.close()
1817 def do_GET(self):
1818 self._do_common_method()
1820 def do_HEAD(self):
1821 self._do_common_method()
1824 class ServerRunner(testserver_base.TestServerRunner):
1825 """TestServerRunner for the net test servers."""
1827 def __init__(self):
1828 super(ServerRunner, self).__init__()
1829 self.__ocsp_server = None
1831 def __make_data_dir(self):
1832 if self.options.data_dir:
1833 if not os.path.isdir(self.options.data_dir):
1834 raise testserver_base.OptionError('specified data dir not found: ' +
1835 self.options.data_dir + ' exiting...')
1836 my_data_dir = self.options.data_dir
1837 else:
1838 # Create the default path to our data dir, relative to the exe dir.
1839 my_data_dir = os.path.join(BASE_DIR, "..", "..", "..", "..",
1840 "test", "data")
1842 #TODO(ibrar): Must use Find* funtion defined in google\tools
1843 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1845 return my_data_dir
1847 def create_server(self, server_data):
1848 port = self.options.port
1849 host = self.options.host
1851 if self.options.server_type == SERVER_HTTP:
1852 if self.options.https:
1853 pem_cert_and_key = None
1854 if self.options.cert_and_key_file:
1855 if not os.path.isfile(self.options.cert_and_key_file):
1856 raise testserver_base.OptionError(
1857 'specified server cert file not found: ' +
1858 self.options.cert_and_key_file + ' exiting...')
1859 pem_cert_and_key = file(self.options.cert_and_key_file, 'r').read()
1860 else:
1861 # generate a new certificate and run an OCSP server for it.
1862 self.__ocsp_server = OCSPServer((host, 0), OCSPHandler)
1863 print ('OCSP server started on %s:%d...' %
1864 (host, self.__ocsp_server.server_port))
1866 ocsp_der = None
1867 ocsp_state = None
1869 if self.options.ocsp == 'ok':
1870 ocsp_state = minica.OCSP_STATE_GOOD
1871 elif self.options.ocsp == 'revoked':
1872 ocsp_state = minica.OCSP_STATE_REVOKED
1873 elif self.options.ocsp == 'invalid':
1874 ocsp_state = minica.OCSP_STATE_INVALID
1875 elif self.options.ocsp == 'unauthorized':
1876 ocsp_state = minica.OCSP_STATE_UNAUTHORIZED
1877 elif self.options.ocsp == 'unknown':
1878 ocsp_state = minica.OCSP_STATE_UNKNOWN
1879 else:
1880 raise testserver_base.OptionError('unknown OCSP status: ' +
1881 self.options.ocsp_status)
1883 (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP(
1884 subject = "127.0.0.1",
1885 ocsp_url = ("http://%s:%d/ocsp" %
1886 (host, self.__ocsp_server.server_port)),
1887 ocsp_state = ocsp_state,
1888 serial = self.options.cert_serial)
1890 self.__ocsp_server.ocsp_response = ocsp_der
1892 for ca_cert in self.options.ssl_client_ca:
1893 if not os.path.isfile(ca_cert):
1894 raise testserver_base.OptionError(
1895 'specified trusted client CA file not found: ' + ca_cert +
1896 ' exiting...')
1897 server = HTTPSServer((host, port), TestPageHandler, pem_cert_and_key,
1898 self.options.ssl_client_auth,
1899 self.options.ssl_client_ca,
1900 self.options.ssl_bulk_cipher,
1901 self.options.record_resume,
1902 self.options.tls_intolerant)
1903 print 'HTTPS server started on %s:%d...' % (host, server.server_port)
1904 else:
1905 server = HTTPServer((host, port), TestPageHandler)
1906 print 'HTTP server started on %s:%d...' % (host, server.server_port)
1908 server.data_dir = self.__make_data_dir()
1909 server.file_root_url = self.options.file_root_url
1910 server_data['port'] = server.server_port
1911 elif self.options.server_type == SERVER_WEBSOCKET:
1912 # Launch pywebsocket via WebSocketServer.
1913 logger = logging.getLogger()
1914 logger.addHandler(logging.StreamHandler())
1915 # TODO(toyoshim): Remove following os.chdir. Currently this operation
1916 # is required to work correctly. It should be fixed from pywebsocket side.
1917 os.chdir(self.__make_data_dir())
1918 websocket_options = WebSocketOptions(host, port, '.')
1919 if self.options.cert_and_key_file:
1920 websocket_options.use_tls = True
1921 websocket_options.private_key = self.options.cert_and_key_file
1922 websocket_options.certificate = self.options.cert_and_key_file
1923 if self.options.ssl_client_auth:
1924 websocket_options.tls_client_auth = True
1925 if len(self.options.ssl_client_ca) != 1:
1926 raise testserver_base.OptionError(
1927 'one trusted client CA file should be specified')
1928 if not os.path.isfile(self.options.ssl_client_ca[0]):
1929 raise testserver_base.OptionError(
1930 'specified trusted client CA file not found: ' +
1931 self.options.ssl_client_ca[0] + ' exiting...')
1932 websocket_options.tls_client_ca = self.options.ssl_client_ca[0]
1933 server = WebSocketServer(websocket_options)
1934 print 'WebSocket server started on %s:%d...' % (host, server.server_port)
1935 server_data['port'] = server.server_port
1936 elif self.options.server_type == SERVER_TCP_ECHO:
1937 # Used for generating the key (randomly) that encodes the "echo request"
1938 # message.
1939 random.seed()
1940 server = TCPEchoServer((host, port), TCPEchoHandler)
1941 print 'Echo TCP server started on port %d...' % server.server_port
1942 server_data['port'] = server.server_port
1943 elif self.options.server_type == SERVER_UDP_ECHO:
1944 # Used for generating the key (randomly) that encodes the "echo request"
1945 # message.
1946 random.seed()
1947 server = UDPEchoServer((host, port), UDPEchoHandler)
1948 print 'Echo UDP server started on port %d...' % server.server_port
1949 server_data['port'] = server.server_port
1950 elif self.options.server_type == SERVER_BASIC_AUTH_PROXY:
1951 server = HTTPServer((host, port), BasicAuthProxyRequestHandler)
1952 print 'BasicAuthProxy server started on port %d...' % server.server_port
1953 server_data['port'] = server.server_port
1954 elif self.options.server_type == SERVER_FTP:
1955 my_data_dir = self.__make_data_dir()
1957 # Instantiate a dummy authorizer for managing 'virtual' users
1958 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
1960 # Define a new user having full r/w permissions and a read-only
1961 # anonymous user
1962 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
1964 authorizer.add_anonymous(my_data_dir)
1966 # Instantiate FTP handler class
1967 ftp_handler = pyftpdlib.ftpserver.FTPHandler
1968 ftp_handler.authorizer = authorizer
1970 # Define a customized banner (string returned when client connects)
1971 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
1972 pyftpdlib.ftpserver.__ver__)
1974 # Instantiate FTP server class and listen to address:port
1975 server = pyftpdlib.ftpserver.FTPServer((host, port), ftp_handler)
1976 server_data['port'] = server.socket.getsockname()[1]
1977 print 'FTP server started on port %d...' % server_data['port']
1978 else:
1979 raise testserver_base.OptionError('unknown server type' +
1980 self.options.server_type)
1982 return server
1984 def run_server(self):
1985 if self.__ocsp_server:
1986 self.__ocsp_server.serve_forever_on_thread()
1988 testserver_base.TestServerRunner.run_server(self)
1990 if self.__ocsp_server:
1991 self.__ocsp_server.stop_serving()
1993 def add_options(self):
1994 testserver_base.TestServerRunner.add_options(self)
1995 self.option_parser.add_option('-f', '--ftp', action='store_const',
1996 const=SERVER_FTP, default=SERVER_HTTP,
1997 dest='server_type',
1998 help='start up an FTP server.')
1999 self.option_parser.add_option('--tcp-echo', action='store_const',
2000 const=SERVER_TCP_ECHO, default=SERVER_HTTP,
2001 dest='server_type',
2002 help='start up a tcp echo server.')
2003 self.option_parser.add_option('--udp-echo', action='store_const',
2004 const=SERVER_UDP_ECHO, default=SERVER_HTTP,
2005 dest='server_type',
2006 help='start up a udp echo server.')
2007 self.option_parser.add_option('--basic-auth-proxy', action='store_const',
2008 const=SERVER_BASIC_AUTH_PROXY,
2009 default=SERVER_HTTP, dest='server_type',
2010 help='start up a proxy server which requires '
2011 'basic authentication.')
2012 self.option_parser.add_option('--websocket', action='store_const',
2013 const=SERVER_WEBSOCKET, default=SERVER_HTTP,
2014 dest='server_type',
2015 help='start up a WebSocket server.')
2016 self.option_parser.add_option('--https', action='store_true',
2017 dest='https', help='Specify that https '
2018 'should be used.')
2019 self.option_parser.add_option('--cert-and-key-file',
2020 dest='cert_and_key_file', help='specify the '
2021 'path to the file containing the certificate '
2022 'and private key for the server in PEM '
2023 'format')
2024 self.option_parser.add_option('--ocsp', dest='ocsp', default='ok',
2025 help='The type of OCSP response generated '
2026 'for the automatically generated '
2027 'certificate. One of [ok,revoked,invalid]')
2028 self.option_parser.add_option('--cert-serial', dest='cert_serial',
2029 default=0, type=int,
2030 help='If non-zero then the generated '
2031 'certificate will have this serial number')
2032 self.option_parser.add_option('--tls-intolerant', dest='tls_intolerant',
2033 default='0', type='int',
2034 help='If nonzero, certain TLS connections '
2035 'will be aborted in order to test version '
2036 'fallback. 1 means all TLS versions will be '
2037 'aborted. 2 means TLS 1.1 or higher will be '
2038 'aborted. 3 means TLS 1.2 or higher will be '
2039 'aborted.')
2040 self.option_parser.add_option('--https-record-resume',
2041 dest='record_resume', const=True,
2042 default=False, action='store_const',
2043 help='Record resumption cache events rather '
2044 'than resuming as normal. Allows the use of '
2045 'the /ssl-session-cache request')
2046 self.option_parser.add_option('--ssl-client-auth', action='store_true',
2047 help='Require SSL client auth on every '
2048 'connection.')
2049 self.option_parser.add_option('--ssl-client-ca', action='append',
2050 default=[], help='Specify that the client '
2051 'certificate request should include the CA '
2052 'named in the subject of the DER-encoded '
2053 'certificate contained in the specified '
2054 'file. This option may appear multiple '
2055 'times, indicating multiple CA names should '
2056 'be sent in the request.')
2057 self.option_parser.add_option('--ssl-bulk-cipher', action='append',
2058 help='Specify the bulk encryption '
2059 'algorithm(s) that will be accepted by the '
2060 'SSL server. Valid values are "aes256", '
2061 '"aes128", "3des", "rc4". If omitted, all '
2062 'algorithms will be used. This option may '
2063 'appear multiple times, indicating '
2064 'multiple algorithms should be enabled.');
2065 self.option_parser.add_option('--file-root-url', default='/files/',
2066 help='Specify a root URL for files served.')
2069 if __name__ == '__main__':
2070 sys.exit(ServerRunner().main())