[Android] Implement 3-way sensor fallback for Device Orientation.
[chromium-blink-merge.git] / net / tools / testserver / testserver.py
blobad1a57d0e4f606294bd1e8801ca0e612107d5069
1 #!/usr/bin/env python
2 # Copyright 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """This is a simple HTTP/FTP/TCP/UDP/BASIC_AUTH_PROXY/WEBSOCKET server used for
7 testing Chrome.
9 It supports several test URLs, as specified by the handlers in TestPageHandler.
10 By default, it listens on an ephemeral port and sends the port number back to
11 the originating process over a pipe. The originating process can specify an
12 explicit port if necessary.
13 It can use https if you specify the flag --https=CERT where CERT is the path
14 to a pem file containing the certificate and private key that should be used.
15 """
17 import base64
18 import BaseHTTPServer
19 import cgi
20 import hashlib
21 import logging
22 import minica
23 import os
24 import json
25 import random
26 import re
27 import select
28 import socket
29 import SocketServer
30 import ssl
31 import struct
32 import sys
33 import threading
34 import time
35 import urllib
36 import urlparse
37 import zlib
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.
46 try:
47 os.remove(os.path.join(ROOT_DIR, 'third_party', 'tlslite',
48 'tlslite', 'utils', 'hmac.pyc'))
49 except Exception:
50 pass
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
56 # unconditionally.
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
62 # import manually
63 mod_pywebsocket.standalone.ssl = ssl
65 import pyftpdlib.ftpserver
67 import tlslite
68 import tlslite.api
70 import echo_message
71 import testserver_base
73 SERVER_HTTP = 0
74 SERVER_FTP = 1
75 SERVER_TCP_ECHO = 2
76 SERVER_UDP_ECHO = 3
77 SERVER_BASIC_AUTH_PROXY = 4
78 SERVER_WEBSOCKET = 5
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
89 self.port = port
90 self.websock_handlers = data_dir
91 self.scan_dir = None
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
97 self.strict = True
99 self.use_tls = 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."""
113 def __init__(self):
114 self.log = []
116 def __getitem__(self, sessionID):
117 self.log.append(('lookup', sessionID))
118 raise KeyError()
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
128 verification."""
130 pass
132 class OCSPServer(testserver_base.ClientRestrictingServerMixIn,
133 testserver_base.BrokenPipeHandlerMixIn,
134 BaseHTTPServer.HTTPServer):
135 """This is a specialization of HTTPServer that serves an
136 OCSP response"""
138 def serve_forever_on_thread(self):
139 self.thread = threading.Thread(target = self.serve_forever,
140 name = "OCSPServerThread")
141 self.thread.start()
143 def stop_serving(self):
144 self.shutdown()
145 self.thread.join()
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,
161 alert_after_handshake):
162 self.cert_chain = tlslite.api.X509CertChain()
163 self.cert_chain.parsePemList(pem_cert_and_key)
164 # Force using only python implementation - otherwise behavior is different
165 # depending on whether m2crypto Python module is present (error is thrown
166 # when it is). m2crypto uses a C (based on OpenSSL) implementation under
167 # the hood.
168 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key,
169 private=True,
170 implementations=['python'])
171 self.ssl_client_auth = ssl_client_auth
172 self.ssl_client_cas = []
173 self.ssl_client_cert_types = []
174 if enable_npn:
175 self.next_protos = ['http/1.1']
176 else:
177 self.next_protos = None
178 self.signed_cert_timestamps = signed_cert_timestamps
179 self.fallback_scsv_enabled = fallback_scsv_enabled
180 self.ocsp_response = ocsp_response
182 if ssl_client_auth:
183 for ca_file in ssl_client_cas:
184 s = open(ca_file).read()
185 x509 = tlslite.api.X509()
186 x509.parse(s)
187 self.ssl_client_cas.append(x509.subject)
189 for cert_type in ssl_client_cert_types:
190 self.ssl_client_cert_types.append({
191 "rsa_sign": tlslite.api.ClientCertificateType.rsa_sign,
192 "ecdsa_sign": tlslite.api.ClientCertificateType.ecdsa_sign,
193 }[cert_type])
195 self.ssl_handshake_settings = tlslite.api.HandshakeSettings()
196 # Enable SSLv3 for testing purposes.
197 self.ssl_handshake_settings.minVersion = (3, 0)
198 if ssl_bulk_ciphers is not None:
199 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers
200 if ssl_key_exchanges is not None:
201 self.ssl_handshake_settings.keyExchangeNames = ssl_key_exchanges
202 if tls_intolerant != 0:
203 self.ssl_handshake_settings.tlsIntolerant = (3, tls_intolerant)
204 self.ssl_handshake_settings.tlsIntoleranceType = tls_intolerance_type
205 if alert_after_handshake:
206 self.ssl_handshake_settings.alertAfterHandshake = True
208 if record_resume_info:
209 # If record_resume_info is true then we'll replace the session cache with
210 # an object that records the lookups and inserts that it sees.
211 self.session_cache = RecordingSSLSessionCache()
212 else:
213 self.session_cache = tlslite.api.SessionCache()
214 testserver_base.StoppableHTTPServer.__init__(self,
215 server_address,
216 request_hander_class)
218 def handshake(self, tlsConnection):
219 """Creates the SSL connection."""
221 try:
222 self.tlsConnection = tlsConnection
223 tlsConnection.handshakeServer(certChain=self.cert_chain,
224 privateKey=self.private_key,
225 sessionCache=self.session_cache,
226 reqCert=self.ssl_client_auth,
227 settings=self.ssl_handshake_settings,
228 reqCAs=self.ssl_client_cas,
229 reqCertTypes=self.ssl_client_cert_types,
230 nextProtos=self.next_protos,
231 signedCertTimestamps=
232 self.signed_cert_timestamps,
233 fallbackSCSV=self.fallback_scsv_enabled,
234 ocspResponse = self.ocsp_response)
235 tlsConnection.ignoreAbruptClose = True
236 return True
237 except tlslite.api.TLSAbruptCloseError:
238 # Ignore abrupt close.
239 return True
240 except tlslite.api.TLSError, error:
241 print "Handshake failure:", str(error)
242 return False
245 class FTPServer(testserver_base.ClientRestrictingServerMixIn,
246 pyftpdlib.ftpserver.FTPServer):
247 """This is a specialization of FTPServer that adds client verification."""
249 pass
252 class TCPEchoServer(testserver_base.ClientRestrictingServerMixIn,
253 SocketServer.TCPServer):
254 """A TCP echo server that echoes back what it has received."""
256 def server_bind(self):
257 """Override server_bind to store the server name."""
259 SocketServer.TCPServer.server_bind(self)
260 host, port = self.socket.getsockname()[:2]
261 self.server_name = socket.getfqdn(host)
262 self.server_port = port
264 def serve_forever(self):
265 self.stop = False
266 self.nonce_time = None
267 while not self.stop:
268 self.handle_request()
269 self.socket.close()
272 class UDPEchoServer(testserver_base.ClientRestrictingServerMixIn,
273 SocketServer.UDPServer):
274 """A UDP echo server that echoes back what it has received."""
276 def server_bind(self):
277 """Override server_bind to store the server name."""
279 SocketServer.UDPServer.server_bind(self)
280 host, port = self.socket.getsockname()[:2]
281 self.server_name = socket.getfqdn(host)
282 self.server_port = port
284 def serve_forever(self):
285 self.stop = False
286 self.nonce_time = None
287 while not self.stop:
288 self.handle_request()
289 self.socket.close()
292 class TestPageHandler(testserver_base.BasePageHandler):
293 # Class variables to allow for persistence state between page handler
294 # invocations
295 rst_limits = {}
296 fail_precondition = {}
298 def __init__(self, request, client_address, socket_server):
299 connect_handlers = [
300 self.RedirectConnectHandler,
301 self.ServerAuthConnectHandler,
302 self.DefaultConnectResponseHandler]
303 get_handlers = [
304 self.NoCacheMaxAgeTimeHandler,
305 self.NoCacheTimeHandler,
306 self.CacheTimeHandler,
307 self.CacheExpiresHandler,
308 self.CacheProxyRevalidateHandler,
309 self.CachePrivateHandler,
310 self.CachePublicHandler,
311 self.CacheSMaxAgeHandler,
312 self.CacheMustRevalidateHandler,
313 self.CacheMustRevalidateMaxAgeHandler,
314 self.CacheNoStoreHandler,
315 self.CacheNoStoreMaxAgeHandler,
316 self.CacheNoTransformHandler,
317 self.DownloadHandler,
318 self.DownloadFinishHandler,
319 self.EchoHeader,
320 self.EchoHeaderCache,
321 self.EchoAllHandler,
322 self.ZipFileHandler,
323 self.FileHandler,
324 self.SetCookieHandler,
325 self.SetManyCookiesHandler,
326 self.ExpectAndSetCookieHandler,
327 self.SetHeaderHandler,
328 self.AuthBasicHandler,
329 self.AuthDigestHandler,
330 self.SlowServerHandler,
331 self.ChunkedServerHandler,
332 self.NoContentHandler,
333 self.ServerRedirectHandler,
334 self.CrossSiteRedirectHandler,
335 self.ClientRedirectHandler,
336 self.GetSSLSessionCacheHandler,
337 self.SSLManySmallRecords,
338 self.GetChannelID,
339 self.GetClientCert,
340 self.ClientCipherListHandler,
341 self.CloseSocketHandler,
342 self.RangeResetHandler,
343 self.DefaultResponseHandler]
344 post_handlers = [
345 self.EchoTitleHandler,
346 self.EchoHandler,
347 self.PostOnlyFileHandler,
348 self.EchoMultipartPostHandler] + get_handlers
349 put_handlers = [
350 self.EchoTitleHandler,
351 self.EchoHandler] + get_handlers
352 head_handlers = [
353 self.FileHandler,
354 self.DefaultResponseHandler]
356 self._mime_types = {
357 'crx' : 'application/x-chrome-extension',
358 'exe' : 'application/octet-stream',
359 'gif': 'image/gif',
360 'jpeg' : 'image/jpeg',
361 'jpg' : 'image/jpeg',
362 'js' : 'application/javascript',
363 'json': 'application/json',
364 'pdf' : 'application/pdf',
365 'txt' : 'text/plain',
366 'wav' : 'audio/wav',
367 'xml' : 'text/xml'
369 self._default_mime_type = 'text/html'
371 testserver_base.BasePageHandler.__init__(self, request, client_address,
372 socket_server, connect_handlers,
373 get_handlers, head_handlers,
374 post_handlers, put_handlers)
376 def GetMIMETypeFromName(self, file_name):
377 """Returns the mime type for the specified file_name. So far it only looks
378 at the file extension."""
380 (_shortname, extension) = os.path.splitext(file_name.split("?")[0])
381 if len(extension) == 0:
382 # no extension.
383 return self._default_mime_type
385 # extension starts with a dot, so we need to remove it
386 return self._mime_types.get(extension[1:], self._default_mime_type)
388 def NoCacheMaxAgeTimeHandler(self):
389 """This request handler yields a page with the title set to the current
390 system time, and no caching requested."""
392 if not self._ShouldHandleRequest("/nocachetime/maxage"):
393 return False
395 self.send_response(200)
396 self.send_header('Cache-Control', 'max-age=0')
397 self.send_header('Content-Type', 'text/html')
398 self.end_headers()
400 self.wfile.write('<html><head><title>%s</title></head></html>' %
401 time.time())
403 return True
405 def NoCacheTimeHandler(self):
406 """This request handler yields a page with the title set to the current
407 system time, and no caching requested."""
409 if not self._ShouldHandleRequest("/nocachetime"):
410 return False
412 self.send_response(200)
413 self.send_header('Cache-Control', 'no-cache')
414 self.send_header('Content-Type', 'text/html')
415 self.end_headers()
417 self.wfile.write('<html><head><title>%s</title></head></html>' %
418 time.time())
420 return True
422 def CacheTimeHandler(self):
423 """This request handler yields a page with the title set to the current
424 system time, and allows caching for one minute."""
426 if not self._ShouldHandleRequest("/cachetime"):
427 return False
429 self.send_response(200)
430 self.send_header('Cache-Control', 'max-age=60')
431 self.send_header('Content-Type', 'text/html')
432 self.end_headers()
434 self.wfile.write('<html><head><title>%s</title></head></html>' %
435 time.time())
437 return True
439 def CacheExpiresHandler(self):
440 """This request handler yields a page with the title set to the current
441 system time, and set the page to expire on 1 Jan 2099."""
443 if not self._ShouldHandleRequest("/cache/expires"):
444 return False
446 self.send_response(200)
447 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT')
448 self.send_header('Content-Type', 'text/html')
449 self.end_headers()
451 self.wfile.write('<html><head><title>%s</title></head></html>' %
452 time.time())
454 return True
456 def CacheProxyRevalidateHandler(self):
457 """This request handler yields a page with the title set to the current
458 system time, and allows caching for 60 seconds"""
460 if not self._ShouldHandleRequest("/cache/proxy-revalidate"):
461 return False
463 self.send_response(200)
464 self.send_header('Content-Type', 'text/html')
465 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate')
466 self.end_headers()
468 self.wfile.write('<html><head><title>%s</title></head></html>' %
469 time.time())
471 return True
473 def CachePrivateHandler(self):
474 """This request handler yields a page with the title set to the current
475 system time, and allows caching for 3 seconds."""
477 if not self._ShouldHandleRequest("/cache/private"):
478 return False
480 self.send_response(200)
481 self.send_header('Content-Type', 'text/html')
482 self.send_header('Cache-Control', 'max-age=3, private')
483 self.end_headers()
485 self.wfile.write('<html><head><title>%s</title></head></html>' %
486 time.time())
488 return True
490 def CachePublicHandler(self):
491 """This request handler yields a page with the title set to the current
492 system time, and allows caching for 3 seconds."""
494 if not self._ShouldHandleRequest("/cache/public"):
495 return False
497 self.send_response(200)
498 self.send_header('Content-Type', 'text/html')
499 self.send_header('Cache-Control', 'max-age=3, public')
500 self.end_headers()
502 self.wfile.write('<html><head><title>%s</title></head></html>' %
503 time.time())
505 return True
507 def CacheSMaxAgeHandler(self):
508 """This request handler yields a page with the title set to the current
509 system time, and does not allow for caching."""
511 if not self._ShouldHandleRequest("/cache/s-maxage"):
512 return False
514 self.send_response(200)
515 self.send_header('Content-Type', 'text/html')
516 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0')
517 self.end_headers()
519 self.wfile.write('<html><head><title>%s</title></head></html>' %
520 time.time())
522 return True
524 def CacheMustRevalidateHandler(self):
525 """This request handler yields a page with the title set to the current
526 system time, and does not allow caching."""
528 if not self._ShouldHandleRequest("/cache/must-revalidate"):
529 return False
531 self.send_response(200)
532 self.send_header('Content-Type', 'text/html')
533 self.send_header('Cache-Control', 'must-revalidate')
534 self.end_headers()
536 self.wfile.write('<html><head><title>%s</title></head></html>' %
537 time.time())
539 return True
541 def CacheMustRevalidateMaxAgeHandler(self):
542 """This request handler yields a page with the title set to the current
543 system time, and does not allow caching event though max-age of 60
544 seconds is specified."""
546 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"):
547 return False
549 self.send_response(200)
550 self.send_header('Content-Type', 'text/html')
551 self.send_header('Cache-Control', 'max-age=60, must-revalidate')
552 self.end_headers()
554 self.wfile.write('<html><head><title>%s</title></head></html>' %
555 time.time())
557 return True
559 def CacheNoStoreHandler(self):
560 """This request handler yields a page with the title set to the current
561 system time, and does not allow the page to be stored."""
563 if not self._ShouldHandleRequest("/cache/no-store"):
564 return False
566 self.send_response(200)
567 self.send_header('Content-Type', 'text/html')
568 self.send_header('Cache-Control', 'no-store')
569 self.end_headers()
571 self.wfile.write('<html><head><title>%s</title></head></html>' %
572 time.time())
574 return True
576 def CacheNoStoreMaxAgeHandler(self):
577 """This request handler yields a page with the title set to the current
578 system time, and does not allow the page to be stored even though max-age
579 of 60 seconds is specified."""
581 if not self._ShouldHandleRequest("/cache/no-store/max-age"):
582 return False
584 self.send_response(200)
585 self.send_header('Content-Type', 'text/html')
586 self.send_header('Cache-Control', 'max-age=60, no-store')
587 self.end_headers()
589 self.wfile.write('<html><head><title>%s</title></head></html>' %
590 time.time())
592 return True
595 def CacheNoTransformHandler(self):
596 """This request handler yields a page with the title set to the current
597 system time, and does not allow the content to transformed during
598 user-agent caching"""
600 if not self._ShouldHandleRequest("/cache/no-transform"):
601 return False
603 self.send_response(200)
604 self.send_header('Content-Type', 'text/html')
605 self.send_header('Cache-Control', 'no-transform')
606 self.end_headers()
608 self.wfile.write('<html><head><title>%s</title></head></html>' %
609 time.time())
611 return True
613 def EchoHeader(self):
614 """This handler echoes back the value of a specific request header."""
616 return self.EchoHeaderHelper("/echoheader")
618 def EchoHeaderCache(self):
619 """This function echoes back the value of a specific request header while
620 allowing caching for 16 hours."""
622 return self.EchoHeaderHelper("/echoheadercache")
624 def EchoHeaderHelper(self, echo_header):
625 """This function echoes back the value of the request header passed in."""
627 if not self._ShouldHandleRequest(echo_header):
628 return False
630 query_char = self.path.find('?')
631 if query_char != -1:
632 header_name = self.path[query_char+1:]
634 self.send_response(200)
635 self.send_header('Content-Type', 'text/plain')
636 if echo_header == '/echoheadercache':
637 self.send_header('Cache-control', 'max-age=60000')
638 else:
639 self.send_header('Cache-control', 'no-cache')
640 # insert a vary header to properly indicate that the cachability of this
641 # request is subject to value of the request header being echoed.
642 if len(header_name) > 0:
643 self.send_header('Vary', header_name)
644 self.end_headers()
646 if len(header_name) > 0:
647 self.wfile.write(self.headers.getheader(header_name))
649 return True
651 def ReadRequestBody(self):
652 """This function reads the body of the current HTTP request, handling
653 both plain and chunked transfer encoded requests."""
655 if self.headers.getheader('transfer-encoding') != 'chunked':
656 length = int(self.headers.getheader('content-length'))
657 return self.rfile.read(length)
659 # Read the request body as chunks.
660 body = ""
661 while True:
662 line = self.rfile.readline()
663 length = int(line, 16)
664 if length == 0:
665 self.rfile.readline()
666 break
667 body += self.rfile.read(length)
668 self.rfile.read(2)
669 return body
671 def EchoHandler(self):
672 """This handler just echoes back the payload of the request, for testing
673 form submission."""
675 if not self._ShouldHandleRequest("/echo"):
676 return False
678 _, _, _, _, query, _ = urlparse.urlparse(self.path)
679 query_params = cgi.parse_qs(query, True)
680 if 'status' in query_params:
681 self.send_response(int(query_params['status'][0]))
682 else:
683 self.send_response(200)
684 self.send_header('Content-Type', 'text/html')
685 self.end_headers()
686 self.wfile.write(self.ReadRequestBody())
687 return True
689 def EchoTitleHandler(self):
690 """This handler is like Echo, but sets the page title to the request."""
692 if not self._ShouldHandleRequest("/echotitle"):
693 return False
695 self.send_response(200)
696 self.send_header('Content-Type', 'text/html')
697 self.end_headers()
698 request = self.ReadRequestBody()
699 self.wfile.write('<html><head><title>')
700 self.wfile.write(request)
701 self.wfile.write('</title></head></html>')
702 return True
704 def EchoAllHandler(self):
705 """This handler yields a (more) human-readable page listing information
706 about the request header & contents."""
708 if not self._ShouldHandleRequest("/echoall"):
709 return False
711 self.send_response(200)
712 self.send_header('Content-Type', 'text/html')
713 self.end_headers()
714 self.wfile.write('<html><head><style>'
715 'pre { border: 1px solid black; margin: 5px; padding: 5px }'
716 '</style></head><body>'
717 '<div style="float: right">'
718 '<a href="/echo">back to referring page</a></div>'
719 '<h1>Request Body:</h1><pre>')
721 if self.command == 'POST' or self.command == 'PUT':
722 qs = self.ReadRequestBody()
723 params = cgi.parse_qs(qs, keep_blank_values=1)
725 for param in params:
726 self.wfile.write('%s=%s\n' % (param, params[param][0]))
728 self.wfile.write('</pre>')
730 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers)
732 self.wfile.write('</body></html>')
733 return True
735 def EchoMultipartPostHandler(self):
736 """This handler echoes received multipart post data as json format."""
738 if not (self._ShouldHandleRequest("/echomultipartpost") or
739 self._ShouldHandleRequest("/searchbyimage")):
740 return False
742 content_type, parameters = cgi.parse_header(
743 self.headers.getheader('content-type'))
744 if content_type == 'multipart/form-data':
745 post_multipart = cgi.parse_multipart(self.rfile, parameters)
746 elif content_type == 'application/x-www-form-urlencoded':
747 raise Exception('POST by application/x-www-form-urlencoded is '
748 'not implemented.')
749 else:
750 post_multipart = {}
752 # Since the data can be binary, we encode them by base64.
753 post_multipart_base64_encoded = {}
754 for field, values in post_multipart.items():
755 post_multipart_base64_encoded[field] = [base64.b64encode(value)
756 for value in values]
758 result = {'POST_multipart' : post_multipart_base64_encoded}
760 self.send_response(200)
761 self.send_header("Content-type", "text/plain")
762 self.end_headers()
763 self.wfile.write(json.dumps(result, indent=2, sort_keys=False))
764 return True
766 def DownloadHandler(self):
767 """This handler sends a downloadable file with or without reporting
768 the size (6K)."""
770 if self.path.startswith("/download-unknown-size"):
771 send_length = False
772 elif self.path.startswith("/download-known-size"):
773 send_length = True
774 else:
775 return False
778 # The test which uses this functionality is attempting to send
779 # small chunks of data to the client. Use a fairly large buffer
780 # so that we'll fill chrome's IO buffer enough to force it to
781 # actually write the data.
782 # See also the comments in the client-side of this test in
783 # download_uitest.cc
785 size_chunk1 = 35*1024
786 size_chunk2 = 10*1024
788 self.send_response(200)
789 self.send_header('Content-Type', 'application/octet-stream')
790 self.send_header('Cache-Control', 'max-age=0')
791 if send_length:
792 self.send_header('Content-Length', size_chunk1 + size_chunk2)
793 self.end_headers()
795 # First chunk of data:
796 self.wfile.write("*" * size_chunk1)
797 self.wfile.flush()
799 # handle requests until one of them clears this flag.
800 self.server.wait_for_download = True
801 while self.server.wait_for_download:
802 self.server.handle_request()
804 # Second chunk of data:
805 self.wfile.write("*" * size_chunk2)
806 return True
808 def DownloadFinishHandler(self):
809 """This handler just tells the server to finish the current download."""
811 if not self._ShouldHandleRequest("/download-finish"):
812 return False
814 self.server.wait_for_download = False
815 self.send_response(200)
816 self.send_header('Content-Type', 'text/html')
817 self.send_header('Cache-Control', 'max-age=0')
818 self.end_headers()
819 return True
821 def _ReplaceFileData(self, data, query_parameters):
822 """Replaces matching substrings in a file.
824 If the 'replace_text' URL query parameter is present, it is expected to be
825 of the form old_text:new_text, which indicates that any old_text strings in
826 the file are replaced with new_text. Multiple 'replace_text' parameters may
827 be specified.
829 If the parameters are not present, |data| is returned.
832 query_dict = cgi.parse_qs(query_parameters)
833 replace_text_values = query_dict.get('replace_text', [])
834 for replace_text_value in replace_text_values:
835 replace_text_args = replace_text_value.split(':')
836 if len(replace_text_args) != 2:
837 raise ValueError(
838 'replace_text must be of form old_text:new_text. Actual value: %s' %
839 replace_text_value)
840 old_text_b64, new_text_b64 = replace_text_args
841 old_text = base64.urlsafe_b64decode(old_text_b64)
842 new_text = base64.urlsafe_b64decode(new_text_b64)
843 data = data.replace(old_text, new_text)
844 return data
846 def ZipFileHandler(self):
847 """This handler sends the contents of the requested file in compressed form.
848 Can pass in a parameter that specifies that the content length be
849 C - the compressed size (OK),
850 U - the uncompressed size (Non-standard, but handled),
851 S - less than compressed (OK because we keep going),
852 M - larger than compressed but less than uncompressed (an error),
853 L - larger than uncompressed (an error)
854 Example: compressedfiles/Picture_1.doc?C
857 prefix = "/compressedfiles/"
858 if not self.path.startswith(prefix):
859 return False
861 # Consume a request body if present.
862 if self.command == 'POST' or self.command == 'PUT' :
863 self.ReadRequestBody()
865 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
867 if not query in ('C', 'U', 'S', 'M', 'L'):
868 return False
870 sub_path = url_path[len(prefix):]
871 entries = sub_path.split('/')
872 file_path = os.path.join(self.server.data_dir, *entries)
873 if os.path.isdir(file_path):
874 file_path = os.path.join(file_path, 'index.html')
876 if not os.path.isfile(file_path):
877 print "File not found " + sub_path + " full path:" + file_path
878 self.send_error(404)
879 return True
881 f = open(file_path, "rb")
882 data = f.read()
883 uncompressed_len = len(data)
884 f.close()
886 # Compress the data.
887 data = zlib.compress(data)
888 compressed_len = len(data)
890 content_length = compressed_len
891 if query == 'U':
892 content_length = uncompressed_len
893 elif query == 'S':
894 content_length = compressed_len / 2
895 elif query == 'M':
896 content_length = (compressed_len + uncompressed_len) / 2
897 elif query == 'L':
898 content_length = compressed_len + uncompressed_len
900 self.send_response(200)
901 self.send_header('Content-Type', 'application/msword')
902 self.send_header('Content-encoding', 'deflate')
903 self.send_header('Connection', 'close')
904 self.send_header('Content-Length', content_length)
905 self.send_header('ETag', '\'' + file_path + '\'')
906 self.end_headers()
908 self.wfile.write(data)
910 return True
912 def FileHandler(self):
913 """This handler sends the contents of the requested file. Wow, it's like
914 a real webserver!"""
916 prefix = self.server.file_root_url
917 if not self.path.startswith(prefix):
918 return False
919 return self._FileHandlerHelper(prefix)
921 def PostOnlyFileHandler(self):
922 """This handler sends the contents of the requested file on a POST."""
924 prefix = urlparse.urljoin(self.server.file_root_url, 'post/')
925 if not self.path.startswith(prefix):
926 return False
927 return self._FileHandlerHelper(prefix)
929 def _FileHandlerHelper(self, prefix):
930 request_body = ''
931 if self.command == 'POST' or self.command == 'PUT':
932 # Consume a request body if present.
933 request_body = self.ReadRequestBody()
935 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
936 query_dict = cgi.parse_qs(query)
938 expected_body = query_dict.get('expected_body', [])
939 if expected_body and request_body not in expected_body:
940 self.send_response(404)
941 self.end_headers()
942 self.wfile.write('')
943 return True
945 expected_headers = query_dict.get('expected_headers', [])
946 for expected_header in expected_headers:
947 header_name, expected_value = expected_header.split(':')
948 if self.headers.getheader(header_name) != expected_value:
949 self.send_response(404)
950 self.end_headers()
951 self.wfile.write('')
952 return True
954 sub_path = url_path[len(prefix):]
955 entries = sub_path.split('/')
956 file_path = os.path.join(self.server.data_dir, *entries)
957 if os.path.isdir(file_path):
958 file_path = os.path.join(file_path, 'index.html')
960 if not os.path.isfile(file_path):
961 print "File not found " + sub_path + " full path:" + file_path
962 self.send_error(404)
963 return True
965 f = open(file_path, "rb")
966 data = f.read()
967 f.close()
969 data = self._ReplaceFileData(data, query)
971 old_protocol_version = self.protocol_version
973 # If file.mock-http-headers exists, it contains the headers we
974 # should send. Read them in and parse them.
975 headers_path = file_path + '.mock-http-headers'
976 if os.path.isfile(headers_path):
977 f = open(headers_path, "r")
979 # "HTTP/1.1 200 OK"
980 response = f.readline()
981 http_major, http_minor, status_code = re.findall(
982 'HTTP/(\d+).(\d+) (\d+)', response)[0]
983 self.protocol_version = "HTTP/%s.%s" % (http_major, http_minor)
984 self.send_response(int(status_code))
986 for line in f:
987 header_values = re.findall('(\S+):\s*(.*)', line)
988 if len(header_values) > 0:
989 # "name: value"
990 name, value = header_values[0]
991 self.send_header(name, value)
992 f.close()
993 else:
994 # Could be more generic once we support mime-type sniffing, but for
995 # now we need to set it explicitly.
997 range_header = self.headers.get('Range')
998 if range_header and range_header.startswith('bytes='):
999 # Note this doesn't handle all valid byte range_header values (i.e.
1000 # left open ended ones), just enough for what we needed so far.
1001 range_header = range_header[6:].split('-')
1002 start = int(range_header[0])
1003 if range_header[1]:
1004 end = int(range_header[1])
1005 else:
1006 end = len(data) - 1
1008 self.send_response(206)
1009 content_range = ('bytes ' + str(start) + '-' + str(end) + '/' +
1010 str(len(data)))
1011 self.send_header('Content-Range', content_range)
1012 data = data[start: end + 1]
1013 else:
1014 self.send_response(200)
1016 self.send_header('Content-Type', self.GetMIMETypeFromName(file_path))
1017 self.send_header('Accept-Ranges', 'bytes')
1018 self.send_header('Content-Length', len(data))
1019 self.send_header('ETag', '\'' + file_path + '\'')
1020 self.end_headers()
1022 if (self.command != 'HEAD'):
1023 self.wfile.write(data)
1025 self.protocol_version = old_protocol_version
1026 return True
1028 def SetCookieHandler(self):
1029 """This handler just sets a cookie, for testing cookie handling."""
1031 if not self._ShouldHandleRequest("/set-cookie"):
1032 return False
1034 query_char = self.path.find('?')
1035 if query_char != -1:
1036 cookie_values = self.path[query_char + 1:].split('&')
1037 else:
1038 cookie_values = ("",)
1039 self.send_response(200)
1040 self.send_header('Content-Type', 'text/html')
1041 for cookie_value in cookie_values:
1042 self.send_header('Set-Cookie', '%s' % cookie_value)
1043 self.end_headers()
1044 for cookie_value in cookie_values:
1045 self.wfile.write('%s' % cookie_value)
1046 return True
1048 def SetManyCookiesHandler(self):
1049 """This handler just sets a given number of cookies, for testing handling
1050 of large numbers of cookies."""
1052 if not self._ShouldHandleRequest("/set-many-cookies"):
1053 return False
1055 query_char = self.path.find('?')
1056 if query_char != -1:
1057 num_cookies = int(self.path[query_char + 1:])
1058 else:
1059 num_cookies = 0
1060 self.send_response(200)
1061 self.send_header('', 'text/html')
1062 for _i in range(0, num_cookies):
1063 self.send_header('Set-Cookie', 'a=')
1064 self.end_headers()
1065 self.wfile.write('%d cookies were sent' % num_cookies)
1066 return True
1068 def ExpectAndSetCookieHandler(self):
1069 """Expects some cookies to be sent, and if they are, sets more cookies.
1071 The expect parameter specifies a required cookie. May be specified multiple
1072 times.
1073 The set parameter specifies a cookie to set if all required cookies are
1074 preset. May be specified multiple times.
1075 The data parameter specifies the response body data to be returned."""
1077 if not self._ShouldHandleRequest("/expect-and-set-cookie"):
1078 return False
1080 _, _, _, _, query, _ = urlparse.urlparse(self.path)
1081 query_dict = cgi.parse_qs(query)
1082 cookies = set()
1083 if 'Cookie' in self.headers:
1084 cookie_header = self.headers.getheader('Cookie')
1085 cookies.update([s.strip() for s in cookie_header.split(';')])
1086 got_all_expected_cookies = True
1087 for expected_cookie in query_dict.get('expect', []):
1088 if expected_cookie not in cookies:
1089 got_all_expected_cookies = False
1090 self.send_response(200)
1091 self.send_header('Content-Type', 'text/html')
1092 if got_all_expected_cookies:
1093 for cookie_value in query_dict.get('set', []):
1094 self.send_header('Set-Cookie', '%s' % cookie_value)
1095 self.end_headers()
1096 for data_value in query_dict.get('data', []):
1097 self.wfile.write(data_value)
1098 return True
1100 def SetHeaderHandler(self):
1101 """This handler sets a response header. Parameters are in the
1102 key%3A%20value&key2%3A%20value2 format."""
1104 if not self._ShouldHandleRequest("/set-header"):
1105 return False
1107 query_char = self.path.find('?')
1108 if query_char != -1:
1109 headers_values = self.path[query_char + 1:].split('&')
1110 else:
1111 headers_values = ("",)
1112 self.send_response(200)
1113 self.send_header('Content-Type', 'text/html')
1114 for header_value in headers_values:
1115 header_value = urllib.unquote(header_value)
1116 (key, value) = header_value.split(': ', 1)
1117 self.send_header(key, value)
1118 self.end_headers()
1119 for header_value in headers_values:
1120 self.wfile.write('%s' % header_value)
1121 return True
1123 def AuthBasicHandler(self):
1124 """This handler tests 'Basic' authentication. It just sends a page with
1125 title 'user/pass' if you succeed."""
1127 if not self._ShouldHandleRequest("/auth-basic"):
1128 return False
1130 username = userpass = password = b64str = ""
1131 expected_password = 'secret'
1132 realm = 'testrealm'
1133 set_cookie_if_challenged = False
1135 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1136 query_params = cgi.parse_qs(query, True)
1137 if 'set-cookie-if-challenged' in query_params:
1138 set_cookie_if_challenged = True
1139 if 'password' in query_params:
1140 expected_password = query_params['password'][0]
1141 if 'realm' in query_params:
1142 realm = query_params['realm'][0]
1144 auth = self.headers.getheader('authorization')
1145 try:
1146 if not auth:
1147 raise Exception('no auth')
1148 b64str = re.findall(r'Basic (\S+)', auth)[0]
1149 userpass = base64.b64decode(b64str)
1150 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0]
1151 if password != expected_password:
1152 raise Exception('wrong password')
1153 except Exception, e:
1154 # Authentication failed.
1155 self.send_response(401)
1156 self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
1157 self.send_header('Content-Type', 'text/html')
1158 if set_cookie_if_challenged:
1159 self.send_header('Set-Cookie', 'got_challenged=true')
1160 self.end_headers()
1161 self.wfile.write('<html><head>')
1162 self.wfile.write('<title>Denied: %s</title>' % e)
1163 self.wfile.write('</head><body>')
1164 self.wfile.write('auth=%s<p>' % auth)
1165 self.wfile.write('b64str=%s<p>' % b64str)
1166 self.wfile.write('username: %s<p>' % username)
1167 self.wfile.write('userpass: %s<p>' % userpass)
1168 self.wfile.write('password: %s<p>' % password)
1169 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1170 self.wfile.write('</body></html>')
1171 return True
1173 # Authentication successful. (Return a cachable response to allow for
1174 # testing cached pages that require authentication.)
1175 old_protocol_version = self.protocol_version
1176 self.protocol_version = "HTTP/1.1"
1178 if_none_match = self.headers.getheader('if-none-match')
1179 if if_none_match == "abc":
1180 self.send_response(304)
1181 self.end_headers()
1182 elif url_path.endswith(".gif"):
1183 # Using chrome/test/data/google/logo.gif as the test image
1184 test_image_path = ['google', 'logo.gif']
1185 gif_path = os.path.join(self.server.data_dir, *test_image_path)
1186 if not os.path.isfile(gif_path):
1187 self.send_error(404)
1188 self.protocol_version = old_protocol_version
1189 return True
1191 f = open(gif_path, "rb")
1192 data = f.read()
1193 f.close()
1195 self.send_response(200)
1196 self.send_header('Content-Type', 'image/gif')
1197 self.send_header('Cache-control', 'max-age=60000')
1198 self.send_header('Etag', 'abc')
1199 self.end_headers()
1200 self.wfile.write(data)
1201 else:
1202 self.send_response(200)
1203 self.send_header('Content-Type', 'text/html')
1204 self.send_header('Cache-control', 'max-age=60000')
1205 self.send_header('Etag', 'abc')
1206 self.end_headers()
1207 self.wfile.write('<html><head>')
1208 self.wfile.write('<title>%s/%s</title>' % (username, password))
1209 self.wfile.write('</head><body>')
1210 self.wfile.write('auth=%s<p>' % auth)
1211 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1212 self.wfile.write('</body></html>')
1214 self.protocol_version = old_protocol_version
1215 return True
1217 def GetNonce(self, force_reset=False):
1218 """Returns a nonce that's stable per request path for the server's lifetime.
1219 This is a fake implementation. A real implementation would only use a given
1220 nonce a single time (hence the name n-once). However, for the purposes of
1221 unittesting, we don't care about the security of the nonce.
1223 Args:
1224 force_reset: Iff set, the nonce will be changed. Useful for testing the
1225 "stale" response.
1228 if force_reset or not self.server.nonce_time:
1229 self.server.nonce_time = time.time()
1230 return hashlib.md5('privatekey%s%d' %
1231 (self.path, self.server.nonce_time)).hexdigest()
1233 def AuthDigestHandler(self):
1234 """This handler tests 'Digest' authentication.
1236 It just sends a page with title 'user/pass' if you succeed.
1238 A stale response is sent iff "stale" is present in the request path.
1241 if not self._ShouldHandleRequest("/auth-digest"):
1242 return False
1244 stale = 'stale' in self.path
1245 nonce = self.GetNonce(force_reset=stale)
1246 opaque = hashlib.md5('opaque').hexdigest()
1247 password = 'secret'
1248 realm = 'testrealm'
1250 auth = self.headers.getheader('authorization')
1251 pairs = {}
1252 try:
1253 if not auth:
1254 raise Exception('no auth')
1255 if not auth.startswith('Digest'):
1256 raise Exception('not digest')
1257 # Pull out all the name="value" pairs as a dictionary.
1258 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth))
1260 # Make sure it's all valid.
1261 if pairs['nonce'] != nonce:
1262 raise Exception('wrong nonce')
1263 if pairs['opaque'] != opaque:
1264 raise Exception('wrong opaque')
1266 # Check the 'response' value and make sure it matches our magic hash.
1267 # See http://www.ietf.org/rfc/rfc2617.txt
1268 hash_a1 = hashlib.md5(
1269 ':'.join([pairs['username'], realm, password])).hexdigest()
1270 hash_a2 = hashlib.md5(':'.join([self.command, pairs['uri']])).hexdigest()
1271 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs:
1272 response = hashlib.md5(':'.join([hash_a1, nonce, pairs['nc'],
1273 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest()
1274 else:
1275 response = hashlib.md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest()
1277 if pairs['response'] != response:
1278 raise Exception('wrong password')
1279 except Exception, e:
1280 # Authentication failed.
1281 self.send_response(401)
1282 hdr = ('Digest '
1283 'realm="%s", '
1284 'domain="/", '
1285 'qop="auth", '
1286 'algorithm=MD5, '
1287 'nonce="%s", '
1288 'opaque="%s"') % (realm, nonce, opaque)
1289 if stale:
1290 hdr += ', stale="TRUE"'
1291 self.send_header('WWW-Authenticate', hdr)
1292 self.send_header('Content-Type', 'text/html')
1293 self.end_headers()
1294 self.wfile.write('<html><head>')
1295 self.wfile.write('<title>Denied: %s</title>' % e)
1296 self.wfile.write('</head><body>')
1297 self.wfile.write('auth=%s<p>' % auth)
1298 self.wfile.write('pairs=%s<p>' % pairs)
1299 self.wfile.write('You sent:<br>%s<p>' % self.headers)
1300 self.wfile.write('We are replying:<br>%s<p>' % hdr)
1301 self.wfile.write('</body></html>')
1302 return True
1304 # Authentication successful.
1305 self.send_response(200)
1306 self.send_header('Content-Type', 'text/html')
1307 self.end_headers()
1308 self.wfile.write('<html><head>')
1309 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password))
1310 self.wfile.write('</head><body>')
1311 self.wfile.write('auth=%s<p>' % auth)
1312 self.wfile.write('pairs=%s<p>' % pairs)
1313 self.wfile.write('</body></html>')
1315 return True
1317 def SlowServerHandler(self):
1318 """Wait for the user suggested time before responding. The syntax is
1319 /slow?0.5 to wait for half a second."""
1321 if not self._ShouldHandleRequest("/slow"):
1322 return False
1323 query_char = self.path.find('?')
1324 wait_sec = 1.0
1325 if query_char >= 0:
1326 try:
1327 wait_sec = float(self.path[query_char + 1:])
1328 except ValueError:
1329 pass
1330 time.sleep(wait_sec)
1331 self.send_response(200)
1332 self.send_header('Content-Type', 'text/plain')
1333 self.end_headers()
1334 self.wfile.write("waited %.1f seconds" % wait_sec)
1335 return True
1337 def ChunkedServerHandler(self):
1338 """Send chunked response. Allows to specify chunks parameters:
1339 - waitBeforeHeaders - ms to wait before sending headers
1340 - waitBetweenChunks - ms to wait between chunks
1341 - chunkSize - size of each chunk in bytes
1342 - chunksNumber - number of chunks
1343 Example: /chunked?waitBeforeHeaders=1000&chunkSize=5&chunksNumber=5
1344 waits one second, then sends headers and five chunks five bytes each."""
1346 if not self._ShouldHandleRequest("/chunked"):
1347 return False
1348 query_char = self.path.find('?')
1349 chunkedSettings = {'waitBeforeHeaders' : 0,
1350 'waitBetweenChunks' : 0,
1351 'chunkSize' : 5,
1352 'chunksNumber' : 5}
1353 if query_char >= 0:
1354 params = self.path[query_char + 1:].split('&')
1355 for param in params:
1356 keyValue = param.split('=')
1357 if len(keyValue) == 2:
1358 try:
1359 chunkedSettings[keyValue[0]] = int(keyValue[1])
1360 except ValueError:
1361 pass
1362 time.sleep(0.001 * chunkedSettings['waitBeforeHeaders'])
1363 self.protocol_version = 'HTTP/1.1' # Needed for chunked encoding
1364 self.send_response(200)
1365 self.send_header('Content-Type', 'text/plain')
1366 self.send_header('Connection', 'close')
1367 self.send_header('Transfer-Encoding', 'chunked')
1368 self.end_headers()
1369 # Chunked encoding: sending all chunks, then final zero-length chunk and
1370 # then final CRLF.
1371 for i in range(0, chunkedSettings['chunksNumber']):
1372 if i > 0:
1373 time.sleep(0.001 * chunkedSettings['waitBetweenChunks'])
1374 self.sendChunkHelp('*' * chunkedSettings['chunkSize'])
1375 self.wfile.flush() # Keep in mind that we start flushing only after 1kb.
1376 self.sendChunkHelp('')
1377 return True
1379 def NoContentHandler(self):
1380 """Returns a 204 No Content response."""
1382 if not self._ShouldHandleRequest("/nocontent"):
1383 return False
1384 self.send_response(204)
1385 self.end_headers()
1386 return True
1388 def ServerRedirectHandler(self):
1389 """Sends a server redirect to the given URL. The syntax is
1390 '/server-redirect?http://foo.bar/asdf' to redirect to
1391 'http://foo.bar/asdf'"""
1393 test_name = "/server-redirect"
1394 if not self._ShouldHandleRequest(test_name):
1395 return False
1397 query_char = self.path.find('?')
1398 if query_char < 0 or len(self.path) <= query_char + 1:
1399 self.sendRedirectHelp(test_name)
1400 return True
1401 dest = urllib.unquote(self.path[query_char + 1:])
1403 self.send_response(301) # moved permanently
1404 self.send_header('Location', dest)
1405 self.send_header('Content-Type', 'text/html')
1406 self.end_headers()
1407 self.wfile.write('<html><head>')
1408 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1410 return True
1412 def CrossSiteRedirectHandler(self):
1413 """Sends a server redirect to the given site. The syntax is
1414 '/cross-site/hostname/...' to redirect to //hostname/...
1415 It is used to navigate between different Sites, causing
1416 cross-site/cross-process navigations in the browser."""
1418 test_name = "/cross-site"
1419 if not self._ShouldHandleRequest(test_name):
1420 return False
1422 params = urllib.unquote(self.path[(len(test_name) + 1):])
1423 slash = params.find('/')
1424 if slash < 0:
1425 self.sendRedirectHelp(test_name)
1426 return True
1428 host = params[:slash]
1429 path = params[(slash+1):]
1430 dest = "//%s:%s/%s" % (host, str(self.server.server_port), path)
1432 self.send_response(301) # moved permanently
1433 self.send_header('Location', dest)
1434 self.send_header('Content-Type', 'text/html')
1435 self.end_headers()
1436 self.wfile.write('<html><head>')
1437 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1439 return True
1441 def ClientRedirectHandler(self):
1442 """Sends a client redirect to the given URL. The syntax is
1443 '/client-redirect?http://foo.bar/asdf' to redirect to
1444 'http://foo.bar/asdf'"""
1446 test_name = "/client-redirect"
1447 if not self._ShouldHandleRequest(test_name):
1448 return False
1450 query_char = self.path.find('?')
1451 if query_char < 0 or len(self.path) <= query_char + 1:
1452 self.sendRedirectHelp(test_name)
1453 return True
1454 dest = urllib.unquote(self.path[query_char + 1:])
1456 self.send_response(200)
1457 self.send_header('Content-Type', 'text/html')
1458 self.end_headers()
1459 self.wfile.write('<html><head>')
1460 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1461 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1463 return True
1465 def GetSSLSessionCacheHandler(self):
1466 """Send a reply containing a log of the session cache operations."""
1468 if not self._ShouldHandleRequest('/ssl-session-cache'):
1469 return False
1471 self.send_response(200)
1472 self.send_header('Content-Type', 'text/plain')
1473 self.end_headers()
1474 try:
1475 log = self.server.session_cache.log
1476 except AttributeError:
1477 self.wfile.write('Pass --https-record-resume in order to use' +
1478 ' this request')
1479 return True
1481 for (action, sessionID) in log:
1482 self.wfile.write('%s\t%s\n' % (action, bytes(sessionID).encode('hex')))
1483 return True
1485 def SSLManySmallRecords(self):
1486 """Sends a reply consisting of a variety of small writes. These will be
1487 translated into a series of small SSL records when used over an HTTPS
1488 server."""
1490 if not self._ShouldHandleRequest('/ssl-many-small-records'):
1491 return False
1493 self.send_response(200)
1494 self.send_header('Content-Type', 'text/plain')
1495 self.end_headers()
1497 # Write ~26K of data, in 1350 byte chunks
1498 for i in xrange(20):
1499 self.wfile.write('*' * 1350)
1500 self.wfile.flush()
1501 return True
1503 def GetChannelID(self):
1504 """Send a reply containing the hashed ChannelID that the client provided."""
1506 if not self._ShouldHandleRequest('/channel-id'):
1507 return False
1509 self.send_response(200)
1510 self.send_header('Content-Type', 'text/plain')
1511 self.end_headers()
1512 channel_id = bytes(self.server.tlsConnection.channel_id)
1513 self.wfile.write(hashlib.sha256(channel_id).digest().encode('base64'))
1514 return True
1516 def GetClientCert(self):
1517 """Send a reply whether a client certificate was provided."""
1519 if not self._ShouldHandleRequest('/client-cert'):
1520 return False
1522 self.send_response(200)
1523 self.send_header('Content-Type', 'text/plain')
1524 self.end_headers()
1526 cert_chain = self.server.tlsConnection.session.clientCertChain
1527 if cert_chain != None:
1528 self.wfile.write('got client cert with fingerprint: ' +
1529 cert_chain.getFingerprint())
1530 else:
1531 self.wfile.write('got no client cert')
1532 return True
1534 def ClientCipherListHandler(self):
1535 """Send a reply containing the cipher suite list that the client
1536 provided. Each cipher suite value is serialized in decimal, followed by a
1537 newline."""
1539 if not self._ShouldHandleRequest('/client-cipher-list'):
1540 return False
1542 self.send_response(200)
1543 self.send_header('Content-Type', 'text/plain')
1544 self.end_headers()
1546 cipher_suites = self.server.tlsConnection.clientHello.cipher_suites
1547 self.wfile.write('\n'.join(str(c) for c in cipher_suites))
1548 return True
1550 def CloseSocketHandler(self):
1551 """Closes the socket without sending anything."""
1553 if not self._ShouldHandleRequest('/close-socket'):
1554 return False
1556 self.wfile.close()
1557 return True
1559 def RangeResetHandler(self):
1560 """Send data broken up by connection resets every N (default 4K) bytes.
1561 Support range requests. If the data requested doesn't straddle a reset
1562 boundary, it will all be sent. Used for testing resuming downloads."""
1564 def DataForRange(start, end):
1565 """Data to be provided for a particular range of bytes."""
1566 # Offset and scale to avoid too obvious (and hence potentially
1567 # collidable) data.
1568 return ''.join([chr(y % 256)
1569 for y in range(start * 2 + 15, end * 2 + 15, 2)])
1571 if not self._ShouldHandleRequest('/rangereset'):
1572 return False
1574 # HTTP/1.1 is required for ETag and range support.
1575 self.protocol_version = 'HTTP/1.1'
1576 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1578 # Defaults
1579 size = 8000
1580 # Note that the rst is sent just before sending the rst_boundary byte.
1581 rst_boundary = 4000
1582 respond_to_range = True
1583 hold_for_signal = False
1584 rst_limit = -1
1585 token = 'DEFAULT'
1586 fail_precondition = 0
1587 send_verifiers = True
1589 # Parse the query
1590 qdict = urlparse.parse_qs(query, True)
1591 if 'size' in qdict:
1592 size = int(qdict['size'][0])
1593 if 'rst_boundary' in qdict:
1594 rst_boundary = int(qdict['rst_boundary'][0])
1595 if 'token' in qdict:
1596 # Identifying token for stateful tests.
1597 token = qdict['token'][0]
1598 if 'rst_limit' in qdict:
1599 # Max number of rsts for a given token.
1600 rst_limit = int(qdict['rst_limit'][0])
1601 if 'bounce_range' in qdict:
1602 respond_to_range = False
1603 if 'hold' in qdict:
1604 # Note that hold_for_signal will not work with null range requests;
1605 # see TODO below.
1606 hold_for_signal = True
1607 if 'no_verifiers' in qdict:
1608 send_verifiers = False
1609 if 'fail_precondition' in qdict:
1610 fail_precondition = int(qdict['fail_precondition'][0])
1612 # Record already set information, or set it.
1613 rst_limit = TestPageHandler.rst_limits.setdefault(token, rst_limit)
1614 if rst_limit != 0:
1615 TestPageHandler.rst_limits[token] -= 1
1616 fail_precondition = TestPageHandler.fail_precondition.setdefault(
1617 token, fail_precondition)
1618 if fail_precondition != 0:
1619 TestPageHandler.fail_precondition[token] -= 1
1621 first_byte = 0
1622 last_byte = size - 1
1624 # Does that define what we want to return, or do we need to apply
1625 # a range?
1626 range_response = False
1627 range_header = self.headers.getheader('range')
1628 if range_header and respond_to_range:
1629 mo = re.match("bytes=(\d*)-(\d*)", range_header)
1630 if mo.group(1):
1631 first_byte = int(mo.group(1))
1632 if mo.group(2):
1633 last_byte = int(mo.group(2))
1634 if last_byte > size - 1:
1635 last_byte = size - 1
1636 range_response = True
1637 if last_byte < first_byte:
1638 return False
1640 if (fail_precondition and
1641 (self.headers.getheader('If-Modified-Since') or
1642 self.headers.getheader('If-Match'))):
1643 self.send_response(412)
1644 self.end_headers()
1645 return True
1647 if range_response:
1648 self.send_response(206)
1649 self.send_header('Content-Range',
1650 'bytes %d-%d/%d' % (first_byte, last_byte, size))
1651 else:
1652 self.send_response(200)
1653 self.send_header('Content-Type', 'application/octet-stream')
1654 self.send_header('Content-Length', last_byte - first_byte + 1)
1655 if send_verifiers:
1656 # If fail_precondition is non-zero, then the ETag for each request will be
1657 # different.
1658 etag = "%s%d" % (token, fail_precondition)
1659 self.send_header('ETag', etag)
1660 self.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST')
1661 self.end_headers()
1663 if hold_for_signal:
1664 # TODO(rdsmith/phajdan.jr): http://crbug.com/169519: Without writing
1665 # a single byte, the self.server.handle_request() below hangs
1666 # without processing new incoming requests.
1667 self.wfile.write(DataForRange(first_byte, first_byte + 1))
1668 first_byte = first_byte + 1
1669 # handle requests until one of them clears this flag.
1670 self.server.wait_for_download = True
1671 while self.server.wait_for_download:
1672 self.server.handle_request()
1674 possible_rst = ((first_byte / rst_boundary) + 1) * rst_boundary
1675 if possible_rst >= last_byte or rst_limit == 0:
1676 # No RST has been requested in this range, so we don't need to
1677 # do anything fancy; just write the data and let the python
1678 # infrastructure close the connection.
1679 self.wfile.write(DataForRange(first_byte, last_byte + 1))
1680 self.wfile.flush()
1681 return True
1683 # We're resetting the connection part way in; go to the RST
1684 # boundary and then send an RST.
1685 # Because socket semantics do not guarantee that all the data will be
1686 # sent when using the linger semantics to hard close a socket,
1687 # we send the data and then wait for our peer to release us
1688 # before sending the reset.
1689 data = DataForRange(first_byte, possible_rst)
1690 self.wfile.write(data)
1691 self.wfile.flush()
1692 self.server.wait_for_download = True
1693 while self.server.wait_for_download:
1694 self.server.handle_request()
1695 l_onoff = 1 # Linger is active.
1696 l_linger = 0 # Seconds to linger for.
1697 self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
1698 struct.pack('ii', l_onoff, l_linger))
1700 # Close all duplicates of the underlying socket to force the RST.
1701 self.wfile.close()
1702 self.rfile.close()
1703 self.connection.close()
1705 return True
1707 def DefaultResponseHandler(self):
1708 """This is the catch-all response handler for requests that aren't handled
1709 by one of the special handlers above.
1710 Note that we specify the content-length as without it the https connection
1711 is not closed properly (and the browser keeps expecting data)."""
1713 contents = "Default response given for path: " + self.path
1714 self.send_response(200)
1715 self.send_header('Content-Type', 'text/html')
1716 self.send_header('Content-Length', len(contents))
1717 self.end_headers()
1718 if (self.command != 'HEAD'):
1719 self.wfile.write(contents)
1720 return True
1722 def RedirectConnectHandler(self):
1723 """Sends a redirect to the CONNECT request for www.redirect.com. This
1724 response is not specified by the RFC, so the browser should not follow
1725 the redirect."""
1727 if (self.path.find("www.redirect.com") < 0):
1728 return False
1730 dest = "http://www.destination.com/foo.js"
1732 self.send_response(302) # moved temporarily
1733 self.send_header('Location', dest)
1734 self.send_header('Connection', 'close')
1735 self.end_headers()
1736 return True
1738 def ServerAuthConnectHandler(self):
1739 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1740 response doesn't make sense because the proxy server cannot request
1741 server authentication."""
1743 if (self.path.find("www.server-auth.com") < 0):
1744 return False
1746 challenge = 'Basic realm="WallyWorld"'
1748 self.send_response(401) # unauthorized
1749 self.send_header('WWW-Authenticate', challenge)
1750 self.send_header('Connection', 'close')
1751 self.end_headers()
1752 return True
1754 def DefaultConnectResponseHandler(self):
1755 """This is the catch-all response handler for CONNECT requests that aren't
1756 handled by one of the special handlers above. Real Web servers respond
1757 with 400 to CONNECT requests."""
1759 contents = "Your client has issued a malformed or illegal request."
1760 self.send_response(400) # bad request
1761 self.send_header('Content-Type', 'text/html')
1762 self.send_header('Content-Length', len(contents))
1763 self.end_headers()
1764 self.wfile.write(contents)
1765 return True
1767 # called by the redirect handling function when there is no parameter
1768 def sendRedirectHelp(self, redirect_name):
1769 self.send_response(200)
1770 self.send_header('Content-Type', 'text/html')
1771 self.end_headers()
1772 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1773 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1774 self.wfile.write('</body></html>')
1776 # called by chunked handling function
1777 def sendChunkHelp(self, chunk):
1778 # Each chunk consists of: chunk size (hex), CRLF, chunk body, CRLF
1779 self.wfile.write('%X\r\n' % len(chunk))
1780 self.wfile.write(chunk)
1781 self.wfile.write('\r\n')
1784 class OCSPHandler(testserver_base.BasePageHandler):
1785 def __init__(self, request, client_address, socket_server):
1786 handlers = [self.OCSPResponse]
1787 self.ocsp_response = socket_server.ocsp_response
1788 testserver_base.BasePageHandler.__init__(self, request, client_address,
1789 socket_server, [], handlers, [],
1790 handlers, [])
1792 def OCSPResponse(self):
1793 self.send_response(200)
1794 self.send_header('Content-Type', 'application/ocsp-response')
1795 self.send_header('Content-Length', str(len(self.ocsp_response)))
1796 self.end_headers()
1798 self.wfile.write(self.ocsp_response)
1801 class TCPEchoHandler(SocketServer.BaseRequestHandler):
1802 """The RequestHandler class for TCP echo server.
1804 It is instantiated once per connection to the server, and overrides the
1805 handle() method to implement communication to the client.
1808 def handle(self):
1809 """Handles the request from the client and constructs a response."""
1811 data = self.request.recv(65536).strip()
1812 # Verify the "echo request" message received from the client. Send back
1813 # "echo response" message if "echo request" message is valid.
1814 try:
1815 return_data = echo_message.GetEchoResponseData(data)
1816 if not return_data:
1817 return
1818 except ValueError:
1819 return
1821 self.request.send(return_data)
1824 class UDPEchoHandler(SocketServer.BaseRequestHandler):
1825 """The RequestHandler class for UDP echo server.
1827 It is instantiated once per connection to the server, and overrides the
1828 handle() method to implement communication to the client.
1831 def handle(self):
1832 """Handles the request from the client and constructs a response."""
1834 data = self.request[0].strip()
1835 request_socket = self.request[1]
1836 # Verify the "echo request" message received from the client. Send back
1837 # "echo response" message if "echo request" message is valid.
1838 try:
1839 return_data = echo_message.GetEchoResponseData(data)
1840 if not return_data:
1841 return
1842 except ValueError:
1843 return
1844 request_socket.sendto(return_data, self.client_address)
1847 class BasicAuthProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
1848 """A request handler that behaves as a proxy server which requires
1849 basic authentication. Only CONNECT, GET and HEAD is supported for now.
1852 _AUTH_CREDENTIAL = 'Basic Zm9vOmJhcg==' # foo:bar
1854 def parse_request(self):
1855 """Overrides parse_request to check credential."""
1857 if not BaseHTTPServer.BaseHTTPRequestHandler.parse_request(self):
1858 return False
1860 auth = self.headers.getheader('Proxy-Authorization')
1861 if auth != self._AUTH_CREDENTIAL:
1862 self.send_response(407)
1863 self.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"')
1864 self.end_headers()
1865 return False
1867 return True
1869 def _start_read_write(self, sock):
1870 sock.setblocking(0)
1871 self.request.setblocking(0)
1872 rlist = [self.request, sock]
1873 while True:
1874 ready_sockets, _unused, errors = select.select(rlist, [], [])
1875 if errors:
1876 self.send_response(500)
1877 self.end_headers()
1878 return
1879 for s in ready_sockets:
1880 received = s.recv(1024)
1881 if len(received) == 0:
1882 return
1883 if s == self.request:
1884 other = sock
1885 else:
1886 other = self.request
1887 other.send(received)
1889 def _do_common_method(self):
1890 url = urlparse.urlparse(self.path)
1891 port = url.port
1892 if not port:
1893 if url.scheme == 'http':
1894 port = 80
1895 elif url.scheme == 'https':
1896 port = 443
1897 if not url.hostname or not port:
1898 self.send_response(400)
1899 self.end_headers()
1900 return
1902 if len(url.path) == 0:
1903 path = '/'
1904 else:
1905 path = url.path
1906 if len(url.query) > 0:
1907 path = '%s?%s' % (url.path, url.query)
1909 sock = None
1910 try:
1911 sock = socket.create_connection((url.hostname, port))
1912 sock.send('%s %s %s\r\n' % (
1913 self.command, path, self.protocol_version))
1914 for header in self.headers.headers:
1915 header = header.strip()
1916 if (header.lower().startswith('connection') or
1917 header.lower().startswith('proxy')):
1918 continue
1919 sock.send('%s\r\n' % header)
1920 sock.send('\r\n')
1921 self._start_read_write(sock)
1922 except Exception:
1923 self.send_response(500)
1924 self.end_headers()
1925 finally:
1926 if sock is not None:
1927 sock.close()
1929 def do_CONNECT(self):
1930 try:
1931 pos = self.path.rfind(':')
1932 host = self.path[:pos]
1933 port = int(self.path[pos+1:])
1934 except Exception:
1935 self.send_response(400)
1936 self.end_headers()
1938 try:
1939 sock = socket.create_connection((host, port))
1940 self.send_response(200, 'Connection established')
1941 self.end_headers()
1942 self._start_read_write(sock)
1943 except Exception:
1944 self.send_response(500)
1945 self.end_headers()
1946 finally:
1947 sock.close()
1949 def do_GET(self):
1950 self._do_common_method()
1952 def do_HEAD(self):
1953 self._do_common_method()
1956 class ServerRunner(testserver_base.TestServerRunner):
1957 """TestServerRunner for the net test servers."""
1959 def __init__(self):
1960 super(ServerRunner, self).__init__()
1961 self.__ocsp_server = None
1963 def __make_data_dir(self):
1964 if self.options.data_dir:
1965 if not os.path.isdir(self.options.data_dir):
1966 raise testserver_base.OptionError('specified data dir not found: ' +
1967 self.options.data_dir + ' exiting...')
1968 my_data_dir = self.options.data_dir
1969 else:
1970 # Create the default path to our data dir, relative to the exe dir.
1971 my_data_dir = os.path.join(BASE_DIR, "..", "..", "..", "..",
1972 "test", "data")
1974 #TODO(ibrar): Must use Find* funtion defined in google\tools
1975 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1977 return my_data_dir
1979 def create_server(self, server_data):
1980 port = self.options.port
1981 host = self.options.host
1983 # Work around a bug in Mac OS 10.6. Spawning a WebSockets server
1984 # will result in a call to |getaddrinfo|, which fails with "nodename
1985 # nor servname provided" for localhost:0 on 10.6.
1986 if self.options.server_type == SERVER_WEBSOCKET and \
1987 host == "localhost" and \
1988 port == 0:
1989 host = "127.0.0.1"
1991 if self.options.server_type == SERVER_HTTP:
1992 if self.options.https:
1993 pem_cert_and_key = None
1994 ocsp_der = None
1995 if self.options.cert_and_key_file:
1996 if not os.path.isfile(self.options.cert_and_key_file):
1997 raise testserver_base.OptionError(
1998 'specified server cert file not found: ' +
1999 self.options.cert_and_key_file + ' exiting...')
2000 pem_cert_and_key = file(self.options.cert_and_key_file, 'r').read()
2001 else:
2002 # generate a new certificate and run an OCSP server for it.
2003 self.__ocsp_server = OCSPServer((host, 0), OCSPHandler)
2004 print ('OCSP server started on %s:%d...' %
2005 (host, self.__ocsp_server.server_port))
2007 ocsp_state = None
2009 if self.options.ocsp == 'ok':
2010 ocsp_state = minica.OCSP_STATE_GOOD
2011 elif self.options.ocsp == 'revoked':
2012 ocsp_state = minica.OCSP_STATE_REVOKED
2013 elif self.options.ocsp == 'invalid':
2014 ocsp_state = minica.OCSP_STATE_INVALID
2015 elif self.options.ocsp == 'unauthorized':
2016 ocsp_state = minica.OCSP_STATE_UNAUTHORIZED
2017 elif self.options.ocsp == 'unknown':
2018 ocsp_state = minica.OCSP_STATE_UNKNOWN
2019 else:
2020 raise testserver_base.OptionError('unknown OCSP status: ' +
2021 self.options.ocsp_status)
2023 (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP(
2024 subject = "127.0.0.1",
2025 ocsp_url = ("http://%s:%d/ocsp" %
2026 (host, self.__ocsp_server.server_port)),
2027 ocsp_state = ocsp_state,
2028 serial = self.options.cert_serial)
2030 if self.options.ocsp_server_unavailable:
2031 # SEQUENCE containing ENUMERATED with value 3 (tryLater).
2032 self.__ocsp_server.ocsp_response = '30030a0103'.decode('hex')
2033 else:
2034 self.__ocsp_server.ocsp_response = ocsp_der
2036 for ca_cert in self.options.ssl_client_ca:
2037 if not os.path.isfile(ca_cert):
2038 raise testserver_base.OptionError(
2039 'specified trusted client CA file not found: ' + ca_cert +
2040 ' exiting...')
2042 stapled_ocsp_response = None
2043 if self.options.staple_ocsp_response:
2044 stapled_ocsp_response = ocsp_der
2046 server = HTTPSServer((host, port), TestPageHandler, pem_cert_and_key,
2047 self.options.ssl_client_auth,
2048 self.options.ssl_client_ca,
2049 self.options.ssl_client_cert_type,
2050 self.options.ssl_bulk_cipher,
2051 self.options.ssl_key_exchange,
2052 self.options.enable_npn,
2053 self.options.record_resume,
2054 self.options.tls_intolerant,
2055 self.options.tls_intolerance_type,
2056 self.options.signed_cert_timestamps_tls_ext.decode(
2057 "base64"),
2058 self.options.fallback_scsv,
2059 stapled_ocsp_response,
2060 self.options.alert_after_handshake)
2061 print 'HTTPS server started on https://%s:%d...' % \
2062 (host, server.server_port)
2063 else:
2064 server = HTTPServer((host, port), TestPageHandler)
2065 print 'HTTP server started on http://%s:%d...' % \
2066 (host, server.server_port)
2068 server.data_dir = self.__make_data_dir()
2069 server.file_root_url = self.options.file_root_url
2070 server_data['port'] = server.server_port
2071 elif self.options.server_type == SERVER_WEBSOCKET:
2072 # Launch pywebsocket via WebSocketServer.
2073 logger = logging.getLogger()
2074 logger.addHandler(logging.StreamHandler())
2075 # TODO(toyoshim): Remove following os.chdir. Currently this operation
2076 # is required to work correctly. It should be fixed from pywebsocket side.
2077 os.chdir(self.__make_data_dir())
2078 websocket_options = WebSocketOptions(host, port, '.')
2079 scheme = "ws"
2080 if self.options.cert_and_key_file:
2081 scheme = "wss"
2082 websocket_options.use_tls = True
2083 websocket_options.private_key = self.options.cert_and_key_file
2084 websocket_options.certificate = self.options.cert_and_key_file
2085 if self.options.ssl_client_auth:
2086 websocket_options.tls_client_cert_optional = False
2087 websocket_options.tls_client_auth = True
2088 if len(self.options.ssl_client_ca) != 1:
2089 raise testserver_base.OptionError(
2090 'one trusted client CA file should be specified')
2091 if not os.path.isfile(self.options.ssl_client_ca[0]):
2092 raise testserver_base.OptionError(
2093 'specified trusted client CA file not found: ' +
2094 self.options.ssl_client_ca[0] + ' exiting...')
2095 websocket_options.tls_client_ca = self.options.ssl_client_ca[0]
2096 print 'Trying to start websocket server on %s://%s:%d...' % \
2097 (scheme, websocket_options.server_host, websocket_options.port)
2098 server = WebSocketServer(websocket_options)
2099 print 'WebSocket server started on %s://%s:%d...' % \
2100 (scheme, host, server.server_port)
2101 server_data['port'] = server.server_port
2102 websocket_options.use_basic_auth = self.options.ws_basic_auth
2103 elif self.options.server_type == SERVER_TCP_ECHO:
2104 # Used for generating the key (randomly) that encodes the "echo request"
2105 # message.
2106 random.seed()
2107 server = TCPEchoServer((host, port), TCPEchoHandler)
2108 print 'Echo TCP server started on port %d...' % server.server_port
2109 server_data['port'] = server.server_port
2110 elif self.options.server_type == SERVER_UDP_ECHO:
2111 # Used for generating the key (randomly) that encodes the "echo request"
2112 # message.
2113 random.seed()
2114 server = UDPEchoServer((host, port), UDPEchoHandler)
2115 print 'Echo UDP server started on port %d...' % server.server_port
2116 server_data['port'] = server.server_port
2117 elif self.options.server_type == SERVER_BASIC_AUTH_PROXY:
2118 server = HTTPServer((host, port), BasicAuthProxyRequestHandler)
2119 print 'BasicAuthProxy server started on port %d...' % server.server_port
2120 server_data['port'] = server.server_port
2121 elif self.options.server_type == SERVER_FTP:
2122 my_data_dir = self.__make_data_dir()
2124 # Instantiate a dummy authorizer for managing 'virtual' users
2125 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
2127 # Define a new user having full r/w permissions
2128 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
2130 # Define a read-only anonymous user unless disabled
2131 if not self.options.no_anonymous_ftp_user:
2132 authorizer.add_anonymous(my_data_dir)
2134 # Instantiate FTP handler class
2135 ftp_handler = pyftpdlib.ftpserver.FTPHandler
2136 ftp_handler.authorizer = authorizer
2138 # Define a customized banner (string returned when client connects)
2139 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
2140 pyftpdlib.ftpserver.__ver__)
2142 # Instantiate FTP server class and listen to address:port
2143 server = pyftpdlib.ftpserver.FTPServer((host, port), ftp_handler)
2144 server_data['port'] = server.socket.getsockname()[1]
2145 print 'FTP server started on port %d...' % server_data['port']
2146 else:
2147 raise testserver_base.OptionError('unknown server type' +
2148 self.options.server_type)
2150 return server
2152 def run_server(self):
2153 if self.__ocsp_server:
2154 self.__ocsp_server.serve_forever_on_thread()
2156 testserver_base.TestServerRunner.run_server(self)
2158 if self.__ocsp_server:
2159 self.__ocsp_server.stop_serving()
2161 def add_options(self):
2162 testserver_base.TestServerRunner.add_options(self)
2163 self.option_parser.add_option('-f', '--ftp', action='store_const',
2164 const=SERVER_FTP, default=SERVER_HTTP,
2165 dest='server_type',
2166 help='start up an FTP server.')
2167 self.option_parser.add_option('--tcp-echo', action='store_const',
2168 const=SERVER_TCP_ECHO, default=SERVER_HTTP,
2169 dest='server_type',
2170 help='start up a tcp echo server.')
2171 self.option_parser.add_option('--udp-echo', action='store_const',
2172 const=SERVER_UDP_ECHO, default=SERVER_HTTP,
2173 dest='server_type',
2174 help='start up a udp echo server.')
2175 self.option_parser.add_option('--basic-auth-proxy', action='store_const',
2176 const=SERVER_BASIC_AUTH_PROXY,
2177 default=SERVER_HTTP, dest='server_type',
2178 help='start up a proxy server which requires '
2179 'basic authentication.')
2180 self.option_parser.add_option('--websocket', action='store_const',
2181 const=SERVER_WEBSOCKET, default=SERVER_HTTP,
2182 dest='server_type',
2183 help='start up a WebSocket server.')
2184 self.option_parser.add_option('--https', action='store_true',
2185 dest='https', help='Specify that https '
2186 'should be used.')
2187 self.option_parser.add_option('--cert-and-key-file',
2188 dest='cert_and_key_file', help='specify the '
2189 'path to the file containing the certificate '
2190 'and private key for the server in PEM '
2191 'format')
2192 self.option_parser.add_option('--ocsp', dest='ocsp', default='ok',
2193 help='The type of OCSP response generated '
2194 'for the automatically generated '
2195 'certificate. One of [ok,revoked,invalid]')
2196 self.option_parser.add_option('--cert-serial', dest='cert_serial',
2197 default=0, type=int,
2198 help='If non-zero then the generated '
2199 'certificate will have this serial number')
2200 self.option_parser.add_option('--tls-intolerant', dest='tls_intolerant',
2201 default='0', type='int',
2202 help='If nonzero, certain TLS connections '
2203 'will be aborted in order to test version '
2204 'fallback. 1 means all TLS versions will be '
2205 'aborted. 2 means TLS 1.1 or higher will be '
2206 'aborted. 3 means TLS 1.2 or higher will be '
2207 'aborted.')
2208 self.option_parser.add_option('--tls-intolerance-type',
2209 dest='tls_intolerance_type',
2210 default="alert",
2211 help='Controls how the server reacts to a '
2212 'TLS version it is intolerant to. Valid '
2213 'values are "alert", "close", and "reset".')
2214 self.option_parser.add_option('--signed-cert-timestamps-tls-ext',
2215 dest='signed_cert_timestamps_tls_ext',
2216 default='',
2217 help='Base64 encoded SCT list. If set, '
2218 'server will respond with a '
2219 'signed_certificate_timestamp TLS extension '
2220 'whenever the client supports it.')
2221 self.option_parser.add_option('--fallback-scsv', dest='fallback_scsv',
2222 default=False, const=True,
2223 action='store_const',
2224 help='If given, TLS_FALLBACK_SCSV support '
2225 'will be enabled. This causes the server to '
2226 'reject fallback connections from compatible '
2227 'clients (e.g. Chrome).')
2228 self.option_parser.add_option('--staple-ocsp-response',
2229 dest='staple_ocsp_response',
2230 default=False, action='store_true',
2231 help='If set, server will staple the OCSP '
2232 'response whenever OCSP is on and the client '
2233 'supports OCSP stapling.')
2234 self.option_parser.add_option('--https-record-resume',
2235 dest='record_resume', const=True,
2236 default=False, action='store_const',
2237 help='Record resumption cache events rather '
2238 'than resuming as normal. Allows the use of '
2239 'the /ssl-session-cache request')
2240 self.option_parser.add_option('--ssl-client-auth', action='store_true',
2241 help='Require SSL client auth on every '
2242 'connection.')
2243 self.option_parser.add_option('--ssl-client-ca', action='append',
2244 default=[], help='Specify that the client '
2245 'certificate request should include the CA '
2246 'named in the subject of the DER-encoded '
2247 'certificate contained in the specified '
2248 'file. This option may appear multiple '
2249 'times, indicating multiple CA names should '
2250 'be sent in the request.')
2251 self.option_parser.add_option('--ssl-client-cert-type', action='append',
2252 default=[], help='Specify that the client '
2253 'certificate request should include the '
2254 'specified certificate_type value. This '
2255 'option may appear multiple times, '
2256 'indicating multiple values should be send '
2257 'in the request. Valid values are '
2258 '"rsa_sign", "dss_sign", and "ecdsa_sign". '
2259 'If omitted, "rsa_sign" will be used.')
2260 self.option_parser.add_option('--ssl-bulk-cipher', action='append',
2261 help='Specify the bulk encryption '
2262 'algorithm(s) that will be accepted by the '
2263 'SSL server. Valid values are "aes128gcm", '
2264 '"aes256", "aes128", "3des", "rc4". If '
2265 'omitted, all algorithms will be used. This '
2266 'option may appear multiple times, '
2267 'indicating multiple algorithms should be '
2268 'enabled.');
2269 self.option_parser.add_option('--ssl-key-exchange', action='append',
2270 help='Specify the key exchange algorithm(s)'
2271 'that will be accepted by the SSL server. '
2272 'Valid values are "rsa", "dhe_rsa", '
2273 '"ecdhe_rsa". If omitted, all algorithms '
2274 'will be used. This option may appear '
2275 'multiple times, indicating multiple '
2276 'algorithms should be enabled.');
2277 # TODO(davidben): Add ALPN support to tlslite.
2278 self.option_parser.add_option('--enable-npn', dest='enable_npn',
2279 default=False, const=True,
2280 action='store_const',
2281 help='Enable server support for the NPN '
2282 'extension. The server will advertise '
2283 'support for exactly one protocol, http/1.1')
2284 self.option_parser.add_option('--file-root-url', default='/files/',
2285 help='Specify a root URL for files served.')
2286 # TODO(ricea): Generalize this to support basic auth for HTTP too.
2287 self.option_parser.add_option('--ws-basic-auth', action='store_true',
2288 dest='ws_basic_auth',
2289 help='Enable basic-auth for WebSocket')
2290 self.option_parser.add_option('--ocsp-server-unavailable',
2291 dest='ocsp_server_unavailable',
2292 default=False, action='store_true',
2293 help='If set, the OCSP server will return '
2294 'a tryLater status rather than the actual '
2295 'OCSP response.')
2296 self.option_parser.add_option('--alert-after-handshake',
2297 dest='alert_after_handshake',
2298 default=False, action='store_true',
2299 help='If set, the server will send a fatal '
2300 'alert immediately after the handshake.')
2301 self.option_parser.add_option('--no-anonymous-ftp-user',
2302 dest='no_anonymous_ftp_user',
2303 default=False, action='store_true',
2304 help='If set, the FTP server will not create '
2305 'an anonymous user.')
2308 if __name__ == '__main__':
2309 sys.exit(ServerRunner().main())