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