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 import pyftpdlib
.ftpserver
40 import testserver_base
44 BASE_DIR
= os
.path
.dirname(os
.path
.abspath(__file__
))
46 0, os
.path
.join(BASE_DIR
, '..', '..', '..', 'third_party/pywebsocket/src'))
47 from mod_pywebsocket
.standalone
import WebSocketServer
53 SERVER_BASIC_AUTH_PROXY
= 4
56 # Default request queue size for WebSocketServer.
57 _DEFAULT_REQUEST_QUEUE_SIZE
= 128
59 class WebSocketOptions
:
60 """Holds options for WebSocketServer."""
62 def __init__(self
, host
, port
, data_dir
):
63 self
.request_queue_size
= _DEFAULT_REQUEST_QUEUE_SIZE
64 self
.server_host
= host
66 self
.websock_handlers
= data_dir
68 self
.allow_handlers_outside_root_dir
= False
69 self
.websock_handlers_map_file
= None
70 self
.cgi_directories
= []
71 self
.is_executable_method
= None
72 self
.allow_draft75
= False
76 self
.private_key
= None
77 self
.certificate
= None
78 self
.tls_client_auth
= False
79 self
.tls_client_ca
= None
80 self
.use_basic_auth
= False
83 class RecordingSSLSessionCache(object):
84 """RecordingSSLSessionCache acts as a TLS session cache and maintains a log of
85 lookups and inserts in order to test session cache behaviours."""
90 def __getitem__(self
, sessionID
):
91 self
.log
.append(('lookup', sessionID
))
94 def __setitem__(self
, sessionID
, session
):
95 self
.log
.append(('insert', sessionID
))
98 class HTTPServer(testserver_base
.ClientRestrictingServerMixIn
,
99 testserver_base
.BrokenPipeHandlerMixIn
,
100 testserver_base
.StoppableHTTPServer
):
101 """This is a specialization of StoppableHTTPServer that adds client
106 class OCSPServer(testserver_base
.ClientRestrictingServerMixIn
,
107 testserver_base
.BrokenPipeHandlerMixIn
,
108 BaseHTTPServer
.HTTPServer
):
109 """This is a specialization of HTTPServer that serves an
112 def serve_forever_on_thread(self
):
113 self
.thread
= threading
.Thread(target
= self
.serve_forever
,
114 name
= "OCSPServerThread")
117 def stop_serving(self
):
122 class HTTPSServer(tlslite
.api
.TLSSocketServerMixIn
,
123 testserver_base
.ClientRestrictingServerMixIn
,
124 testserver_base
.BrokenPipeHandlerMixIn
,
125 testserver_base
.StoppableHTTPServer
):
126 """This is a specialization of StoppableHTTPServer that add https support and
127 client verification."""
129 def __init__(self
, server_address
, request_hander_class
, pem_cert_and_key
,
130 ssl_client_auth
, ssl_client_cas
, ssl_bulk_ciphers
,
131 record_resume_info
, tls_intolerant
):
132 self
.cert_chain
= tlslite
.api
.X509CertChain().parseChain(pem_cert_and_key
)
133 # Force using only python implementation - otherwise behavior is different
134 # depending on whether m2crypto Python module is present (error is thrown
135 # when it is). m2crypto uses a C (based on OpenSSL) implementation under
137 self
.private_key
= tlslite
.api
.parsePEMKey(pem_cert_and_key
,
139 implementations
=['python'])
140 self
.ssl_client_auth
= ssl_client_auth
141 self
.ssl_client_cas
= []
142 self
.tls_intolerant
= tls_intolerant
144 for ca_file
in ssl_client_cas
:
145 s
= open(ca_file
).read()
146 x509
= tlslite
.api
.X509()
148 self
.ssl_client_cas
.append(x509
.subject
)
149 self
.ssl_handshake_settings
= tlslite
.api
.HandshakeSettings()
150 if ssl_bulk_ciphers
is not None:
151 self
.ssl_handshake_settings
.cipherNames
= ssl_bulk_ciphers
153 if record_resume_info
:
154 # If record_resume_info is true then we'll replace the session cache with
155 # an object that records the lookups and inserts that it sees.
156 self
.session_cache
= RecordingSSLSessionCache()
158 self
.session_cache
= tlslite
.api
.SessionCache()
159 testserver_base
.StoppableHTTPServer
.__init
__(self
,
161 request_hander_class
)
163 def handshake(self
, tlsConnection
):
164 """Creates the SSL connection."""
167 self
.tlsConnection
= tlsConnection
168 tlsConnection
.handshakeServer(certChain
=self
.cert_chain
,
169 privateKey
=self
.private_key
,
170 sessionCache
=self
.session_cache
,
171 reqCert
=self
.ssl_client_auth
,
172 settings
=self
.ssl_handshake_settings
,
173 reqCAs
=self
.ssl_client_cas
,
174 tlsIntolerant
=self
.tls_intolerant
)
175 tlsConnection
.ignoreAbruptClose
= True
177 except tlslite
.api
.TLSAbruptCloseError
:
178 # Ignore abrupt close.
180 except tlslite
.api
.TLSError
, error
:
181 print "Handshake failure:", str(error
)
185 class FTPServer(testserver_base
.ClientRestrictingServerMixIn
,
186 pyftpdlib
.ftpserver
.FTPServer
):
187 """This is a specialization of FTPServer that adds client verification."""
192 class TCPEchoServer(testserver_base
.ClientRestrictingServerMixIn
,
193 SocketServer
.TCPServer
):
194 """A TCP echo server that echoes back what it has received."""
196 def server_bind(self
):
197 """Override server_bind to store the server name."""
199 SocketServer
.TCPServer
.server_bind(self
)
200 host
, port
= self
.socket
.getsockname()[:2]
201 self
.server_name
= socket
.getfqdn(host
)
202 self
.server_port
= port
204 def serve_forever(self
):
206 self
.nonce_time
= None
208 self
.handle_request()
212 class UDPEchoServer(testserver_base
.ClientRestrictingServerMixIn
,
213 SocketServer
.UDPServer
):
214 """A UDP echo server that echoes back what it has received."""
216 def server_bind(self
):
217 """Override server_bind to store the server name."""
219 SocketServer
.UDPServer
.server_bind(self
)
220 host
, port
= self
.socket
.getsockname()[:2]
221 self
.server_name
= socket
.getfqdn(host
)
222 self
.server_port
= port
224 def serve_forever(self
):
226 self
.nonce_time
= None
228 self
.handle_request()
232 class TestPageHandler(testserver_base
.BasePageHandler
):
233 # Class variables to allow for persistence state between page handler
236 fail_precondition
= {}
238 def __init__(self
, request
, client_address
, socket_server
):
240 self
.RedirectConnectHandler
,
241 self
.ServerAuthConnectHandler
,
242 self
.DefaultConnectResponseHandler
]
244 self
.NoCacheMaxAgeTimeHandler
,
245 self
.NoCacheTimeHandler
,
246 self
.CacheTimeHandler
,
247 self
.CacheExpiresHandler
,
248 self
.CacheProxyRevalidateHandler
,
249 self
.CachePrivateHandler
,
250 self
.CachePublicHandler
,
251 self
.CacheSMaxAgeHandler
,
252 self
.CacheMustRevalidateHandler
,
253 self
.CacheMustRevalidateMaxAgeHandler
,
254 self
.CacheNoStoreHandler
,
255 self
.CacheNoStoreMaxAgeHandler
,
256 self
.CacheNoTransformHandler
,
257 self
.DownloadHandler
,
258 self
.DownloadFinishHandler
,
260 self
.EchoHeaderCache
,
264 self
.SetCookieHandler
,
265 self
.SetManyCookiesHandler
,
266 self
.ExpectAndSetCookieHandler
,
267 self
.SetHeaderHandler
,
268 self
.AuthBasicHandler
,
269 self
.AuthDigestHandler
,
270 self
.SlowServerHandler
,
271 self
.ChunkedServerHandler
,
272 self
.ContentTypeHandler
,
273 self
.NoContentHandler
,
274 self
.ServerRedirectHandler
,
275 self
.ClientRedirectHandler
,
276 self
.MultipartHandler
,
277 self
.GetSSLSessionCacheHandler
,
278 self
.SSLManySmallRecords
,
280 self
.CloseSocketHandler
,
281 self
.RangeResetHandler
,
282 self
.DefaultResponseHandler
]
284 self
.EchoTitleHandler
,
286 self
.PostOnlyFileHandler
,
287 self
.EchoMultipartPostHandler
] + get_handlers
289 self
.EchoTitleHandler
,
290 self
.EchoHandler
] + get_handlers
293 self
.DefaultResponseHandler
]
296 'crx' : 'application/x-chrome-extension',
297 'exe' : 'application/octet-stream',
299 'jpeg' : 'image/jpeg',
300 'jpg' : 'image/jpeg',
301 'json': 'application/json',
302 'pdf' : 'application/pdf',
303 'txt' : 'text/plain',
307 self
._default
_mime
_type
= 'text/html'
309 testserver_base
.BasePageHandler
.__init
__(self
, request
, client_address
,
310 socket_server
, connect_handlers
,
311 get_handlers
, head_handlers
,
312 post_handlers
, put_handlers
)
314 def GetMIMETypeFromName(self
, file_name
):
315 """Returns the mime type for the specified file_name. So far it only looks
316 at the file extension."""
318 (_shortname
, extension
) = os
.path
.splitext(file_name
.split("?")[0])
319 if len(extension
) == 0:
321 return self
._default
_mime
_type
323 # extension starts with a dot, so we need to remove it
324 return self
._mime
_types
.get(extension
[1:], self
._default
_mime
_type
)
326 def NoCacheMaxAgeTimeHandler(self
):
327 """This request handler yields a page with the title set to the current
328 system time, and no caching requested."""
330 if not self
._ShouldHandleRequest
("/nocachetime/maxage"):
333 self
.send_response(200)
334 self
.send_header('Cache-Control', 'max-age=0')
335 self
.send_header('Content-Type', 'text/html')
338 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
343 def NoCacheTimeHandler(self
):
344 """This request handler yields a page with the title set to the current
345 system time, and no caching requested."""
347 if not self
._ShouldHandleRequest
("/nocachetime"):
350 self
.send_response(200)
351 self
.send_header('Cache-Control', 'no-cache')
352 self
.send_header('Content-Type', 'text/html')
355 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
360 def CacheTimeHandler(self
):
361 """This request handler yields a page with the title set to the current
362 system time, and allows caching for one minute."""
364 if not self
._ShouldHandleRequest
("/cachetime"):
367 self
.send_response(200)
368 self
.send_header('Cache-Control', 'max-age=60')
369 self
.send_header('Content-Type', 'text/html')
372 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
377 def CacheExpiresHandler(self
):
378 """This request handler yields a page with the title set to the current
379 system time, and set the page to expire on 1 Jan 2099."""
381 if not self
._ShouldHandleRequest
("/cache/expires"):
384 self
.send_response(200)
385 self
.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
386 self
.send_header('Content-Type', 'text/html')
389 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
394 def CacheProxyRevalidateHandler(self
):
395 """This request handler yields a page with the title set to the current
396 system time, and allows caching for 60 seconds"""
398 if not self
._ShouldHandleRequest
("/cache/proxy-revalidate"):
401 self
.send_response(200)
402 self
.send_header('Content-Type', 'text/html')
403 self
.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
406 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
411 def CachePrivateHandler(self
):
412 """This request handler yields a page with the title set to the current
413 system time, and allows caching for 5 seconds."""
415 if not self
._ShouldHandleRequest
("/cache/private"):
418 self
.send_response(200)
419 self
.send_header('Content-Type', 'text/html')
420 self
.send_header('Cache-Control', 'max-age=3, private')
423 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
428 def CachePublicHandler(self
):
429 """This request handler yields a page with the title set to the current
430 system time, and allows caching for 5 seconds."""
432 if not self
._ShouldHandleRequest
("/cache/public"):
435 self
.send_response(200)
436 self
.send_header('Content-Type', 'text/html')
437 self
.send_header('Cache-Control', 'max-age=3, public')
440 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
445 def CacheSMaxAgeHandler(self
):
446 """This request handler yields a page with the title set to the current
447 system time, and does not allow for caching."""
449 if not self
._ShouldHandleRequest
("/cache/s-maxage"):
452 self
.send_response(200)
453 self
.send_header('Content-Type', 'text/html')
454 self
.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
457 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
462 def CacheMustRevalidateHandler(self
):
463 """This request handler yields a page with the title set to the current
464 system time, and does not allow caching."""
466 if not self
._ShouldHandleRequest
("/cache/must-revalidate"):
469 self
.send_response(200)
470 self
.send_header('Content-Type', 'text/html')
471 self
.send_header('Cache-Control', 'must-revalidate')
474 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
479 def CacheMustRevalidateMaxAgeHandler(self
):
480 """This request handler yields a page with the title set to the current
481 system time, and does not allow caching event though max-age of 60
482 seconds is specified."""
484 if not self
._ShouldHandleRequest
("/cache/must-revalidate/max-age"):
487 self
.send_response(200)
488 self
.send_header('Content-Type', 'text/html')
489 self
.send_header('Cache-Control', 'max-age=60, must-revalidate')
492 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
497 def CacheNoStoreHandler(self
):
498 """This request handler yields a page with the title set to the current
499 system time, and does not allow the page to be stored."""
501 if not self
._ShouldHandleRequest
("/cache/no-store"):
504 self
.send_response(200)
505 self
.send_header('Content-Type', 'text/html')
506 self
.send_header('Cache-Control', 'no-store')
509 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
514 def CacheNoStoreMaxAgeHandler(self
):
515 """This request handler yields a page with the title set to the current
516 system time, and does not allow the page to be stored even though max-age
517 of 60 seconds is specified."""
519 if not self
._ShouldHandleRequest
("/cache/no-store/max-age"):
522 self
.send_response(200)
523 self
.send_header('Content-Type', 'text/html')
524 self
.send_header('Cache-Control', 'max-age=60, no-store')
527 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
533 def CacheNoTransformHandler(self
):
534 """This request handler yields a page with the title set to the current
535 system time, and does not allow the content to transformed during
536 user-agent caching"""
538 if not self
._ShouldHandleRequest
("/cache/no-transform"):
541 self
.send_response(200)
542 self
.send_header('Content-Type', 'text/html')
543 self
.send_header('Cache-Control', 'no-transform')
546 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
551 def EchoHeader(self
):
552 """This handler echoes back the value of a specific request header."""
554 return self
.EchoHeaderHelper("/echoheader")
556 def EchoHeaderCache(self
):
557 """This function echoes back the value of a specific request header while
558 allowing caching for 16 hours."""
560 return self
.EchoHeaderHelper("/echoheadercache")
562 def EchoHeaderHelper(self
, echo_header
):
563 """This function echoes back the value of the request header passed in."""
565 if not self
._ShouldHandleRequest
(echo_header
):
568 query_char
= self
.path
.find('?')
570 header_name
= self
.path
[query_char
+1:]
572 self
.send_response(200)
573 self
.send_header('Content-Type', 'text/plain')
574 if echo_header
== '/echoheadercache':
575 self
.send_header('Cache-control', 'max-age=60000')
577 self
.send_header('Cache-control', 'no-cache')
578 # insert a vary header to properly indicate that the cachability of this
579 # request is subject to value of the request header being echoed.
580 if len(header_name
) > 0:
581 self
.send_header('Vary', header_name
)
584 if len(header_name
) > 0:
585 self
.wfile
.write(self
.headers
.getheader(header_name
))
589 def ReadRequestBody(self
):
590 """This function reads the body of the current HTTP request, handling
591 both plain and chunked transfer encoded requests."""
593 if self
.headers
.getheader('transfer-encoding') != 'chunked':
594 length
= int(self
.headers
.getheader('content-length'))
595 return self
.rfile
.read(length
)
597 # Read the request body as chunks.
600 line
= self
.rfile
.readline()
601 length
= int(line
, 16)
603 self
.rfile
.readline()
605 body
+= self
.rfile
.read(length
)
609 def EchoHandler(self
):
610 """This handler just echoes back the payload of the request, for testing
613 if not self
._ShouldHandleRequest
("/echo"):
616 self
.send_response(200)
617 self
.send_header('Content-Type', 'text/html')
619 self
.wfile
.write(self
.ReadRequestBody())
622 def EchoTitleHandler(self
):
623 """This handler is like Echo, but sets the page title to the request."""
625 if not self
._ShouldHandleRequest
("/echotitle"):
628 self
.send_response(200)
629 self
.send_header('Content-Type', 'text/html')
631 request
= self
.ReadRequestBody()
632 self
.wfile
.write('<html><head><title>')
633 self
.wfile
.write(request
)
634 self
.wfile
.write('</title></head></html>')
637 def EchoAllHandler(self
):
638 """This handler yields a (more) human-readable page listing information
639 about the request header & contents."""
641 if not self
._ShouldHandleRequest
("/echoall"):
644 self
.send_response(200)
645 self
.send_header('Content-Type', 'text/html')
647 self
.wfile
.write('<html><head><style>'
648 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
649 '</style></head><body>'
650 '<div style="float: right">'
651 '<a href="/echo">back to referring page</a></div>'
652 '<h1>Request Body:</h1><pre>')
654 if self
.command
== 'POST' or self
.command
== 'PUT':
655 qs
= self
.ReadRequestBody()
656 params
= cgi
.parse_qs(qs
, keep_blank_values
=1)
659 self
.wfile
.write('%s=%s\n' % (param
, params
[param
][0]))
661 self
.wfile
.write('</pre>')
663 self
.wfile
.write('<h1>Request Headers:</h1><pre>%s</pre>' % self
.headers
)
665 self
.wfile
.write('</body></html>')
668 def EchoMultipartPostHandler(self
):
669 """This handler echoes received multipart post data as json format."""
671 if not (self
._ShouldHandleRequest
("/echomultipartpost") or
672 self
._ShouldHandleRequest
("/searchbyimage")):
675 content_type
, parameters
= cgi
.parse_header(
676 self
.headers
.getheader('content-type'))
677 if content_type
== 'multipart/form-data':
678 post_multipart
= cgi
.parse_multipart(self
.rfile
, parameters
)
679 elif content_type
== 'application/x-www-form-urlencoded':
680 raise Exception('POST by application/x-www-form-urlencoded is '
685 # Since the data can be binary, we encode them by base64.
686 post_multipart_base64_encoded
= {}
687 for field
, values
in post_multipart
.items():
688 post_multipart_base64_encoded
[field
] = [base64
.b64encode(value
)
691 result
= {'POST_multipart' : post_multipart_base64_encoded
}
693 self
.send_response(200)
694 self
.send_header("Content-type", "text/plain")
696 self
.wfile
.write(json
.dumps(result
, indent
=2, sort_keys
=False))
699 def DownloadHandler(self
):
700 """This handler sends a downloadable file with or without reporting
703 if self
.path
.startswith("/download-unknown-size"):
705 elif self
.path
.startswith("/download-known-size"):
711 # The test which uses this functionality is attempting to send
712 # small chunks of data to the client. Use a fairly large buffer
713 # so that we'll fill chrome's IO buffer enough to force it to
714 # actually write the data.
715 # See also the comments in the client-side of this test in
718 size_chunk1
= 35*1024
719 size_chunk2
= 10*1024
721 self
.send_response(200)
722 self
.send_header('Content-Type', 'application/octet-stream')
723 self
.send_header('Cache-Control', 'max-age=0')
725 self
.send_header('Content-Length', size_chunk1
+ size_chunk2
)
728 # First chunk of data:
729 self
.wfile
.write("*" * size_chunk1
)
732 # handle requests until one of them clears this flag.
733 self
.server
.wait_for_download
= True
734 while self
.server
.wait_for_download
:
735 self
.server
.handle_request()
737 # Second chunk of data:
738 self
.wfile
.write("*" * size_chunk2
)
741 def DownloadFinishHandler(self
):
742 """This handler just tells the server to finish the current download."""
744 if not self
._ShouldHandleRequest
("/download-finish"):
747 self
.server
.wait_for_download
= False
748 self
.send_response(200)
749 self
.send_header('Content-Type', 'text/html')
750 self
.send_header('Cache-Control', 'max-age=0')
754 def _ReplaceFileData(self
, data
, query_parameters
):
755 """Replaces matching substrings in a file.
757 If the 'replace_text' URL query parameter is present, it is expected to be
758 of the form old_text:new_text, which indicates that any old_text strings in
759 the file are replaced with new_text. Multiple 'replace_text' parameters may
762 If the parameters are not present, |data| is returned.
765 query_dict
= cgi
.parse_qs(query_parameters
)
766 replace_text_values
= query_dict
.get('replace_text', [])
767 for replace_text_value
in replace_text_values
:
768 replace_text_args
= replace_text_value
.split(':')
769 if len(replace_text_args
) != 2:
771 'replace_text must be of form old_text:new_text. Actual value: %s' %
773 old_text_b64
, new_text_b64
= replace_text_args
774 old_text
= base64
.urlsafe_b64decode(old_text_b64
)
775 new_text
= base64
.urlsafe_b64decode(new_text_b64
)
776 data
= data
.replace(old_text
, new_text
)
779 def ZipFileHandler(self
):
780 """This handler sends the contents of the requested file in compressed form.
781 Can pass in a parameter that specifies that the content length be
782 C - the compressed size (OK),
783 U - the uncompressed size (Non-standard, but handled),
784 S - less than compressed (OK because we keep going),
785 M - larger than compressed but less than uncompressed (an error),
786 L - larger than uncompressed (an error)
787 Example: compressedfiles/Picture_1.doc?C
790 prefix
= "/compressedfiles/"
791 if not self
.path
.startswith(prefix
):
794 # Consume a request body if present.
795 if self
.command
== 'POST' or self
.command
== 'PUT' :
796 self
.ReadRequestBody()
798 _
, _
, url_path
, _
, query
, _
= urlparse
.urlparse(self
.path
)
800 if not query
in ('C', 'U', 'S', 'M', 'L'):
803 sub_path
= url_path
[len(prefix
):]
804 entries
= sub_path
.split('/')
805 file_path
= os
.path
.join(self
.server
.data_dir
, *entries
)
806 if os
.path
.isdir(file_path
):
807 file_path
= os
.path
.join(file_path
, 'index.html')
809 if not os
.path
.isfile(file_path
):
810 print "File not found " + sub_path
+ " full path:" + file_path
814 f
= open(file_path
, "rb")
816 uncompressed_len
= len(data
)
820 data
= zlib
.compress(data
)
821 compressed_len
= len(data
)
823 content_length
= compressed_len
825 content_length
= uncompressed_len
827 content_length
= compressed_len
/ 2
829 content_length
= (compressed_len
+ uncompressed_len
) / 2
831 content_length
= compressed_len
+ uncompressed_len
833 self
.send_response(200)
834 self
.send_header('Content-Type', 'application/msword')
835 self
.send_header('Content-encoding', 'deflate')
836 self
.send_header('Connection', 'close')
837 self
.send_header('Content-Length', content_length
)
838 self
.send_header('ETag', '\'' + file_path
+ '\'')
841 self
.wfile
.write(data
)
845 def FileHandler(self
):
846 """This handler sends the contents of the requested file. Wow, it's like
849 prefix
= self
.server
.file_root_url
850 if not self
.path
.startswith(prefix
):
852 return self
._FileHandlerHelper
(prefix
)
854 def PostOnlyFileHandler(self
):
855 """This handler sends the contents of the requested file on a POST."""
857 prefix
= urlparse
.urljoin(self
.server
.file_root_url
, 'post/')
858 if not self
.path
.startswith(prefix
):
860 return self
._FileHandlerHelper
(prefix
)
862 def _FileHandlerHelper(self
, prefix
):
864 if self
.command
== 'POST' or self
.command
== 'PUT':
865 # Consume a request body if present.
866 request_body
= self
.ReadRequestBody()
868 _
, _
, url_path
, _
, query
, _
= urlparse
.urlparse(self
.path
)
869 query_dict
= cgi
.parse_qs(query
)
871 expected_body
= query_dict
.get('expected_body', [])
872 if expected_body
and request_body
not in expected_body
:
873 self
.send_response(404)
878 expected_headers
= query_dict
.get('expected_headers', [])
879 for expected_header
in expected_headers
:
880 header_name
, expected_value
= expected_header
.split(':')
881 if self
.headers
.getheader(header_name
) != expected_value
:
882 self
.send_response(404)
887 sub_path
= url_path
[len(prefix
):]
888 entries
= sub_path
.split('/')
889 file_path
= os
.path
.join(self
.server
.data_dir
, *entries
)
890 if os
.path
.isdir(file_path
):
891 file_path
= os
.path
.join(file_path
, 'index.html')
893 if not os
.path
.isfile(file_path
):
894 print "File not found " + sub_path
+ " full path:" + file_path
898 f
= open(file_path
, "rb")
902 data
= self
._ReplaceFileData
(data
, query
)
904 old_protocol_version
= self
.protocol_version
906 # If file.mock-http-headers exists, it contains the headers we
907 # should send. Read them in and parse them.
908 headers_path
= file_path
+ '.mock-http-headers'
909 if os
.path
.isfile(headers_path
):
910 f
= open(headers_path
, "r")
913 response
= f
.readline()
914 http_major
, http_minor
, status_code
= re
.findall(
915 'HTTP/(\d+).(\d+) (\d+)', response
)[0]
916 self
.protocol_version
= "HTTP/%s.%s" % (http_major
, http_minor
)
917 self
.send_response(int(status_code
))
920 header_values
= re
.findall('(\S+):\s*(.*)', line
)
921 if len(header_values
) > 0:
923 name
, value
= header_values
[0]
924 self
.send_header(name
, value
)
927 # Could be more generic once we support mime-type sniffing, but for
928 # now we need to set it explicitly.
930 range_header
= self
.headers
.get('Range')
931 if range_header
and range_header
.startswith('bytes='):
932 # Note this doesn't handle all valid byte range_header values (i.e.
933 # left open ended ones), just enough for what we needed so far.
934 range_header
= range_header
[6:].split('-')
935 start
= int(range_header
[0])
937 end
= int(range_header
[1])
941 self
.send_response(206)
942 content_range
= ('bytes ' + str(start
) + '-' + str(end
) + '/' +
944 self
.send_header('Content-Range', content_range
)
945 data
= data
[start
: end
+ 1]
947 self
.send_response(200)
949 self
.send_header('Content-Type', self
.GetMIMETypeFromName(file_path
))
950 self
.send_header('Accept-Ranges', 'bytes')
951 self
.send_header('Content-Length', len(data
))
952 self
.send_header('ETag', '\'' + file_path
+ '\'')
955 if (self
.command
!= 'HEAD'):
956 self
.wfile
.write(data
)
958 self
.protocol_version
= old_protocol_version
961 def SetCookieHandler(self
):
962 """This handler just sets a cookie, for testing cookie handling."""
964 if not self
._ShouldHandleRequest
("/set-cookie"):
967 query_char
= self
.path
.find('?')
969 cookie_values
= self
.path
[query_char
+ 1:].split('&')
971 cookie_values
= ("",)
972 self
.send_response(200)
973 self
.send_header('Content-Type', 'text/html')
974 for cookie_value
in cookie_values
:
975 self
.send_header('Set-Cookie', '%s' % cookie_value
)
977 for cookie_value
in cookie_values
:
978 self
.wfile
.write('%s' % cookie_value
)
981 def SetManyCookiesHandler(self
):
982 """This handler just sets a given number of cookies, for testing handling
983 of large numbers of cookies."""
985 if not self
._ShouldHandleRequest
("/set-many-cookies"):
988 query_char
= self
.path
.find('?')
990 num_cookies
= int(self
.path
[query_char
+ 1:])
993 self
.send_response(200)
994 self
.send_header('', 'text/html')
995 for _i
in range(0, num_cookies
):
996 self
.send_header('Set-Cookie', 'a=')
998 self
.wfile
.write('%d cookies were sent' % num_cookies
)
1001 def ExpectAndSetCookieHandler(self
):
1002 """Expects some cookies to be sent, and if they are, sets more cookies.
1004 The expect parameter specifies a required cookie. May be specified multiple
1006 The set parameter specifies a cookie to set if all required cookies are
1007 preset. May be specified multiple times.
1008 The data parameter specifies the response body data to be returned."""
1010 if not self
._ShouldHandleRequest
("/expect-and-set-cookie"):
1013 _
, _
, _
, _
, query
, _
= urlparse
.urlparse(self
.path
)
1014 query_dict
= cgi
.parse_qs(query
)
1016 if 'Cookie' in self
.headers
:
1017 cookie_header
= self
.headers
.getheader('Cookie')
1018 cookies
.update([s
.strip() for s
in cookie_header
.split(';')])
1019 got_all_expected_cookies
= True
1020 for expected_cookie
in query_dict
.get('expect', []):
1021 if expected_cookie
not in cookies
:
1022 got_all_expected_cookies
= False
1023 self
.send_response(200)
1024 self
.send_header('Content-Type', 'text/html')
1025 if got_all_expected_cookies
:
1026 for cookie_value
in query_dict
.get('set', []):
1027 self
.send_header('Set-Cookie', '%s' % cookie_value
)
1029 for data_value
in query_dict
.get('data', []):
1030 self
.wfile
.write(data_value
)
1033 def SetHeaderHandler(self
):
1034 """This handler sets a response header. Parameters are in the
1035 key%3A%20value&key2%3A%20value2 format."""
1037 if not self
._ShouldHandleRequest
("/set-header"):
1040 query_char
= self
.path
.find('?')
1041 if query_char
!= -1:
1042 headers_values
= self
.path
[query_char
+ 1:].split('&')
1044 headers_values
= ("",)
1045 self
.send_response(200)
1046 self
.send_header('Content-Type', 'text/html')
1047 for header_value
in headers_values
:
1048 header_value
= urllib
.unquote(header_value
)
1049 (key
, value
) = header_value
.split(': ', 1)
1050 self
.send_header(key
, value
)
1052 for header_value
in headers_values
:
1053 self
.wfile
.write('%s' % header_value
)
1056 def AuthBasicHandler(self
):
1057 """This handler tests 'Basic' authentication. It just sends a page with
1058 title 'user/pass' if you succeed."""
1060 if not self
._ShouldHandleRequest
("/auth-basic"):
1063 username
= userpass
= password
= b64str
= ""
1064 expected_password
= 'secret'
1066 set_cookie_if_challenged
= False
1068 _
, _
, url_path
, _
, query
, _
= urlparse
.urlparse(self
.path
)
1069 query_params
= cgi
.parse_qs(query
, True)
1070 if 'set-cookie-if-challenged' in query_params
:
1071 set_cookie_if_challenged
= True
1072 if 'password' in query_params
:
1073 expected_password
= query_params
['password'][0]
1074 if 'realm' in query_params
:
1075 realm
= query_params
['realm'][0]
1077 auth
= self
.headers
.getheader('authorization')
1080 raise Exception('no auth')
1081 b64str
= re
.findall(r
'Basic (\S+)', auth
)[0]
1082 userpass
= base64
.b64decode(b64str
)
1083 username
, password
= re
.findall(r
'([^:]+):(\S+)', userpass
)[0]
1084 if password
!= expected_password
:
1085 raise Exception('wrong password')
1086 except Exception, e
:
1087 # Authentication failed.
1088 self
.send_response(401)
1089 self
.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm
)
1090 self
.send_header('Content-Type', 'text/html')
1091 if set_cookie_if_challenged
:
1092 self
.send_header('Set-Cookie', 'got_challenged=true')
1094 self
.wfile
.write('<html><head>')
1095 self
.wfile
.write('<title>Denied: %s</title>' % e
)
1096 self
.wfile
.write('</head><body>')
1097 self
.wfile
.write('auth=%s<p>' % auth
)
1098 self
.wfile
.write('b64str=%s<p>' % b64str
)
1099 self
.wfile
.write('username: %s<p>' % username
)
1100 self
.wfile
.write('userpass: %s<p>' % userpass
)
1101 self
.wfile
.write('password: %s<p>' % password
)
1102 self
.wfile
.write('You sent:<br>%s<p>' % self
.headers
)
1103 self
.wfile
.write('</body></html>')
1106 # Authentication successful. (Return a cachable response to allow for
1107 # testing cached pages that require authentication.)
1108 old_protocol_version
= self
.protocol_version
1109 self
.protocol_version
= "HTTP/1.1"
1111 if_none_match
= self
.headers
.getheader('if-none-match')
1112 if if_none_match
== "abc":
1113 self
.send_response(304)
1115 elif url_path
.endswith(".gif"):
1116 # Using chrome/test/data/google/logo.gif as the test image
1117 test_image_path
= ['google', 'logo.gif']
1118 gif_path
= os
.path
.join(self
.server
.data_dir
, *test_image_path
)
1119 if not os
.path
.isfile(gif_path
):
1120 self
.send_error(404)
1121 self
.protocol_version
= old_protocol_version
1124 f
= open(gif_path
, "rb")
1128 self
.send_response(200)
1129 self
.send_header('Content-Type', 'image/gif')
1130 self
.send_header('Cache-control', 'max-age=60000')
1131 self
.send_header('Etag', 'abc')
1133 self
.wfile
.write(data
)
1135 self
.send_response(200)
1136 self
.send_header('Content-Type', 'text/html')
1137 self
.send_header('Cache-control', 'max-age=60000')
1138 self
.send_header('Etag', 'abc')
1140 self
.wfile
.write('<html><head>')
1141 self
.wfile
.write('<title>%s/%s</title>' % (username
, password
))
1142 self
.wfile
.write('</head><body>')
1143 self
.wfile
.write('auth=%s<p>' % auth
)
1144 self
.wfile
.write('You sent:<br>%s<p>' % self
.headers
)
1145 self
.wfile
.write('</body></html>')
1147 self
.protocol_version
= old_protocol_version
1150 def GetNonce(self
, force_reset
=False):
1151 """Returns a nonce that's stable per request path for the server's lifetime.
1152 This is a fake implementation. A real implementation would only use a given
1153 nonce a single time (hence the name n-once). However, for the purposes of
1154 unittesting, we don't care about the security of the nonce.
1157 force_reset: Iff set, the nonce will be changed. Useful for testing the
1161 if force_reset
or not self
.server
.nonce_time
:
1162 self
.server
.nonce_time
= time
.time()
1163 return hashlib
.md5('privatekey%s%d' %
1164 (self
.path
, self
.server
.nonce_time
)).hexdigest()
1166 def AuthDigestHandler(self
):
1167 """This handler tests 'Digest' authentication.
1169 It just sends a page with title 'user/pass' if you succeed.
1171 A stale response is sent iff "stale" is present in the request path.
1174 if not self
._ShouldHandleRequest
("/auth-digest"):
1177 stale
= 'stale' in self
.path
1178 nonce
= self
.GetNonce(force_reset
=stale
)
1179 opaque
= hashlib
.md5('opaque').hexdigest()
1183 auth
= self
.headers
.getheader('authorization')
1187 raise Exception('no auth')
1188 if not auth
.startswith('Digest'):
1189 raise Exception('not digest')
1190 # Pull out all the name="value" pairs as a dictionary.
1191 pairs
= dict(re
.findall(r
'(\b[^ ,=]+)="?([^",]+)"?', auth
))
1193 # Make sure it's all valid.
1194 if pairs
['nonce'] != nonce
:
1195 raise Exception('wrong nonce')
1196 if pairs
['opaque'] != opaque
:
1197 raise Exception('wrong opaque')
1199 # Check the 'response' value and make sure it matches our magic hash.
1200 # See http://www.ietf.org/rfc/rfc2617.txt
1201 hash_a1
= hashlib
.md5(
1202 ':'.join([pairs
['username'], realm
, password
])).hexdigest()
1203 hash_a2
= hashlib
.md5(':'.join([self
.command
, pairs
['uri']])).hexdigest()
1204 if 'qop' in pairs
and 'nc' in pairs
and 'cnonce' in pairs
:
1205 response
= hashlib
.md5(':'.join([hash_a1
, nonce
, pairs
['nc'],
1206 pairs
['cnonce'], pairs
['qop'], hash_a2
])).hexdigest()
1208 response
= hashlib
.md5(':'.join([hash_a1
, nonce
, hash_a2
])).hexdigest()
1210 if pairs
['response'] != response
:
1211 raise Exception('wrong password')
1212 except Exception, e
:
1213 # Authentication failed.
1214 self
.send_response(401)
1221 'opaque="%s"') % (realm
, nonce
, opaque
)
1223 hdr
+= ', stale="TRUE"'
1224 self
.send_header('WWW-Authenticate', hdr
)
1225 self
.send_header('Content-Type', 'text/html')
1227 self
.wfile
.write('<html><head>')
1228 self
.wfile
.write('<title>Denied: %s</title>' % e
)
1229 self
.wfile
.write('</head><body>')
1230 self
.wfile
.write('auth=%s<p>' % auth
)
1231 self
.wfile
.write('pairs=%s<p>' % pairs
)
1232 self
.wfile
.write('You sent:<br>%s<p>' % self
.headers
)
1233 self
.wfile
.write('We are replying:<br>%s<p>' % hdr
)
1234 self
.wfile
.write('</body></html>')
1237 # Authentication successful.
1238 self
.send_response(200)
1239 self
.send_header('Content-Type', 'text/html')
1241 self
.wfile
.write('<html><head>')
1242 self
.wfile
.write('<title>%s/%s</title>' % (pairs
['username'], password
))
1243 self
.wfile
.write('</head><body>')
1244 self
.wfile
.write('auth=%s<p>' % auth
)
1245 self
.wfile
.write('pairs=%s<p>' % pairs
)
1246 self
.wfile
.write('</body></html>')
1250 def SlowServerHandler(self
):
1251 """Wait for the user suggested time before responding. The syntax is
1252 /slow?0.5 to wait for half a second."""
1254 if not self
._ShouldHandleRequest
("/slow"):
1256 query_char
= self
.path
.find('?')
1260 wait_sec
= int(self
.path
[query_char
+ 1:])
1263 time
.sleep(wait_sec
)
1264 self
.send_response(200)
1265 self
.send_header('Content-Type', 'text/plain')
1267 self
.wfile
.write("waited %d seconds" % wait_sec
)
1270 def ChunkedServerHandler(self
):
1271 """Send chunked response. Allows to specify chunks parameters:
1272 - waitBeforeHeaders - ms to wait before sending headers
1273 - waitBetweenChunks - ms to wait between chunks
1274 - chunkSize - size of each chunk in bytes
1275 - chunksNumber - number of chunks
1276 Example: /chunked?waitBeforeHeaders=1000&chunkSize=5&chunksNumber=5
1277 waits one second, then sends headers and five chunks five bytes each."""
1279 if not self
._ShouldHandleRequest
("/chunked"):
1281 query_char
= self
.path
.find('?')
1282 chunkedSettings
= {'waitBeforeHeaders' : 0,
1283 'waitBetweenChunks' : 0,
1287 params
= self
.path
[query_char
+ 1:].split('&')
1288 for param
in params
:
1289 keyValue
= param
.split('=')
1290 if len(keyValue
) == 2:
1292 chunkedSettings
[keyValue
[0]] = int(keyValue
[1])
1295 time
.sleep(0.001 * chunkedSettings
['waitBeforeHeaders'])
1296 self
.protocol_version
= 'HTTP/1.1' # Needed for chunked encoding
1297 self
.send_response(200)
1298 self
.send_header('Content-Type', 'text/plain')
1299 self
.send_header('Connection', 'close')
1300 self
.send_header('Transfer-Encoding', 'chunked')
1302 # Chunked encoding: sending all chunks, then final zero-length chunk and
1304 for i
in range(0, chunkedSettings
['chunksNumber']):
1306 time
.sleep(0.001 * chunkedSettings
['waitBetweenChunks'])
1307 self
.sendChunkHelp('*' * chunkedSettings
['chunkSize'])
1308 self
.wfile
.flush() # Keep in mind that we start flushing only after 1kb.
1309 self
.sendChunkHelp('')
1312 def ContentTypeHandler(self
):
1313 """Returns a string of html with the given content type. E.g.,
1314 /contenttype?text/css returns an html file with the Content-Type
1315 header set to text/css."""
1317 if not self
._ShouldHandleRequest
("/contenttype"):
1319 query_char
= self
.path
.find('?')
1320 content_type
= self
.path
[query_char
+ 1:].strip()
1321 if not content_type
:
1322 content_type
= 'text/html'
1323 self
.send_response(200)
1324 self
.send_header('Content-Type', content_type
)
1326 self
.wfile
.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n")
1329 def NoContentHandler(self
):
1330 """Returns a 204 No Content response."""
1332 if not self
._ShouldHandleRequest
("/nocontent"):
1334 self
.send_response(204)
1338 def ServerRedirectHandler(self
):
1339 """Sends a server redirect to the given URL. The syntax is
1340 '/server-redirect?http://foo.bar/asdf' to redirect to
1341 'http://foo.bar/asdf'"""
1343 test_name
= "/server-redirect"
1344 if not self
._ShouldHandleRequest
(test_name
):
1347 query_char
= self
.path
.find('?')
1348 if query_char
< 0 or len(self
.path
) <= query_char
+ 1:
1349 self
.sendRedirectHelp(test_name
)
1351 dest
= self
.path
[query_char
+ 1:]
1353 self
.send_response(301) # moved permanently
1354 self
.send_header('Location', dest
)
1355 self
.send_header('Content-Type', 'text/html')
1357 self
.wfile
.write('<html><head>')
1358 self
.wfile
.write('</head><body>Redirecting to %s</body></html>' % dest
)
1362 def ClientRedirectHandler(self
):
1363 """Sends a client redirect to the given URL. The syntax is
1364 '/client-redirect?http://foo.bar/asdf' to redirect to
1365 'http://foo.bar/asdf'"""
1367 test_name
= "/client-redirect"
1368 if not self
._ShouldHandleRequest
(test_name
):
1371 query_char
= self
.path
.find('?')
1372 if query_char
< 0 or len(self
.path
) <= query_char
+ 1:
1373 self
.sendRedirectHelp(test_name
)
1375 dest
= self
.path
[query_char
+ 1:]
1377 self
.send_response(200)
1378 self
.send_header('Content-Type', 'text/html')
1380 self
.wfile
.write('<html><head>')
1381 self
.wfile
.write('<meta http-equiv="refresh" content="0;url=%s">' % dest
)
1382 self
.wfile
.write('</head><body>Redirecting to %s</body></html>' % dest
)
1386 def MultipartHandler(self
):
1387 """Send a multipart response (10 text/html pages)."""
1389 test_name
= '/multipart'
1390 if not self
._ShouldHandleRequest
(test_name
):
1395 self
.send_response(200)
1396 self
.send_header('Content-Type',
1397 'multipart/x-mixed-replace;boundary=' + bound
)
1400 for i
in xrange(num_frames
):
1401 self
.wfile
.write('--' + bound
+ '\r\n')
1402 self
.wfile
.write('Content-Type: text/html\r\n\r\n')
1403 self
.wfile
.write('<title>page ' + str(i
) + '</title>')
1404 self
.wfile
.write('page ' + str(i
))
1406 self
.wfile
.write('--' + bound
+ '--')
1409 def GetSSLSessionCacheHandler(self
):
1410 """Send a reply containing a log of the session cache operations."""
1412 if not self
._ShouldHandleRequest
('/ssl-session-cache'):
1415 self
.send_response(200)
1416 self
.send_header('Content-Type', 'text/plain')
1419 for (action
, sessionID
) in self
.server
.session_cache
.log
:
1420 self
.wfile
.write('%s\t%s\n' % (action
, sessionID
.encode('hex')))
1421 except AttributeError:
1422 self
.wfile
.write('Pass --https-record-resume in order to use' +
1426 def SSLManySmallRecords(self
):
1427 """Sends a reply consisting of a variety of small writes. These will be
1428 translated into a series of small SSL records when used over an HTTPS
1431 if not self
._ShouldHandleRequest
('/ssl-many-small-records'):
1434 self
.send_response(200)
1435 self
.send_header('Content-Type', 'text/plain')
1438 # Write ~26K of data, in 1350 byte chunks
1439 for i
in xrange(20):
1440 self
.wfile
.write('*' * 1350)
1444 def GetChannelID(self
):
1445 """Send a reply containing the hashed ChannelID that the client provided."""
1447 if not self
._ShouldHandleRequest
('/channel-id'):
1450 self
.send_response(200)
1451 self
.send_header('Content-Type', 'text/plain')
1453 channel_id
= self
.server
.tlsConnection
.channel_id
.tostring()
1454 self
.wfile
.write(hashlib
.sha256(channel_id
).digest().encode('base64'))
1457 def CloseSocketHandler(self
):
1458 """Closes the socket without sending anything."""
1460 if not self
._ShouldHandleRequest
('/close-socket'):
1466 def RangeResetHandler(self
):
1467 """Send data broken up by connection resets every N (default 4K) bytes.
1468 Support range requests. If the data requested doesn't straddle a reset
1469 boundary, it will all be sent. Used for testing resuming downloads."""
1471 def DataForRange(start
, end
):
1472 """Data to be provided for a particular range of bytes."""
1473 # Offset and scale to avoid too obvious (and hence potentially
1475 return ''.join([chr(y
% 256)
1476 for y
in range(start
* 2 + 15, end
* 2 + 15, 2)])
1478 if not self
._ShouldHandleRequest
('/rangereset'):
1481 _
, _
, url_path
, _
, query
, _
= urlparse
.urlparse(self
.path
)
1485 # Note that the rst is sent just before sending the rst_boundary byte.
1487 respond_to_range
= True
1488 hold_for_signal
= False
1491 fail_precondition
= 0
1492 send_verifiers
= True
1495 qdict
= urlparse
.parse_qs(query
, True)
1497 size
= int(qdict
['size'][0])
1498 if 'rst_boundary' in qdict
:
1499 rst_boundary
= int(qdict
['rst_boundary'][0])
1500 if 'token' in qdict
:
1501 # Identifying token for stateful tests.
1502 token
= qdict
['token'][0]
1503 if 'rst_limit' in qdict
:
1504 # Max number of rsts for a given token.
1505 rst_limit
= int(qdict
['rst_limit'][0])
1506 if 'bounce_range' in qdict
:
1507 respond_to_range
= False
1509 # Note that hold_for_signal will not work with null range requests;
1511 hold_for_signal
= True
1512 if 'no_verifiers' in qdict
:
1513 send_verifiers
= False
1514 if 'fail_precondition' in qdict
:
1515 fail_precondition
= int(qdict
['fail_precondition'][0])
1517 # Record already set information, or set it.
1518 rst_limit
= TestPageHandler
.rst_limits
.setdefault(token
, rst_limit
)
1520 TestPageHandler
.rst_limits
[token
] -= 1
1521 fail_precondition
= TestPageHandler
.fail_precondition
.setdefault(
1522 token
, fail_precondition
)
1523 if fail_precondition
!= 0:
1524 TestPageHandler
.fail_precondition
[token
] -= 1
1527 last_byte
= size
- 1
1529 # Does that define what we want to return, or do we need to apply
1531 range_response
= False
1532 range_header
= self
.headers
.getheader('range')
1533 if range_header
and respond_to_range
:
1534 mo
= re
.match("bytes=(\d*)-(\d*)", range_header
)
1536 first_byte
= int(mo
.group(1))
1538 last_byte
= int(mo
.group(2))
1539 if last_byte
> size
- 1:
1540 last_byte
= size
- 1
1541 range_response
= True
1542 if last_byte
< first_byte
:
1545 if (fail_precondition
and
1546 (self
.headers
.getheader('If-Modified-Since') or
1547 self
.headers
.getheader('If-Match'))):
1548 self
.send_response(412)
1553 self
.send_response(206)
1554 self
.send_header('Content-Range',
1555 'bytes %d-%d/%d' % (first_byte
, last_byte
, size
))
1557 self
.send_response(200)
1558 self
.send_header('Content-Type', 'application/octet-stream')
1559 self
.send_header('Content-Length', last_byte
- first_byte
+ 1)
1561 self
.send_header('Etag', '"XYZZY"')
1562 self
.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST')
1566 # TODO(rdsmith/phajdan.jr): http://crbug.com/169519: Without writing
1567 # a single byte, the self.server.handle_request() below hangs
1568 # without processing new incoming requests.
1569 self
.wfile
.write(DataForRange(first_byte
, first_byte
+ 1))
1570 first_byte
= first_byte
+ 1
1571 # handle requests until one of them clears this flag.
1572 self
.server
.wait_for_download
= True
1573 while self
.server
.wait_for_download
:
1574 self
.server
.handle_request()
1576 possible_rst
= ((first_byte
/ rst_boundary
) + 1) * rst_boundary
1577 if possible_rst
>= last_byte
or rst_limit
== 0:
1578 # No RST has been requested in this range, so we don't need to
1579 # do anything fancy; just write the data and let the python
1580 # infrastructure close the connection.
1581 self
.wfile
.write(DataForRange(first_byte
, last_byte
+ 1))
1585 # We're resetting the connection part way in; go to the RST
1586 # boundary and then send an RST.
1587 # Because socket semantics do not guarantee that all the data will be
1588 # sent when using the linger semantics to hard close a socket,
1589 # we send the data and then wait for our peer to release us
1590 # before sending the reset.
1591 data
= DataForRange(first_byte
, possible_rst
)
1592 self
.wfile
.write(data
)
1594 self
.server
.wait_for_download
= True
1595 while self
.server
.wait_for_download
:
1596 self
.server
.handle_request()
1597 l_onoff
= 1 # Linger is active.
1598 l_linger
= 0 # Seconds to linger for.
1599 self
.connection
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_LINGER
,
1600 struct
.pack('ii', l_onoff
, l_linger
))
1602 # Close all duplicates of the underlying socket to force the RST.
1605 self
.connection
.close()
1609 def DefaultResponseHandler(self
):
1610 """This is the catch-all response handler for requests that aren't handled
1611 by one of the special handlers above.
1612 Note that we specify the content-length as without it the https connection
1613 is not closed properly (and the browser keeps expecting data)."""
1615 contents
= "Default response given for path: " + self
.path
1616 self
.send_response(200)
1617 self
.send_header('Content-Type', 'text/html')
1618 self
.send_header('Content-Length', len(contents
))
1620 if (self
.command
!= 'HEAD'):
1621 self
.wfile
.write(contents
)
1624 def RedirectConnectHandler(self
):
1625 """Sends a redirect to the CONNECT request for www.redirect.com. This
1626 response is not specified by the RFC, so the browser should not follow
1629 if (self
.path
.find("www.redirect.com") < 0):
1632 dest
= "http://www.destination.com/foo.js"
1634 self
.send_response(302) # moved temporarily
1635 self
.send_header('Location', dest
)
1636 self
.send_header('Connection', 'close')
1640 def ServerAuthConnectHandler(self
):
1641 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1642 response doesn't make sense because the proxy server cannot request
1643 server authentication."""
1645 if (self
.path
.find("www.server-auth.com") < 0):
1648 challenge
= 'Basic realm="WallyWorld"'
1650 self
.send_response(401) # unauthorized
1651 self
.send_header('WWW-Authenticate', challenge
)
1652 self
.send_header('Connection', 'close')
1656 def DefaultConnectResponseHandler(self
):
1657 """This is the catch-all response handler for CONNECT requests that aren't
1658 handled by one of the special handlers above. Real Web servers respond
1659 with 400 to CONNECT requests."""
1661 contents
= "Your client has issued a malformed or illegal request."
1662 self
.send_response(400) # bad request
1663 self
.send_header('Content-Type', 'text/html')
1664 self
.send_header('Content-Length', len(contents
))
1666 self
.wfile
.write(contents
)
1669 # called by the redirect handling function when there is no parameter
1670 def sendRedirectHelp(self
, redirect_name
):
1671 self
.send_response(200)
1672 self
.send_header('Content-Type', 'text/html')
1674 self
.wfile
.write('<html><body><h1>Error: no redirect destination</h1>')
1675 self
.wfile
.write('Use <pre>%s?http://dest...</pre>' % redirect_name
)
1676 self
.wfile
.write('</body></html>')
1678 # called by chunked handling function
1679 def sendChunkHelp(self
, chunk
):
1680 # Each chunk consists of: chunk size (hex), CRLF, chunk body, CRLF
1681 self
.wfile
.write('%X\r\n' % len(chunk
))
1682 self
.wfile
.write(chunk
)
1683 self
.wfile
.write('\r\n')
1686 class OCSPHandler(testserver_base
.BasePageHandler
):
1687 def __init__(self
, request
, client_address
, socket_server
):
1688 handlers
= [self
.OCSPResponse
]
1689 self
.ocsp_response
= socket_server
.ocsp_response
1690 testserver_base
.BasePageHandler
.__init
__(self
, request
, client_address
,
1691 socket_server
, [], handlers
, [],
1694 def OCSPResponse(self
):
1695 self
.send_response(200)
1696 self
.send_header('Content-Type', 'application/ocsp-response')
1697 self
.send_header('Content-Length', str(len(self
.ocsp_response
)))
1700 self
.wfile
.write(self
.ocsp_response
)
1703 class TCPEchoHandler(SocketServer
.BaseRequestHandler
):
1704 """The RequestHandler class for TCP echo server.
1706 It is instantiated once per connection to the server, and overrides the
1707 handle() method to implement communication to the client.
1711 """Handles the request from the client and constructs a response."""
1713 data
= self
.request
.recv(65536).strip()
1714 # Verify the "echo request" message received from the client. Send back
1715 # "echo response" message if "echo request" message is valid.
1717 return_data
= echo_message
.GetEchoResponseData(data
)
1723 self
.request
.send(return_data
)
1726 class UDPEchoHandler(SocketServer
.BaseRequestHandler
):
1727 """The RequestHandler class for UDP echo server.
1729 It is instantiated once per connection to the server, and overrides the
1730 handle() method to implement communication to the client.
1734 """Handles the request from the client and constructs a response."""
1736 data
= self
.request
[0].strip()
1737 request_socket
= self
.request
[1]
1738 # Verify the "echo request" message received from the client. Send back
1739 # "echo response" message if "echo request" message is valid.
1741 return_data
= echo_message
.GetEchoResponseData(data
)
1746 request_socket
.sendto(return_data
, self
.client_address
)
1749 class BasicAuthProxyRequestHandler(BaseHTTPServer
.BaseHTTPRequestHandler
):
1750 """A request handler that behaves as a proxy server which requires
1751 basic authentication. Only CONNECT, GET and HEAD is supported for now.
1754 _AUTH_CREDENTIAL
= 'Basic Zm9vOmJhcg==' # foo:bar
1756 def parse_request(self
):
1757 """Overrides parse_request to check credential."""
1759 if not BaseHTTPServer
.BaseHTTPRequestHandler
.parse_request(self
):
1762 auth
= self
.headers
.getheader('Proxy-Authorization')
1763 if auth
!= self
._AUTH
_CREDENTIAL
:
1764 self
.send_response(407)
1765 self
.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"')
1771 def _start_read_write(self
, sock
):
1773 self
.request
.setblocking(0)
1774 rlist
= [self
.request
, sock
]
1776 ready_sockets
, _unused
, errors
= select
.select(rlist
, [], [])
1778 self
.send_response(500)
1781 for s
in ready_sockets
:
1782 received
= s
.recv(1024)
1783 if len(received
) == 0:
1785 if s
== self
.request
:
1788 other
= self
.request
1789 other
.send(received
)
1791 def _do_common_method(self
):
1792 url
= urlparse
.urlparse(self
.path
)
1795 if url
.scheme
== 'http':
1797 elif url
.scheme
== 'https':
1799 if not url
.hostname
or not port
:
1800 self
.send_response(400)
1804 if len(url
.path
) == 0:
1808 if len(url
.query
) > 0:
1809 path
= '%s?%s' % (url
.path
, url
.query
)
1813 sock
= socket
.create_connection((url
.hostname
, port
))
1814 sock
.send('%s %s %s\r\n' % (
1815 self
.command
, path
, self
.protocol_version
))
1816 for header
in self
.headers
.headers
:
1817 header
= header
.strip()
1818 if (header
.lower().startswith('connection') or
1819 header
.lower().startswith('proxy')):
1821 sock
.send('%s\r\n' % header
)
1823 self
._start
_read
_write
(sock
)
1825 self
.send_response(500)
1828 if sock
is not None:
1831 def do_CONNECT(self
):
1833 pos
= self
.path
.rfind(':')
1834 host
= self
.path
[:pos
]
1835 port
= int(self
.path
[pos
+1:])
1837 self
.send_response(400)
1841 sock
= socket
.create_connection((host
, port
))
1842 self
.send_response(200, 'Connection established')
1844 self
._start
_read
_write
(sock
)
1846 self
.send_response(500)
1852 self
._do
_common
_method
()
1855 self
._do
_common
_method
()
1858 class ServerRunner(testserver_base
.TestServerRunner
):
1859 """TestServerRunner for the net test servers."""
1862 super(ServerRunner
, self
).__init
__()
1863 self
.__ocsp
_server
= None
1865 def __make_data_dir(self
):
1866 if self
.options
.data_dir
:
1867 if not os
.path
.isdir(self
.options
.data_dir
):
1868 raise testserver_base
.OptionError('specified data dir not found: ' +
1869 self
.options
.data_dir
+ ' exiting...')
1870 my_data_dir
= self
.options
.data_dir
1872 # Create the default path to our data dir, relative to the exe dir.
1873 my_data_dir
= os
.path
.join(BASE_DIR
, "..", "..", "..", "..",
1876 #TODO(ibrar): Must use Find* funtion defined in google\tools
1877 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1881 def create_server(self
, server_data
):
1882 port
= self
.options
.port
1883 host
= self
.options
.host
1885 if self
.options
.server_type
== SERVER_HTTP
:
1886 if self
.options
.https
:
1887 pem_cert_and_key
= None
1888 if self
.options
.cert_and_key_file
:
1889 if not os
.path
.isfile(self
.options
.cert_and_key_file
):
1890 raise testserver_base
.OptionError(
1891 'specified server cert file not found: ' +
1892 self
.options
.cert_and_key_file
+ ' exiting...')
1893 pem_cert_and_key
= file(self
.options
.cert_and_key_file
, 'r').read()
1895 # generate a new certificate and run an OCSP server for it.
1896 self
.__ocsp
_server
= OCSPServer((host
, 0), OCSPHandler
)
1897 print ('OCSP server started on %s:%d...' %
1898 (host
, self
.__ocsp
_server
.server_port
))
1903 if self
.options
.ocsp
== 'ok':
1904 ocsp_state
= minica
.OCSP_STATE_GOOD
1905 elif self
.options
.ocsp
== 'revoked':
1906 ocsp_state
= minica
.OCSP_STATE_REVOKED
1907 elif self
.options
.ocsp
== 'invalid':
1908 ocsp_state
= minica
.OCSP_STATE_INVALID
1909 elif self
.options
.ocsp
== 'unauthorized':
1910 ocsp_state
= minica
.OCSP_STATE_UNAUTHORIZED
1911 elif self
.options
.ocsp
== 'unknown':
1912 ocsp_state
= minica
.OCSP_STATE_UNKNOWN
1914 raise testserver_base
.OptionError('unknown OCSP status: ' +
1915 self
.options
.ocsp_status
)
1917 (pem_cert_and_key
, ocsp_der
) = minica
.GenerateCertKeyAndOCSP(
1918 subject
= "127.0.0.1",
1919 ocsp_url
= ("http://%s:%d/ocsp" %
1920 (host
, self
.__ocsp
_server
.server_port
)),
1921 ocsp_state
= ocsp_state
,
1922 serial
= self
.options
.cert_serial
)
1924 self
.__ocsp
_server
.ocsp_response
= ocsp_der
1926 for ca_cert
in self
.options
.ssl_client_ca
:
1927 if not os
.path
.isfile(ca_cert
):
1928 raise testserver_base
.OptionError(
1929 'specified trusted client CA file not found: ' + ca_cert
+
1931 server
= HTTPSServer((host
, port
), TestPageHandler
, pem_cert_and_key
,
1932 self
.options
.ssl_client_auth
,
1933 self
.options
.ssl_client_ca
,
1934 self
.options
.ssl_bulk_cipher
,
1935 self
.options
.record_resume
,
1936 self
.options
.tls_intolerant
)
1937 print 'HTTPS server started on %s:%d...' % (host
, server
.server_port
)
1939 server
= HTTPServer((host
, port
), TestPageHandler
)
1940 print 'HTTP server started on %s:%d...' % (host
, server
.server_port
)
1942 server
.data_dir
= self
.__make
_data
_dir
()
1943 server
.file_root_url
= self
.options
.file_root_url
1944 server_data
['port'] = server
.server_port
1945 elif self
.options
.server_type
== SERVER_WEBSOCKET
:
1946 # Launch pywebsocket via WebSocketServer.
1947 logger
= logging
.getLogger()
1948 logger
.addHandler(logging
.StreamHandler())
1949 # TODO(toyoshim): Remove following os.chdir. Currently this operation
1950 # is required to work correctly. It should be fixed from pywebsocket side.
1951 os
.chdir(self
.__make
_data
_dir
())
1952 websocket_options
= WebSocketOptions(host
, port
, '.')
1953 if self
.options
.cert_and_key_file
:
1954 websocket_options
.use_tls
= True
1955 websocket_options
.private_key
= self
.options
.cert_and_key_file
1956 websocket_options
.certificate
= self
.options
.cert_and_key_file
1957 if self
.options
.ssl_client_auth
:
1958 websocket_options
.tls_client_auth
= True
1959 if len(self
.options
.ssl_client_ca
) != 1:
1960 raise testserver_base
.OptionError(
1961 'one trusted client CA file should be specified')
1962 if not os
.path
.isfile(self
.options
.ssl_client_ca
[0]):
1963 raise testserver_base
.OptionError(
1964 'specified trusted client CA file not found: ' +
1965 self
.options
.ssl_client_ca
[0] + ' exiting...')
1966 websocket_options
.tls_client_ca
= self
.options
.ssl_client_ca
[0]
1967 server
= WebSocketServer(websocket_options
)
1968 print 'WebSocket server started on %s:%d...' % (host
, server
.server_port
)
1969 server_data
['port'] = server
.server_port
1970 elif self
.options
.server_type
== SERVER_TCP_ECHO
:
1971 # Used for generating the key (randomly) that encodes the "echo request"
1974 server
= TCPEchoServer((host
, port
), TCPEchoHandler
)
1975 print 'Echo TCP server started on port %d...' % server
.server_port
1976 server_data
['port'] = server
.server_port
1977 elif self
.options
.server_type
== SERVER_UDP_ECHO
:
1978 # Used for generating the key (randomly) that encodes the "echo request"
1981 server
= UDPEchoServer((host
, port
), UDPEchoHandler
)
1982 print 'Echo UDP server started on port %d...' % server
.server_port
1983 server_data
['port'] = server
.server_port
1984 elif self
.options
.server_type
== SERVER_BASIC_AUTH_PROXY
:
1985 server
= HTTPServer((host
, port
), BasicAuthProxyRequestHandler
)
1986 print 'BasicAuthProxy server started on port %d...' % server
.server_port
1987 server_data
['port'] = server
.server_port
1988 elif self
.options
.server_type
== SERVER_FTP
:
1989 my_data_dir
= self
.__make
_data
_dir
()
1991 # Instantiate a dummy authorizer for managing 'virtual' users
1992 authorizer
= pyftpdlib
.ftpserver
.DummyAuthorizer()
1994 # Define a new user having full r/w permissions and a read-only
1996 authorizer
.add_user('chrome', 'chrome', my_data_dir
, perm
='elradfmw')
1998 authorizer
.add_anonymous(my_data_dir
)
2000 # Instantiate FTP handler class
2001 ftp_handler
= pyftpdlib
.ftpserver
.FTPHandler
2002 ftp_handler
.authorizer
= authorizer
2004 # Define a customized banner (string returned when client connects)
2005 ftp_handler
.banner
= ("pyftpdlib %s based ftpd ready." %
2006 pyftpdlib
.ftpserver
.__ver
__)
2008 # Instantiate FTP server class and listen to address:port
2009 server
= pyftpdlib
.ftpserver
.FTPServer((host
, port
), ftp_handler
)
2010 server_data
['port'] = server
.socket
.getsockname()[1]
2011 print 'FTP server started on port %d...' % server_data
['port']
2013 raise testserver_base
.OptionError('unknown server type' +
2014 self
.options
.server_type
)
2018 def run_server(self
):
2019 if self
.__ocsp
_server
:
2020 self
.__ocsp
_server
.serve_forever_on_thread()
2022 testserver_base
.TestServerRunner
.run_server(self
)
2024 if self
.__ocsp
_server
:
2025 self
.__ocsp
_server
.stop_serving()
2027 def add_options(self
):
2028 testserver_base
.TestServerRunner
.add_options(self
)
2029 self
.option_parser
.add_option('-f', '--ftp', action
='store_const',
2030 const
=SERVER_FTP
, default
=SERVER_HTTP
,
2032 help='start up an FTP server.')
2033 self
.option_parser
.add_option('--tcp-echo', action
='store_const',
2034 const
=SERVER_TCP_ECHO
, default
=SERVER_HTTP
,
2036 help='start up a tcp echo server.')
2037 self
.option_parser
.add_option('--udp-echo', action
='store_const',
2038 const
=SERVER_UDP_ECHO
, default
=SERVER_HTTP
,
2040 help='start up a udp echo server.')
2041 self
.option_parser
.add_option('--basic-auth-proxy', action
='store_const',
2042 const
=SERVER_BASIC_AUTH_PROXY
,
2043 default
=SERVER_HTTP
, dest
='server_type',
2044 help='start up a proxy server which requires '
2045 'basic authentication.')
2046 self
.option_parser
.add_option('--websocket', action
='store_const',
2047 const
=SERVER_WEBSOCKET
, default
=SERVER_HTTP
,
2049 help='start up a WebSocket server.')
2050 self
.option_parser
.add_option('--https', action
='store_true',
2051 dest
='https', help='Specify that https '
2053 self
.option_parser
.add_option('--cert-and-key-file',
2054 dest
='cert_and_key_file', help='specify the '
2055 'path to the file containing the certificate '
2056 'and private key for the server in PEM '
2058 self
.option_parser
.add_option('--ocsp', dest
='ocsp', default
='ok',
2059 help='The type of OCSP response generated '
2060 'for the automatically generated '
2061 'certificate. One of [ok,revoked,invalid]')
2062 self
.option_parser
.add_option('--cert-serial', dest
='cert_serial',
2063 default
=0, type=int,
2064 help='If non-zero then the generated '
2065 'certificate will have this serial number')
2066 self
.option_parser
.add_option('--tls-intolerant', dest
='tls_intolerant',
2067 default
='0', type='int',
2068 help='If nonzero, certain TLS connections '
2069 'will be aborted in order to test version '
2070 'fallback. 1 means all TLS versions will be '
2071 'aborted. 2 means TLS 1.1 or higher will be '
2072 'aborted. 3 means TLS 1.2 or higher will be '
2074 self
.option_parser
.add_option('--https-record-resume',
2075 dest
='record_resume', const
=True,
2076 default
=False, action
='store_const',
2077 help='Record resumption cache events rather '
2078 'than resuming as normal. Allows the use of '
2079 'the /ssl-session-cache request')
2080 self
.option_parser
.add_option('--ssl-client-auth', action
='store_true',
2081 help='Require SSL client auth on every '
2083 self
.option_parser
.add_option('--ssl-client-ca', action
='append',
2084 default
=[], help='Specify that the client '
2085 'certificate request should include the CA '
2086 'named in the subject of the DER-encoded '
2087 'certificate contained in the specified '
2088 'file. This option may appear multiple '
2089 'times, indicating multiple CA names should '
2090 'be sent in the request.')
2091 self
.option_parser
.add_option('--ssl-bulk-cipher', action
='append',
2092 help='Specify the bulk encryption '
2093 'algorithm(s) that will be accepted by the '
2094 'SSL server. Valid values are "aes256", '
2095 '"aes128", "3des", "rc4". If omitted, all '
2096 'algorithms will be used. This option may '
2097 'appear multiple times, indicating '
2098 'multiple algorithms should be enabled.');
2099 self
.option_parser
.add_option('--file-root-url', default
='/files/',
2100 help='Specify a root URL for files served.')
2103 if __name__
== '__main__':
2104 sys
.exit(ServerRunner().main())