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.
38 import pyftpdlib
.ftpserver
39 import testserver_base
43 BASE_DIR
= os
.path
.dirname(os
.path
.abspath(__file__
))
45 0, os
.path
.join(BASE_DIR
, '..', '..', '..', 'third_party/pywebsocket/src'))
46 from mod_pywebsocket
.standalone
import WebSocketServer
52 SERVER_BASIC_AUTH_PROXY
= 4
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
65 self
.websock_handlers
= data_dir
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
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."""
89 def __getitem__(self
, sessionID
):
90 self
.log
.append(('lookup', sessionID
))
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
105 class OCSPServer(testserver_base
.ClientRestrictingServerMixIn
,
106 testserver_base
.BrokenPipeHandlerMixIn
,
107 BaseHTTPServer
.HTTPServer
):
108 """This is a specialization of HTTPServer that serves an
111 def serve_forever_on_thread(self
):
112 self
.thread
= threading
.Thread(target
= self
.serve_forever
,
113 name
= "OCSPServerThread")
116 def stop_serving(self
):
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
136 self
.private_key
= tlslite
.api
.parsePEMKey(pem_cert_and_key
,
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()
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()
157 self
.session_cache
= tlslite
.api
.SessionCache()
158 testserver_base
.StoppableHTTPServer
.__init
__(self
,
160 request_hander_class
)
162 def handshake(self
, tlsConnection
):
163 """Creates the SSL connection."""
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
176 except tlslite
.api
.TLSAbruptCloseError
:
177 # Ignore abrupt close.
179 except tlslite
.api
.TLSError
, error
:
180 print "Handshake failure:", str(error
)
184 class FTPServer(testserver_base
.ClientRestrictingServerMixIn
,
185 pyftpdlib
.ftpserver
.FTPServer
):
186 """This is a specialization of FTPServer that adds client verification."""
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
):
205 self
.nonce_time
= None
207 self
.handle_request()
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
):
225 self
.nonce_time
= None
227 self
.handle_request()
231 class TestPageHandler(testserver_base
.BasePageHandler
):
232 # Class variables to allow for persistence state between page handler
235 fail_precondition
= {}
237 def __init__(self
, request
, client_address
, socket_server
):
239 self
.RedirectConnectHandler
,
240 self
.ServerAuthConnectHandler
,
241 self
.DefaultConnectResponseHandler
]
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
,
259 self
.EchoHeaderCache
,
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
,
279 self
.CloseSocketHandler
,
280 self
.RangeResetHandler
,
281 self
.DefaultResponseHandler
]
283 self
.EchoTitleHandler
,
285 self
.PostOnlyFileHandler
] + get_handlers
287 self
.EchoTitleHandler
,
288 self
.EchoHandler
] + get_handlers
291 self
.DefaultResponseHandler
]
294 'crx' : 'application/x-chrome-extension',
295 'exe' : 'application/octet-stream',
297 'jpeg' : 'image/jpeg',
298 'jpg' : 'image/jpeg',
299 'json': 'application/json',
300 'pdf' : 'application/pdf',
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:
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"):
330 self
.send_response(200)
331 self
.send_header('Cache-Control', 'max-age=0')
332 self
.send_header('Content-Type', 'text/html')
335 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
347 self
.send_response(200)
348 self
.send_header('Cache-Control', 'no-cache')
349 self
.send_header('Content-Type', 'text/html')
352 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
364 self
.send_response(200)
365 self
.send_header('Cache-Control', 'max-age=60')
366 self
.send_header('Content-Type', 'text/html')
369 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
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')
386 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
398 self
.send_response(200)
399 self
.send_header('Content-Type', 'text/html')
400 self
.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
403 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
415 self
.send_response(200)
416 self
.send_header('Content-Type', 'text/html')
417 self
.send_header('Cache-Control', 'max-age=3, private')
420 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
432 self
.send_response(200)
433 self
.send_header('Content-Type', 'text/html')
434 self
.send_header('Cache-Control', 'max-age=3, public')
437 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
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')
454 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
466 self
.send_response(200)
467 self
.send_header('Content-Type', 'text/html')
468 self
.send_header('Cache-Control', 'must-revalidate')
471 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
484 self
.send_response(200)
485 self
.send_header('Content-Type', 'text/html')
486 self
.send_header('Cache-Control', 'max-age=60, must-revalidate')
489 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
501 self
.send_response(200)
502 self
.send_header('Content-Type', 'text/html')
503 self
.send_header('Cache-Control', 'no-store')
506 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
519 self
.send_response(200)
520 self
.send_header('Content-Type', 'text/html')
521 self
.send_header('Cache-Control', 'max-age=60, no-store')
524 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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"):
538 self
.send_response(200)
539 self
.send_header('Content-Type', 'text/html')
540 self
.send_header('Cache-Control', 'no-transform')
543 self
.wfile
.write('<html><head><title>%s</title></head></html>' %
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
):
565 query_char
= self
.path
.find('?')
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')
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
)
581 if len(header_name
) > 0:
582 self
.wfile
.write(self
.headers
.getheader(header_name
))
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.
597 line
= self
.rfile
.readline()
598 length
= int(line
, 16)
600 self
.rfile
.readline()
602 body
+= self
.rfile
.read(length
)
606 def EchoHandler(self
):
607 """This handler just echoes back the payload of the request, for testing
610 if not self
._ShouldHandleRequest
("/echo"):
613 self
.send_response(200)
614 self
.send_header('Content-Type', 'text/html')
616 self
.wfile
.write(self
.ReadRequestBody())
619 def EchoTitleHandler(self
):
620 """This handler is like Echo, but sets the page title to the request."""
622 if not self
._ShouldHandleRequest
("/echotitle"):
625 self
.send_response(200)
626 self
.send_header('Content-Type', 'text/html')
628 request
= self
.ReadRequestBody()
629 self
.wfile
.write('<html><head><title>')
630 self
.wfile
.write(request
)
631 self
.wfile
.write('</title></head></html>')
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"):
641 self
.send_response(200)
642 self
.send_header('Content-Type', 'text/html')
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)
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>')
665 def DownloadHandler(self
):
666 """This handler sends a downloadable file with or without reporting
669 if self
.path
.startswith("/download-unknown-size"):
671 elif self
.path
.startswith("/download-known-size"):
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
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')
691 self
.send_header('Content-Length', size_chunk1
+ size_chunk2
)
694 # First chunk of data:
695 self
.wfile
.write("*" * size_chunk1
)
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
)
707 def DownloadFinishHandler(self
):
708 """This handler just tells the server to finish the current download."""
710 if not self
._ShouldHandleRequest
("/download-finish"):
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')
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
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:
737 'replace_text must be of form old_text:new_text. Actual value: %s' %
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
)
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
):
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'):
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
780 f
= open(file_path
, "rb")
782 uncompressed_len
= len(data
)
786 data
= zlib
.compress(data
)
787 compressed_len
= len(data
)
789 content_length
= compressed_len
791 content_length
= uncompressed_len
793 content_length
= compressed_len
/ 2
795 content_length
= (compressed_len
+ uncompressed_len
) / 2
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
+ '\'')
807 self
.wfile
.write(data
)
811 def FileHandler(self
):
812 """This handler sends the contents of the requested file. Wow, it's like
815 prefix
= self
.server
.file_root_url
816 if not self
.path
.startswith(prefix
):
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
):
826 return self
._FileHandlerHelper
(prefix
)
828 def _FileHandlerHelper(self
, prefix
):
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)
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)
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
864 f
= open(file_path
, "rb")
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")
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
))
886 header_values
= re
.findall('(\S+):\s*(.*)', line
)
887 if len(header_values
) > 0:
889 name
, value
= header_values
[0]
890 self
.send_header(name
, value
)
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])
903 end
= int(range_header
[1])
907 self
.send_response(206)
908 content_range
= ('bytes ' + str(start
) + '-' + str(end
) + '/' +
910 self
.send_header('Content-Range', content_range
)
911 data
= data
[start
: end
+ 1]
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
+ '\'')
921 if (self
.command
!= 'HEAD'):
922 self
.wfile
.write(data
)
924 self
.protocol_version
= old_protocol_version
927 def SetCookieHandler(self
):
928 """This handler just sets a cookie, for testing cookie handling."""
930 if not self
._ShouldHandleRequest
("/set-cookie"):
933 query_char
= self
.path
.find('?')
935 cookie_values
= self
.path
[query_char
+ 1:].split('&')
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
)
943 for cookie_value
in cookie_values
:
944 self
.wfile
.write('%s' % cookie_value
)
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"):
954 query_char
= self
.path
.find('?')
956 num_cookies
= int(self
.path
[query_char
+ 1:])
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=')
964 self
.wfile
.write('%d cookies were sent' % num_cookies
)
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
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"):
979 _
, _
, _
, _
, query
, _
= urlparse
.urlparse(self
.path
)
980 query_dict
= cgi
.parse_qs(query
)
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
)
995 for data_value
in query_dict
.get('data', []):
996 self
.wfile
.write(data_value
)
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"):
1006 query_char
= self
.path
.find('?')
1007 if query_char
!= -1:
1008 headers_values
= self
.path
[query_char
+ 1:].split('&')
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
)
1018 for header_value
in headers_values
:
1019 self
.wfile
.write('%s' % header_value
)
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"):
1029 username
= userpass
= password
= b64str
= ""
1030 expected_password
= 'secret'
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')
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')
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>')
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)
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
1090 f
= open(gif_path
, "rb")
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')
1099 self
.wfile
.write(data
)
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')
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
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.
1123 force_reset: Iff set, the nonce will be changed. Useful for testing the
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"):
1143 stale
= 'stale' in self
.path
1144 nonce
= self
.GetNonce(force_reset
=stale
)
1145 opaque
= hashlib
.md5('opaque').hexdigest()
1149 auth
= self
.headers
.getheader('authorization')
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()
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)
1187 'opaque="%s"') % (realm
, nonce
, opaque
)
1189 hdr
+= ', stale="TRUE"'
1190 self
.send_header('WWW-Authenticate', hdr
)
1191 self
.send_header('Content-Type', 'text/html')
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>')
1203 # Authentication successful.
1204 self
.send_response(200)
1205 self
.send_header('Content-Type', 'text/html')
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>')
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"):
1222 query_char
= self
.path
.find('?')
1226 wait_sec
= int(self
.path
[query_char
+ 1:])
1229 time
.sleep(wait_sec
)
1230 self
.send_response(200)
1231 self
.send_header('Content-Type', 'text/plain')
1233 self
.wfile
.write("waited %d seconds" % wait_sec
)
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"):
1247 query_char
= self
.path
.find('?')
1248 chunkedSettings
= {'waitBeforeHeaders' : 0,
1249 'waitBetweenChunks' : 0,
1253 params
= self
.path
[query_char
+ 1:].split('&')
1254 for param
in params
:
1255 keyValue
= param
.split('=')
1256 if len(keyValue
) == 2:
1258 chunkedSettings
[keyValue
[0]] = int(keyValue
[1])
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')
1268 # Chunked encoding: sending all chunks, then final zero-length chunk and
1270 for i
in range(0, chunkedSettings
['chunksNumber']):
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('')
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"):
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
)
1292 self
.wfile
.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n")
1295 def NoContentHandler(self
):
1296 """Returns a 204 No Content response."""
1298 if not self
._ShouldHandleRequest
("/nocontent"):
1300 self
.send_response(204)
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
):
1313 query_char
= self
.path
.find('?')
1314 if query_char
< 0 or len(self
.path
) <= query_char
+ 1:
1315 self
.sendRedirectHelp(test_name
)
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')
1323 self
.wfile
.write('<html><head>')
1324 self
.wfile
.write('</head><body>Redirecting to %s</body></html>' % dest
)
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
):
1337 query_char
= self
.path
.find('?')
1338 if query_char
< 0 or len(self
.path
) <= query_char
+ 1:
1339 self
.sendRedirectHelp(test_name
)
1341 dest
= self
.path
[query_char
+ 1:]
1343 self
.send_response(200)
1344 self
.send_header('Content-Type', 'text/html')
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
)
1352 def MultipartHandler(self
):
1353 """Send a multipart response (10 text/html pages)."""
1355 test_name
= '/multipart'
1356 if not self
._ShouldHandleRequest
(test_name
):
1361 self
.send_response(200)
1362 self
.send_header('Content-Type',
1363 'multipart/x-mixed-replace;boundary=' + bound
)
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
+ '--')
1375 def GetSSLSessionCacheHandler(self
):
1376 """Send a reply containing a log of the session cache operations."""
1378 if not self
._ShouldHandleRequest
('/ssl-session-cache'):
1381 self
.send_response(200)
1382 self
.send_header('Content-Type', 'text/plain')
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' +
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
1397 if not self
._ShouldHandleRequest
('/ssl-many-small-records'):
1400 self
.send_response(200)
1401 self
.send_header('Content-Type', 'text/plain')
1404 # Write ~26K of data, in 1350 byte chunks
1405 for i
in xrange(20):
1406 self
.wfile
.write('*' * 1350)
1410 def GetChannelID(self
):
1411 """Send a reply containing the hashed ChannelID that the client provided."""
1413 if not self
._ShouldHandleRequest
('/channel-id'):
1416 self
.send_response(200)
1417 self
.send_header('Content-Type', 'text/plain')
1419 channel_id
= self
.server
.tlsConnection
.channel_id
.tostring()
1420 self
.wfile
.write(hashlib
.sha256(channel_id
).digest().encode('base64'))
1423 def CloseSocketHandler(self
):
1424 """Closes the socket without sending anything."""
1426 if not self
._ShouldHandleRequest
('/close-socket'):
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
1441 return ''.join([chr(y
% 256)
1442 for y
in range(start
* 2 + 15, end
* 2 + 15, 2)])
1444 if not self
._ShouldHandleRequest
('/rangereset'):
1447 _
, _
, url_path
, _
, query
, _
= urlparse
.urlparse(self
.path
)
1451 # Note that the rst is sent just before sending the rst_boundary byte.
1453 respond_to_range
= True
1454 hold_for_signal
= False
1457 fail_precondition
= 0
1458 send_verifiers
= True
1461 qdict
= urlparse
.parse_qs(query
, True)
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
1475 # Note that hold_for_signal will not work with null range requests;
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
)
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
1493 last_byte
= size
- 1
1495 # Does that define what we want to return, or do we need to apply
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
)
1502 first_byte
= int(mo
.group(1))
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
:
1511 if (fail_precondition
and
1512 (self
.headers
.getheader('If-Modified-Since') or
1513 self
.headers
.getheader('If-Match'))):
1514 self
.send_response(412)
1519 self
.send_response(206)
1520 self
.send_header('Content-Range',
1521 'bytes %d-%d/%d' % (first_byte
, last_byte
, size
))
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)
1527 self
.send_header('Etag', '"XYZZY"')
1528 self
.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST')
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))
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
)
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.
1571 self
.connection
.close()
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
))
1586 if (self
.command
!= 'HEAD'):
1587 self
.wfile
.write(contents
)
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
1595 if (self
.path
.find("www.redirect.com") < 0):
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')
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):
1614 challenge
= 'Basic realm="WallyWorld"'
1616 self
.send_response(401) # unauthorized
1617 self
.send_header('WWW-Authenticate', challenge
)
1618 self
.send_header('Connection', 'close')
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
))
1632 self
.wfile
.write(contents
)
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')
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
, [],
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
)))
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.
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.
1683 return_data
= echo_message
.GetEchoResponseData(data
)
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.
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.
1707 return_data
= echo_message
.GetEchoResponseData(data
)
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
):
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"')
1737 def _start_read_write(self
, sock
):
1739 self
.request
.setblocking(0)
1740 rlist
= [self
.request
, sock
]
1742 ready_sockets
, _unused
, errors
= select
.select(rlist
, [], [])
1744 self
.send_response(500)
1747 for s
in ready_sockets
:
1748 received
= s
.recv(1024)
1749 if len(received
) == 0:
1751 if s
== self
.request
:
1754 other
= self
.request
1755 other
.send(received
)
1757 def _do_common_method(self
):
1758 url
= urlparse
.urlparse(self
.path
)
1761 if url
.scheme
== 'http':
1763 elif url
.scheme
== 'https':
1765 if not url
.hostname
or not port
:
1766 self
.send_response(400)
1770 if len(url
.path
) == 0:
1774 if len(url
.query
) > 0:
1775 path
= '%s?%s' % (url
.path
, url
.query
)
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')):
1787 sock
.send('%s\r\n' % header
)
1789 self
._start
_read
_write
(sock
)
1791 self
.send_response(500)
1794 if sock
is not None:
1797 def do_CONNECT(self
):
1799 pos
= self
.path
.rfind(':')
1800 host
= self
.path
[:pos
]
1801 port
= int(self
.path
[pos
+1:])
1803 self
.send_response(400)
1807 sock
= socket
.create_connection((host
, port
))
1808 self
.send_response(200, 'Connection established')
1810 self
._start
_read
_write
(sock
)
1812 self
.send_response(500)
1818 self
._do
_common
_method
()
1821 self
._do
_common
_method
()
1824 class ServerRunner(testserver_base
.TestServerRunner
):
1825 """TestServerRunner for the net test servers."""
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
1838 # Create the default path to our data dir, relative to the exe dir.
1839 my_data_dir
= os
.path
.join(BASE_DIR
, "..", "..", "..", "..",
1842 #TODO(ibrar): Must use Find* funtion defined in google\tools
1843 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
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()
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
))
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
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
+
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
)
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"
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"
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
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']
1979 raise testserver_base
.OptionError('unknown server type' +
1980 self
.options
.server_type
)
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
,
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
,
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
,
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
,
2015 help='start up a WebSocket server.')
2016 self
.option_parser
.add_option('--https', action
='store_true',
2017 dest
='https', help='Specify that https '
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 '
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 '
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 '
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())