ChildAccountService: get service flags from AccountTrackerService instead of fetching...
[chromium-blink-merge.git] / net / tools / testserver / testserver.py
blobc861feeef9cb2848909c1acb5c74b58023021a6c
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.ContentTypeHandler,
333 self.NoContentHandler,
334 self.ServerRedirectHandler,
335 self.CrossSiteRedirectHandler,
336 self.ClientRedirectHandler,
337 self.GetSSLSessionCacheHandler,
338 self.SSLManySmallRecords,
339 self.GetChannelID,
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 5 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 5 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 ContentTypeHandler(self):
1380 """Returns a string of html with the given content type. E.g.,
1381 /contenttype?text/css returns an html file with the Content-Type
1382 header set to text/css."""
1384 if not self._ShouldHandleRequest("/contenttype"):
1385 return False
1386 query_char = self.path.find('?')
1387 content_type = self.path[query_char + 1:].strip()
1388 if not content_type:
1389 content_type = 'text/html'
1390 self.send_response(200)
1391 self.send_header('Content-Type', content_type)
1392 self.end_headers()
1393 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n")
1394 return True
1396 def NoContentHandler(self):
1397 """Returns a 204 No Content response."""
1399 if not self._ShouldHandleRequest("/nocontent"):
1400 return False
1401 self.send_response(204)
1402 self.end_headers()
1403 return True
1405 def ServerRedirectHandler(self):
1406 """Sends a server redirect to the given URL. The syntax is
1407 '/server-redirect?http://foo.bar/asdf' to redirect to
1408 'http://foo.bar/asdf'"""
1410 test_name = "/server-redirect"
1411 if not self._ShouldHandleRequest(test_name):
1412 return False
1414 query_char = self.path.find('?')
1415 if query_char < 0 or len(self.path) <= query_char + 1:
1416 self.sendRedirectHelp(test_name)
1417 return True
1418 dest = urllib.unquote(self.path[query_char + 1:])
1420 self.send_response(301) # moved permanently
1421 self.send_header('Location', dest)
1422 self.send_header('Content-Type', 'text/html')
1423 self.end_headers()
1424 self.wfile.write('<html><head>')
1425 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1427 return True
1429 def CrossSiteRedirectHandler(self):
1430 """Sends a server redirect to the given site. The syntax is
1431 '/cross-site/hostname/...' to redirect to //hostname/...
1432 It is used to navigate between different Sites, causing
1433 cross-site/cross-process navigations in the browser."""
1435 test_name = "/cross-site"
1436 if not self._ShouldHandleRequest(test_name):
1437 return False
1439 params = urllib.unquote(self.path[(len(test_name) + 1):])
1440 slash = params.find('/')
1441 if slash < 0:
1442 self.sendRedirectHelp(test_name)
1443 return True
1445 host = params[:slash]
1446 path = params[(slash+1):]
1447 dest = "//%s:%s/%s" % (host, str(self.server.server_port), path)
1449 self.send_response(301) # moved permanently
1450 self.send_header('Location', dest)
1451 self.send_header('Content-Type', 'text/html')
1452 self.end_headers()
1453 self.wfile.write('<html><head>')
1454 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1456 return True
1458 def ClientRedirectHandler(self):
1459 """Sends a client redirect to the given URL. The syntax is
1460 '/client-redirect?http://foo.bar/asdf' to redirect to
1461 'http://foo.bar/asdf'"""
1463 test_name = "/client-redirect"
1464 if not self._ShouldHandleRequest(test_name):
1465 return False
1467 query_char = self.path.find('?')
1468 if query_char < 0 or len(self.path) <= query_char + 1:
1469 self.sendRedirectHelp(test_name)
1470 return True
1471 dest = urllib.unquote(self.path[query_char + 1:])
1473 self.send_response(200)
1474 self.send_header('Content-Type', 'text/html')
1475 self.end_headers()
1476 self.wfile.write('<html><head>')
1477 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest)
1478 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest)
1480 return True
1482 def GetSSLSessionCacheHandler(self):
1483 """Send a reply containing a log of the session cache operations."""
1485 if not self._ShouldHandleRequest('/ssl-session-cache'):
1486 return False
1488 self.send_response(200)
1489 self.send_header('Content-Type', 'text/plain')
1490 self.end_headers()
1491 try:
1492 log = self.server.session_cache.log
1493 except AttributeError:
1494 self.wfile.write('Pass --https-record-resume in order to use' +
1495 ' this request')
1496 return True
1498 for (action, sessionID) in log:
1499 self.wfile.write('%s\t%s\n' % (action, bytes(sessionID).encode('hex')))
1500 return True
1502 def SSLManySmallRecords(self):
1503 """Sends a reply consisting of a variety of small writes. These will be
1504 translated into a series of small SSL records when used over an HTTPS
1505 server."""
1507 if not self._ShouldHandleRequest('/ssl-many-small-records'):
1508 return False
1510 self.send_response(200)
1511 self.send_header('Content-Type', 'text/plain')
1512 self.end_headers()
1514 # Write ~26K of data, in 1350 byte chunks
1515 for i in xrange(20):
1516 self.wfile.write('*' * 1350)
1517 self.wfile.flush()
1518 return True
1520 def GetChannelID(self):
1521 """Send a reply containing the hashed ChannelID that the client provided."""
1523 if not self._ShouldHandleRequest('/channel-id'):
1524 return False
1526 self.send_response(200)
1527 self.send_header('Content-Type', 'text/plain')
1528 self.end_headers()
1529 channel_id = bytes(self.server.tlsConnection.channel_id)
1530 self.wfile.write(hashlib.sha256(channel_id).digest().encode('base64'))
1531 return True
1533 def ClientCipherListHandler(self):
1534 """Send a reply containing the cipher suite list that the client
1535 provided. Each cipher suite value is serialized in decimal, followed by a
1536 newline."""
1538 if not self._ShouldHandleRequest('/client-cipher-list'):
1539 return False
1541 self.send_response(200)
1542 self.send_header('Content-Type', 'text/plain')
1543 self.end_headers()
1545 cipher_suites = self.server.tlsConnection.clientHello.cipher_suites
1546 self.wfile.write('\n'.join(str(c) for c in cipher_suites))
1547 return True
1549 def CloseSocketHandler(self):
1550 """Closes the socket without sending anything."""
1552 if not self._ShouldHandleRequest('/close-socket'):
1553 return False
1555 self.wfile.close()
1556 return True
1558 def RangeResetHandler(self):
1559 """Send data broken up by connection resets every N (default 4K) bytes.
1560 Support range requests. If the data requested doesn't straddle a reset
1561 boundary, it will all be sent. Used for testing resuming downloads."""
1563 def DataForRange(start, end):
1564 """Data to be provided for a particular range of bytes."""
1565 # Offset and scale to avoid too obvious (and hence potentially
1566 # collidable) data.
1567 return ''.join([chr(y % 256)
1568 for y in range(start * 2 + 15, end * 2 + 15, 2)])
1570 if not self._ShouldHandleRequest('/rangereset'):
1571 return False
1573 # HTTP/1.1 is required for ETag and range support.
1574 self.protocol_version = 'HTTP/1.1'
1575 _, _, url_path, _, query, _ = urlparse.urlparse(self.path)
1577 # Defaults
1578 size = 8000
1579 # Note that the rst is sent just before sending the rst_boundary byte.
1580 rst_boundary = 4000
1581 respond_to_range = True
1582 hold_for_signal = False
1583 rst_limit = -1
1584 token = 'DEFAULT'
1585 fail_precondition = 0
1586 send_verifiers = True
1588 # Parse the query
1589 qdict = urlparse.parse_qs(query, True)
1590 if 'size' in qdict:
1591 size = int(qdict['size'][0])
1592 if 'rst_boundary' in qdict:
1593 rst_boundary = int(qdict['rst_boundary'][0])
1594 if 'token' in qdict:
1595 # Identifying token for stateful tests.
1596 token = qdict['token'][0]
1597 if 'rst_limit' in qdict:
1598 # Max number of rsts for a given token.
1599 rst_limit = int(qdict['rst_limit'][0])
1600 if 'bounce_range' in qdict:
1601 respond_to_range = False
1602 if 'hold' in qdict:
1603 # Note that hold_for_signal will not work with null range requests;
1604 # see TODO below.
1605 hold_for_signal = True
1606 if 'no_verifiers' in qdict:
1607 send_verifiers = False
1608 if 'fail_precondition' in qdict:
1609 fail_precondition = int(qdict['fail_precondition'][0])
1611 # Record already set information, or set it.
1612 rst_limit = TestPageHandler.rst_limits.setdefault(token, rst_limit)
1613 if rst_limit != 0:
1614 TestPageHandler.rst_limits[token] -= 1
1615 fail_precondition = TestPageHandler.fail_precondition.setdefault(
1616 token, fail_precondition)
1617 if fail_precondition != 0:
1618 TestPageHandler.fail_precondition[token] -= 1
1620 first_byte = 0
1621 last_byte = size - 1
1623 # Does that define what we want to return, or do we need to apply
1624 # a range?
1625 range_response = False
1626 range_header = self.headers.getheader('range')
1627 if range_header and respond_to_range:
1628 mo = re.match("bytes=(\d*)-(\d*)", range_header)
1629 if mo.group(1):
1630 first_byte = int(mo.group(1))
1631 if mo.group(2):
1632 last_byte = int(mo.group(2))
1633 if last_byte > size - 1:
1634 last_byte = size - 1
1635 range_response = True
1636 if last_byte < first_byte:
1637 return False
1639 if (fail_precondition and
1640 (self.headers.getheader('If-Modified-Since') or
1641 self.headers.getheader('If-Match'))):
1642 self.send_response(412)
1643 self.end_headers()
1644 return True
1646 if range_response:
1647 self.send_response(206)
1648 self.send_header('Content-Range',
1649 'bytes %d-%d/%d' % (first_byte, last_byte, size))
1650 else:
1651 self.send_response(200)
1652 self.send_header('Content-Type', 'application/octet-stream')
1653 self.send_header('Content-Length', last_byte - first_byte + 1)
1654 if send_verifiers:
1655 # If fail_precondition is non-zero, then the ETag for each request will be
1656 # different.
1657 etag = "%s%d" % (token, fail_precondition)
1658 self.send_header('ETag', etag)
1659 self.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST')
1660 self.end_headers()
1662 if hold_for_signal:
1663 # TODO(rdsmith/phajdan.jr): http://crbug.com/169519: Without writing
1664 # a single byte, the self.server.handle_request() below hangs
1665 # without processing new incoming requests.
1666 self.wfile.write(DataForRange(first_byte, first_byte + 1))
1667 first_byte = first_byte + 1
1668 # handle requests until one of them clears this flag.
1669 self.server.wait_for_download = True
1670 while self.server.wait_for_download:
1671 self.server.handle_request()
1673 possible_rst = ((first_byte / rst_boundary) + 1) * rst_boundary
1674 if possible_rst >= last_byte or rst_limit == 0:
1675 # No RST has been requested in this range, so we don't need to
1676 # do anything fancy; just write the data and let the python
1677 # infrastructure close the connection.
1678 self.wfile.write(DataForRange(first_byte, last_byte + 1))
1679 self.wfile.flush()
1680 return True
1682 # We're resetting the connection part way in; go to the RST
1683 # boundary and then send an RST.
1684 # Because socket semantics do not guarantee that all the data will be
1685 # sent when using the linger semantics to hard close a socket,
1686 # we send the data and then wait for our peer to release us
1687 # before sending the reset.
1688 data = DataForRange(first_byte, possible_rst)
1689 self.wfile.write(data)
1690 self.wfile.flush()
1691 self.server.wait_for_download = True
1692 while self.server.wait_for_download:
1693 self.server.handle_request()
1694 l_onoff = 1 # Linger is active.
1695 l_linger = 0 # Seconds to linger for.
1696 self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
1697 struct.pack('ii', l_onoff, l_linger))
1699 # Close all duplicates of the underlying socket to force the RST.
1700 self.wfile.close()
1701 self.rfile.close()
1702 self.connection.close()
1704 return True
1706 def DefaultResponseHandler(self):
1707 """This is the catch-all response handler for requests that aren't handled
1708 by one of the special handlers above.
1709 Note that we specify the content-length as without it the https connection
1710 is not closed properly (and the browser keeps expecting data)."""
1712 contents = "Default response given for path: " + self.path
1713 self.send_response(200)
1714 self.send_header('Content-Type', 'text/html')
1715 self.send_header('Content-Length', len(contents))
1716 self.end_headers()
1717 if (self.command != 'HEAD'):
1718 self.wfile.write(contents)
1719 return True
1721 def RedirectConnectHandler(self):
1722 """Sends a redirect to the CONNECT request for www.redirect.com. This
1723 response is not specified by the RFC, so the browser should not follow
1724 the redirect."""
1726 if (self.path.find("www.redirect.com") < 0):
1727 return False
1729 dest = "http://www.destination.com/foo.js"
1731 self.send_response(302) # moved temporarily
1732 self.send_header('Location', dest)
1733 self.send_header('Connection', 'close')
1734 self.end_headers()
1735 return True
1737 def ServerAuthConnectHandler(self):
1738 """Sends a 401 to the CONNECT request for www.server-auth.com. This
1739 response doesn't make sense because the proxy server cannot request
1740 server authentication."""
1742 if (self.path.find("www.server-auth.com") < 0):
1743 return False
1745 challenge = 'Basic realm="WallyWorld"'
1747 self.send_response(401) # unauthorized
1748 self.send_header('WWW-Authenticate', challenge)
1749 self.send_header('Connection', 'close')
1750 self.end_headers()
1751 return True
1753 def DefaultConnectResponseHandler(self):
1754 """This is the catch-all response handler for CONNECT requests that aren't
1755 handled by one of the special handlers above. Real Web servers respond
1756 with 400 to CONNECT requests."""
1758 contents = "Your client has issued a malformed or illegal request."
1759 self.send_response(400) # bad request
1760 self.send_header('Content-Type', 'text/html')
1761 self.send_header('Content-Length', len(contents))
1762 self.end_headers()
1763 self.wfile.write(contents)
1764 return True
1766 # called by the redirect handling function when there is no parameter
1767 def sendRedirectHelp(self, redirect_name):
1768 self.send_response(200)
1769 self.send_header('Content-Type', 'text/html')
1770 self.end_headers()
1771 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>')
1772 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name)
1773 self.wfile.write('</body></html>')
1775 # called by chunked handling function
1776 def sendChunkHelp(self, chunk):
1777 # Each chunk consists of: chunk size (hex), CRLF, chunk body, CRLF
1778 self.wfile.write('%X\r\n' % len(chunk))
1779 self.wfile.write(chunk)
1780 self.wfile.write('\r\n')
1783 class OCSPHandler(testserver_base.BasePageHandler):
1784 def __init__(self, request, client_address, socket_server):
1785 handlers = [self.OCSPResponse]
1786 self.ocsp_response = socket_server.ocsp_response
1787 testserver_base.BasePageHandler.__init__(self, request, client_address,
1788 socket_server, [], handlers, [],
1789 handlers, [])
1791 def OCSPResponse(self):
1792 self.send_response(200)
1793 self.send_header('Content-Type', 'application/ocsp-response')
1794 self.send_header('Content-Length', str(len(self.ocsp_response)))
1795 self.end_headers()
1797 self.wfile.write(self.ocsp_response)
1800 class TCPEchoHandler(SocketServer.BaseRequestHandler):
1801 """The RequestHandler class for TCP echo server.
1803 It is instantiated once per connection to the server, and overrides the
1804 handle() method to implement communication to the client.
1807 def handle(self):
1808 """Handles the request from the client and constructs a response."""
1810 data = self.request.recv(65536).strip()
1811 # Verify the "echo request" message received from the client. Send back
1812 # "echo response" message if "echo request" message is valid.
1813 try:
1814 return_data = echo_message.GetEchoResponseData(data)
1815 if not return_data:
1816 return
1817 except ValueError:
1818 return
1820 self.request.send(return_data)
1823 class UDPEchoHandler(SocketServer.BaseRequestHandler):
1824 """The RequestHandler class for UDP echo server.
1826 It is instantiated once per connection to the server, and overrides the
1827 handle() method to implement communication to the client.
1830 def handle(self):
1831 """Handles the request from the client and constructs a response."""
1833 data = self.request[0].strip()
1834 request_socket = self.request[1]
1835 # Verify the "echo request" message received from the client. Send back
1836 # "echo response" message if "echo request" message is valid.
1837 try:
1838 return_data = echo_message.GetEchoResponseData(data)
1839 if not return_data:
1840 return
1841 except ValueError:
1842 return
1843 request_socket.sendto(return_data, self.client_address)
1846 class BasicAuthProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
1847 """A request handler that behaves as a proxy server which requires
1848 basic authentication. Only CONNECT, GET and HEAD is supported for now.
1851 _AUTH_CREDENTIAL = 'Basic Zm9vOmJhcg==' # foo:bar
1853 def parse_request(self):
1854 """Overrides parse_request to check credential."""
1856 if not BaseHTTPServer.BaseHTTPRequestHandler.parse_request(self):
1857 return False
1859 auth = self.headers.getheader('Proxy-Authorization')
1860 if auth != self._AUTH_CREDENTIAL:
1861 self.send_response(407)
1862 self.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"')
1863 self.end_headers()
1864 return False
1866 return True
1868 def _start_read_write(self, sock):
1869 sock.setblocking(0)
1870 self.request.setblocking(0)
1871 rlist = [self.request, sock]
1872 while True:
1873 ready_sockets, _unused, errors = select.select(rlist, [], [])
1874 if errors:
1875 self.send_response(500)
1876 self.end_headers()
1877 return
1878 for s in ready_sockets:
1879 received = s.recv(1024)
1880 if len(received) == 0:
1881 return
1882 if s == self.request:
1883 other = sock
1884 else:
1885 other = self.request
1886 other.send(received)
1888 def _do_common_method(self):
1889 url = urlparse.urlparse(self.path)
1890 port = url.port
1891 if not port:
1892 if url.scheme == 'http':
1893 port = 80
1894 elif url.scheme == 'https':
1895 port = 443
1896 if not url.hostname or not port:
1897 self.send_response(400)
1898 self.end_headers()
1899 return
1901 if len(url.path) == 0:
1902 path = '/'
1903 else:
1904 path = url.path
1905 if len(url.query) > 0:
1906 path = '%s?%s' % (url.path, url.query)
1908 sock = None
1909 try:
1910 sock = socket.create_connection((url.hostname, port))
1911 sock.send('%s %s %s\r\n' % (
1912 self.command, path, self.protocol_version))
1913 for header in self.headers.headers:
1914 header = header.strip()
1915 if (header.lower().startswith('connection') or
1916 header.lower().startswith('proxy')):
1917 continue
1918 sock.send('%s\r\n' % header)
1919 sock.send('\r\n')
1920 self._start_read_write(sock)
1921 except Exception:
1922 self.send_response(500)
1923 self.end_headers()
1924 finally:
1925 if sock is not None:
1926 sock.close()
1928 def do_CONNECT(self):
1929 try:
1930 pos = self.path.rfind(':')
1931 host = self.path[:pos]
1932 port = int(self.path[pos+1:])
1933 except Exception:
1934 self.send_response(400)
1935 self.end_headers()
1937 try:
1938 sock = socket.create_connection((host, port))
1939 self.send_response(200, 'Connection established')
1940 self.end_headers()
1941 self._start_read_write(sock)
1942 except Exception:
1943 self.send_response(500)
1944 self.end_headers()
1945 finally:
1946 sock.close()
1948 def do_GET(self):
1949 self._do_common_method()
1951 def do_HEAD(self):
1952 self._do_common_method()
1955 class ServerRunner(testserver_base.TestServerRunner):
1956 """TestServerRunner for the net test servers."""
1958 def __init__(self):
1959 super(ServerRunner, self).__init__()
1960 self.__ocsp_server = None
1962 def __make_data_dir(self):
1963 if self.options.data_dir:
1964 if not os.path.isdir(self.options.data_dir):
1965 raise testserver_base.OptionError('specified data dir not found: ' +
1966 self.options.data_dir + ' exiting...')
1967 my_data_dir = self.options.data_dir
1968 else:
1969 # Create the default path to our data dir, relative to the exe dir.
1970 my_data_dir = os.path.join(BASE_DIR, "..", "..", "..", "..",
1971 "test", "data")
1973 #TODO(ibrar): Must use Find* funtion defined in google\tools
1974 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data")
1976 return my_data_dir
1978 def create_server(self, server_data):
1979 port = self.options.port
1980 host = self.options.host
1982 # Work around a bug in Mac OS 10.6. Spawning a WebSockets server
1983 # will result in a call to |getaddrinfo|, which fails with "nodename
1984 # nor servname provided" for localhost:0 on 10.6.
1985 if self.options.server_type == SERVER_WEBSOCKET and \
1986 host == "localhost" and \
1987 port == 0:
1988 host = "127.0.0.1"
1990 if self.options.server_type == SERVER_HTTP:
1991 if self.options.https:
1992 pem_cert_and_key = None
1993 ocsp_der = None
1994 if self.options.cert_and_key_file:
1995 if not os.path.isfile(self.options.cert_and_key_file):
1996 raise testserver_base.OptionError(
1997 'specified server cert file not found: ' +
1998 self.options.cert_and_key_file + ' exiting...')
1999 pem_cert_and_key = file(self.options.cert_and_key_file, 'r').read()
2000 else:
2001 # generate a new certificate and run an OCSP server for it.
2002 self.__ocsp_server = OCSPServer((host, 0), OCSPHandler)
2003 print ('OCSP server started on %s:%d...' %
2004 (host, self.__ocsp_server.server_port))
2006 ocsp_state = None
2008 if self.options.ocsp == 'ok':
2009 ocsp_state = minica.OCSP_STATE_GOOD
2010 elif self.options.ocsp == 'revoked':
2011 ocsp_state = minica.OCSP_STATE_REVOKED
2012 elif self.options.ocsp == 'invalid':
2013 ocsp_state = minica.OCSP_STATE_INVALID
2014 elif self.options.ocsp == 'unauthorized':
2015 ocsp_state = minica.OCSP_STATE_UNAUTHORIZED
2016 elif self.options.ocsp == 'unknown':
2017 ocsp_state = minica.OCSP_STATE_UNKNOWN
2018 else:
2019 raise testserver_base.OptionError('unknown OCSP status: ' +
2020 self.options.ocsp_status)
2022 (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP(
2023 subject = "127.0.0.1",
2024 ocsp_url = ("http://%s:%d/ocsp" %
2025 (host, self.__ocsp_server.server_port)),
2026 ocsp_state = ocsp_state,
2027 serial = self.options.cert_serial)
2029 if self.options.ocsp_server_unavailable:
2030 # SEQUENCE containing ENUMERATED with value 3 (tryLater).
2031 self.__ocsp_server.ocsp_response = '30030a0103'.decode('hex')
2032 else:
2033 self.__ocsp_server.ocsp_response = ocsp_der
2035 for ca_cert in self.options.ssl_client_ca:
2036 if not os.path.isfile(ca_cert):
2037 raise testserver_base.OptionError(
2038 'specified trusted client CA file not found: ' + ca_cert +
2039 ' exiting...')
2041 stapled_ocsp_response = None
2042 if self.options.staple_ocsp_response:
2043 stapled_ocsp_response = ocsp_der
2045 server = HTTPSServer((host, port), TestPageHandler, pem_cert_and_key,
2046 self.options.ssl_client_auth,
2047 self.options.ssl_client_ca,
2048 self.options.ssl_client_cert_type,
2049 self.options.ssl_bulk_cipher,
2050 self.options.ssl_key_exchange,
2051 self.options.enable_npn,
2052 self.options.record_resume,
2053 self.options.tls_intolerant,
2054 self.options.tls_intolerance_type,
2055 self.options.signed_cert_timestamps_tls_ext.decode(
2056 "base64"),
2057 self.options.fallback_scsv,
2058 stapled_ocsp_response,
2059 self.options.alert_after_handshake)
2060 print 'HTTPS server started on https://%s:%d...' % \
2061 (host, server.server_port)
2062 else:
2063 server = HTTPServer((host, port), TestPageHandler)
2064 print 'HTTP server started on http://%s:%d...' % \
2065 (host, server.server_port)
2067 server.data_dir = self.__make_data_dir()
2068 server.file_root_url = self.options.file_root_url
2069 server_data['port'] = server.server_port
2070 elif self.options.server_type == SERVER_WEBSOCKET:
2071 # Launch pywebsocket via WebSocketServer.
2072 logger = logging.getLogger()
2073 logger.addHandler(logging.StreamHandler())
2074 # TODO(toyoshim): Remove following os.chdir. Currently this operation
2075 # is required to work correctly. It should be fixed from pywebsocket side.
2076 os.chdir(self.__make_data_dir())
2077 websocket_options = WebSocketOptions(host, port, '.')
2078 scheme = "ws"
2079 if self.options.cert_and_key_file:
2080 scheme = "wss"
2081 websocket_options.use_tls = True
2082 websocket_options.private_key = self.options.cert_and_key_file
2083 websocket_options.certificate = self.options.cert_and_key_file
2084 if self.options.ssl_client_auth:
2085 websocket_options.tls_client_cert_optional = False
2086 websocket_options.tls_client_auth = True
2087 if len(self.options.ssl_client_ca) != 1:
2088 raise testserver_base.OptionError(
2089 'one trusted client CA file should be specified')
2090 if not os.path.isfile(self.options.ssl_client_ca[0]):
2091 raise testserver_base.OptionError(
2092 'specified trusted client CA file not found: ' +
2093 self.options.ssl_client_ca[0] + ' exiting...')
2094 websocket_options.tls_client_ca = self.options.ssl_client_ca[0]
2095 print 'Trying to start websocket server on %s://%s:%d...' % \
2096 (scheme, websocket_options.server_host, websocket_options.port)
2097 server = WebSocketServer(websocket_options)
2098 print 'WebSocket server started on %s://%s:%d...' % \
2099 (scheme, host, server.server_port)
2100 server_data['port'] = server.server_port
2101 websocket_options.use_basic_auth = self.options.ws_basic_auth
2102 elif self.options.server_type == SERVER_TCP_ECHO:
2103 # Used for generating the key (randomly) that encodes the "echo request"
2104 # message.
2105 random.seed()
2106 server = TCPEchoServer((host, port), TCPEchoHandler)
2107 print 'Echo TCP server started on port %d...' % server.server_port
2108 server_data['port'] = server.server_port
2109 elif self.options.server_type == SERVER_UDP_ECHO:
2110 # Used for generating the key (randomly) that encodes the "echo request"
2111 # message.
2112 random.seed()
2113 server = UDPEchoServer((host, port), UDPEchoHandler)
2114 print 'Echo UDP server started on port %d...' % server.server_port
2115 server_data['port'] = server.server_port
2116 elif self.options.server_type == SERVER_BASIC_AUTH_PROXY:
2117 server = HTTPServer((host, port), BasicAuthProxyRequestHandler)
2118 print 'BasicAuthProxy server started on port %d...' % server.server_port
2119 server_data['port'] = server.server_port
2120 elif self.options.server_type == SERVER_FTP:
2121 my_data_dir = self.__make_data_dir()
2123 # Instantiate a dummy authorizer for managing 'virtual' users
2124 authorizer = pyftpdlib.ftpserver.DummyAuthorizer()
2126 # Define a new user having full r/w permissions
2127 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw')
2129 # Define a read-only anonymous user unless disabled
2130 if not self.options.no_anonymous_ftp_user:
2131 authorizer.add_anonymous(my_data_dir)
2133 # Instantiate FTP handler class
2134 ftp_handler = pyftpdlib.ftpserver.FTPHandler
2135 ftp_handler.authorizer = authorizer
2137 # Define a customized banner (string returned when client connects)
2138 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." %
2139 pyftpdlib.ftpserver.__ver__)
2141 # Instantiate FTP server class and listen to address:port
2142 server = pyftpdlib.ftpserver.FTPServer((host, port), ftp_handler)
2143 server_data['port'] = server.socket.getsockname()[1]
2144 print 'FTP server started on port %d...' % server_data['port']
2145 else:
2146 raise testserver_base.OptionError('unknown server type' +
2147 self.options.server_type)
2149 return server
2151 def run_server(self):
2152 if self.__ocsp_server:
2153 self.__ocsp_server.serve_forever_on_thread()
2155 testserver_base.TestServerRunner.run_server(self)
2157 if self.__ocsp_server:
2158 self.__ocsp_server.stop_serving()
2160 def add_options(self):
2161 testserver_base.TestServerRunner.add_options(self)
2162 self.option_parser.add_option('-f', '--ftp', action='store_const',
2163 const=SERVER_FTP, default=SERVER_HTTP,
2164 dest='server_type',
2165 help='start up an FTP server.')
2166 self.option_parser.add_option('--tcp-echo', action='store_const',
2167 const=SERVER_TCP_ECHO, default=SERVER_HTTP,
2168 dest='server_type',
2169 help='start up a tcp echo server.')
2170 self.option_parser.add_option('--udp-echo', action='store_const',
2171 const=SERVER_UDP_ECHO, default=SERVER_HTTP,
2172 dest='server_type',
2173 help='start up a udp echo server.')
2174 self.option_parser.add_option('--basic-auth-proxy', action='store_const',
2175 const=SERVER_BASIC_AUTH_PROXY,
2176 default=SERVER_HTTP, dest='server_type',
2177 help='start up a proxy server which requires '
2178 'basic authentication.')
2179 self.option_parser.add_option('--websocket', action='store_const',
2180 const=SERVER_WEBSOCKET, default=SERVER_HTTP,
2181 dest='server_type',
2182 help='start up a WebSocket server.')
2183 self.option_parser.add_option('--https', action='store_true',
2184 dest='https', help='Specify that https '
2185 'should be used.')
2186 self.option_parser.add_option('--cert-and-key-file',
2187 dest='cert_and_key_file', help='specify the '
2188 'path to the file containing the certificate '
2189 'and private key for the server in PEM '
2190 'format')
2191 self.option_parser.add_option('--ocsp', dest='ocsp', default='ok',
2192 help='The type of OCSP response generated '
2193 'for the automatically generated '
2194 'certificate. One of [ok,revoked,invalid]')
2195 self.option_parser.add_option('--cert-serial', dest='cert_serial',
2196 default=0, type=int,
2197 help='If non-zero then the generated '
2198 'certificate will have this serial number')
2199 self.option_parser.add_option('--tls-intolerant', dest='tls_intolerant',
2200 default='0', type='int',
2201 help='If nonzero, certain TLS connections '
2202 'will be aborted in order to test version '
2203 'fallback. 1 means all TLS versions will be '
2204 'aborted. 2 means TLS 1.1 or higher will be '
2205 'aborted. 3 means TLS 1.2 or higher will be '
2206 'aborted.')
2207 self.option_parser.add_option('--tls-intolerance-type',
2208 dest='tls_intolerance_type',
2209 default="alert",
2210 help='Controls how the server reacts to a '
2211 'TLS version it is intolerant to. Valid '
2212 'values are "alert", "close", and "reset".')
2213 self.option_parser.add_option('--signed-cert-timestamps-tls-ext',
2214 dest='signed_cert_timestamps_tls_ext',
2215 default='',
2216 help='Base64 encoded SCT list. If set, '
2217 'server will respond with a '
2218 'signed_certificate_timestamp TLS extension '
2219 'whenever the client supports it.')
2220 self.option_parser.add_option('--fallback-scsv', dest='fallback_scsv',
2221 default=False, const=True,
2222 action='store_const',
2223 help='If given, TLS_FALLBACK_SCSV support '
2224 'will be enabled. This causes the server to '
2225 'reject fallback connections from compatible '
2226 'clients (e.g. Chrome).')
2227 self.option_parser.add_option('--staple-ocsp-response',
2228 dest='staple_ocsp_response',
2229 default=False, action='store_true',
2230 help='If set, server will staple the OCSP '
2231 'response whenever OCSP is on and the client '
2232 'supports OCSP stapling.')
2233 self.option_parser.add_option('--https-record-resume',
2234 dest='record_resume', const=True,
2235 default=False, action='store_const',
2236 help='Record resumption cache events rather '
2237 'than resuming as normal. Allows the use of '
2238 'the /ssl-session-cache request')
2239 self.option_parser.add_option('--ssl-client-auth', action='store_true',
2240 help='Require SSL client auth on every '
2241 'connection.')
2242 self.option_parser.add_option('--ssl-client-ca', action='append',
2243 default=[], help='Specify that the client '
2244 'certificate request should include the CA '
2245 'named in the subject of the DER-encoded '
2246 'certificate contained in the specified '
2247 'file. This option may appear multiple '
2248 'times, indicating multiple CA names should '
2249 'be sent in the request.')
2250 self.option_parser.add_option('--ssl-client-cert-type', action='append',
2251 default=[], help='Specify that the client '
2252 'certificate request should include the '
2253 'specified certificate_type value. This '
2254 'option may appear multiple times, '
2255 'indicating multiple values should be send '
2256 'in the request. Valid values are '
2257 '"rsa_sign", "dss_sign", and "ecdsa_sign". '
2258 'If omitted, "rsa_sign" will be used.')
2259 self.option_parser.add_option('--ssl-bulk-cipher', action='append',
2260 help='Specify the bulk encryption '
2261 'algorithm(s) that will be accepted by the '
2262 'SSL server. Valid values are "aes128gcm", '
2263 '"aes256", "aes128", "3des", "rc4". If '
2264 'omitted, all algorithms will be used. This '
2265 'option may appear multiple times, '
2266 'indicating multiple algorithms should be '
2267 'enabled.');
2268 self.option_parser.add_option('--ssl-key-exchange', action='append',
2269 help='Specify the key exchange algorithm(s)'
2270 'that will be accepted by the SSL server. '
2271 'Valid values are "rsa", "dhe_rsa", '
2272 '"ecdhe_rsa". If omitted, all algorithms '
2273 'will be used. This option may appear '
2274 'multiple times, indicating multiple '
2275 'algorithms should be enabled.');
2276 # TODO(davidben): Add ALPN support to tlslite.
2277 self.option_parser.add_option('--enable-npn', dest='enable_npn',
2278 default=False, const=True,
2279 action='store_const',
2280 help='Enable server support for the NPN '
2281 'extension. The server will advertise '
2282 'support for exactly one protocol, http/1.1')
2283 self.option_parser.add_option('--file-root-url', default='/files/',
2284 help='Specify a root URL for files served.')
2285 # TODO(ricea): Generalize this to support basic auth for HTTP too.
2286 self.option_parser.add_option('--ws-basic-auth', action='store_true',
2287 dest='ws_basic_auth',
2288 help='Enable basic-auth for WebSocket')
2289 self.option_parser.add_option('--ocsp-server-unavailable',
2290 dest='ocsp_server_unavailable',
2291 default=False, action='store_true',
2292 help='If set, the OCSP server will return '
2293 'a tryLater status rather than the actual '
2294 'OCSP response.')
2295 self.option_parser.add_option('--alert-after-handshake',
2296 dest='alert_after_handshake',
2297 default=False, action='store_true',
2298 help='If set, the server will send a fatal '
2299 'alert immediately after the handshake.')
2300 self.option_parser.add_option('--no-anonymous-ftp-user',
2301 dest='no_anonymous_ftp_user',
2302 default=False, action='store_true',
2303 help='If set, the FTP server will not create '
2304 'an anonymous user.')
2307 if __name__ == '__main__':
2308 sys.exit(ServerRunner().main())