testserver.py revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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. 5 6"""This is a simple HTTP/FTP/TCP/UDP/BASIC_AUTH_PROXY/WEBSOCKET server used for 7testing Chrome. 8 9It supports several test URLs, as specified by the handlers in TestPageHandler. 10By default, it listens on an ephemeral port and sends the port number back to 11the originating process over a pipe. The originating process can specify an 12explicit port if necessary. 13It can use https if you specify the flag --https=CERT where CERT is the path 14to a pem file containing the certificate and private key that should be used. 15""" 16 17import base64 18import BaseHTTPServer 19import cgi 20import hashlib 21import logging 22import minica 23import os 24import random 25import re 26import select 27import socket 28import SocketServer 29import struct 30import sys 31import threading 32import time 33import urllib 34import urlparse 35import zlib 36 37import echo_message 38import pyftpdlib.ftpserver 39import testserver_base 40import tlslite 41import tlslite.api 42 43BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 44sys.path.insert( 45 0, os.path.join(BASE_DIR, '..', '..', '..', 'third_party/pywebsocket/src')) 46from mod_pywebsocket.standalone import WebSocketServer 47 48SERVER_HTTP = 0 49SERVER_FTP = 1 50SERVER_TCP_ECHO = 2 51SERVER_UDP_ECHO = 3 52SERVER_BASIC_AUTH_PROXY = 4 53SERVER_WEBSOCKET = 5 54 55# Default request queue size for WebSocketServer. 56_DEFAULT_REQUEST_QUEUE_SIZE = 128 57 58class WebSocketOptions: 59 """Holds options for WebSocketServer.""" 60 61 def __init__(self, host, port, data_dir): 62 self.request_queue_size = _DEFAULT_REQUEST_QUEUE_SIZE 63 self.server_host = host 64 self.port = port 65 self.websock_handlers = data_dir 66 self.scan_dir = None 67 self.allow_handlers_outside_root_dir = False 68 self.websock_handlers_map_file = None 69 self.cgi_directories = [] 70 self.is_executable_method = None 71 self.allow_draft75 = False 72 self.strict = True 73 74 self.use_tls = False 75 self.private_key = None 76 self.certificate = None 77 self.tls_client_auth = False 78 self.tls_client_ca = None 79 self.use_basic_auth = False 80 81 82class RecordingSSLSessionCache(object): 83 """RecordingSSLSessionCache acts as a TLS session cache and maintains a log of 84 lookups and inserts in order to test session cache behaviours.""" 85 86 def __init__(self): 87 self.log = [] 88 89 def __getitem__(self, sessionID): 90 self.log.append(('lookup', sessionID)) 91 raise KeyError() 92 93 def __setitem__(self, sessionID, session): 94 self.log.append(('insert', sessionID)) 95 96 97class HTTPServer(testserver_base.ClientRestrictingServerMixIn, 98 testserver_base.BrokenPipeHandlerMixIn, 99 testserver_base.StoppableHTTPServer): 100 """This is a specialization of StoppableHTTPServer that adds client 101 verification.""" 102 103 pass 104 105class OCSPServer(testserver_base.ClientRestrictingServerMixIn, 106 testserver_base.BrokenPipeHandlerMixIn, 107 BaseHTTPServer.HTTPServer): 108 """This is a specialization of HTTPServer that serves an 109 OCSP response""" 110 111 def serve_forever_on_thread(self): 112 self.thread = threading.Thread(target = self.serve_forever, 113 name = "OCSPServerThread") 114 self.thread.start() 115 116 def stop_serving(self): 117 self.shutdown() 118 self.thread.join() 119 120 121class HTTPSServer(tlslite.api.TLSSocketServerMixIn, 122 testserver_base.ClientRestrictingServerMixIn, 123 testserver_base.BrokenPipeHandlerMixIn, 124 testserver_base.StoppableHTTPServer): 125 """This is a specialization of StoppableHTTPServer that add https support and 126 client verification.""" 127 128 def __init__(self, server_address, request_hander_class, pem_cert_and_key, 129 ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers, 130 record_resume_info, tls_intolerant): 131 self.cert_chain = tlslite.api.X509CertChain().parseChain(pem_cert_and_key) 132 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key, private=True) 133 self.ssl_client_auth = ssl_client_auth 134 self.ssl_client_cas = [] 135 self.tls_intolerant = tls_intolerant 136 137 for ca_file in ssl_client_cas: 138 s = open(ca_file).read() 139 x509 = tlslite.api.X509() 140 x509.parse(s) 141 self.ssl_client_cas.append(x509.subject) 142 self.ssl_handshake_settings = tlslite.api.HandshakeSettings() 143 if ssl_bulk_ciphers is not None: 144 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers 145 146 if record_resume_info: 147 # If record_resume_info is true then we'll replace the session cache with 148 # an object that records the lookups and inserts that it sees. 149 self.session_cache = RecordingSSLSessionCache() 150 else: 151 self.session_cache = tlslite.api.SessionCache() 152 testserver_base.StoppableHTTPServer.__init__(self, 153 server_address, 154 request_hander_class) 155 156 def handshake(self, tlsConnection): 157 """Creates the SSL connection.""" 158 159 try: 160 self.tlsConnection = tlsConnection 161 tlsConnection.handshakeServer(certChain=self.cert_chain, 162 privateKey=self.private_key, 163 sessionCache=self.session_cache, 164 reqCert=self.ssl_client_auth, 165 settings=self.ssl_handshake_settings, 166 reqCAs=self.ssl_client_cas, 167 tlsIntolerant=self.tls_intolerant) 168 tlsConnection.ignoreAbruptClose = True 169 return True 170 except tlslite.api.TLSAbruptCloseError: 171 # Ignore abrupt close. 172 return True 173 except tlslite.api.TLSError, error: 174 print "Handshake failure:", str(error) 175 return False 176 177 178class FTPServer(testserver_base.ClientRestrictingServerMixIn, 179 pyftpdlib.ftpserver.FTPServer): 180 """This is a specialization of FTPServer that adds client verification.""" 181 182 pass 183 184 185class TCPEchoServer(testserver_base.ClientRestrictingServerMixIn, 186 SocketServer.TCPServer): 187 """A TCP echo server that echoes back what it has received.""" 188 189 def server_bind(self): 190 """Override server_bind to store the server name.""" 191 192 SocketServer.TCPServer.server_bind(self) 193 host, port = self.socket.getsockname()[:2] 194 self.server_name = socket.getfqdn(host) 195 self.server_port = port 196 197 def serve_forever(self): 198 self.stop = False 199 self.nonce_time = None 200 while not self.stop: 201 self.handle_request() 202 self.socket.close() 203 204 205class UDPEchoServer(testserver_base.ClientRestrictingServerMixIn, 206 SocketServer.UDPServer): 207 """A UDP echo server that echoes back what it has received.""" 208 209 def server_bind(self): 210 """Override server_bind to store the server name.""" 211 212 SocketServer.UDPServer.server_bind(self) 213 host, port = self.socket.getsockname()[:2] 214 self.server_name = socket.getfqdn(host) 215 self.server_port = port 216 217 def serve_forever(self): 218 self.stop = False 219 self.nonce_time = None 220 while not self.stop: 221 self.handle_request() 222 self.socket.close() 223 224 225class TestPageHandler(testserver_base.BasePageHandler): 226 # Class variables to allow for persistence state between page handler 227 # invocations 228 rst_limits = {} 229 fail_precondition = {} 230 231 def __init__(self, request, client_address, socket_server): 232 connect_handlers = [ 233 self.RedirectConnectHandler, 234 self.ServerAuthConnectHandler, 235 self.DefaultConnectResponseHandler] 236 get_handlers = [ 237 self.NoCacheMaxAgeTimeHandler, 238 self.NoCacheTimeHandler, 239 self.CacheTimeHandler, 240 self.CacheExpiresHandler, 241 self.CacheProxyRevalidateHandler, 242 self.CachePrivateHandler, 243 self.CachePublicHandler, 244 self.CacheSMaxAgeHandler, 245 self.CacheMustRevalidateHandler, 246 self.CacheMustRevalidateMaxAgeHandler, 247 self.CacheNoStoreHandler, 248 self.CacheNoStoreMaxAgeHandler, 249 self.CacheNoTransformHandler, 250 self.DownloadHandler, 251 self.DownloadFinishHandler, 252 self.EchoHeader, 253 self.EchoHeaderCache, 254 self.EchoAllHandler, 255 self.ZipFileHandler, 256 self.FileHandler, 257 self.SetCookieHandler, 258 self.SetManyCookiesHandler, 259 self.ExpectAndSetCookieHandler, 260 self.SetHeaderHandler, 261 self.AuthBasicHandler, 262 self.AuthDigestHandler, 263 self.SlowServerHandler, 264 self.ChunkedServerHandler, 265 self.ContentTypeHandler, 266 self.NoContentHandler, 267 self.ServerRedirectHandler, 268 self.ClientRedirectHandler, 269 self.MultipartHandler, 270 self.MultipartSlowHandler, 271 self.GetSSLSessionCacheHandler, 272 self.SSLManySmallRecords, 273 self.GetChannelID, 274 self.CloseSocketHandler, 275 self.RangeResetHandler, 276 self.DefaultResponseHandler] 277 post_handlers = [ 278 self.EchoTitleHandler, 279 self.EchoHandler, 280 self.PostOnlyFileHandler] + get_handlers 281 put_handlers = [ 282 self.EchoTitleHandler, 283 self.EchoHandler] + get_handlers 284 head_handlers = [ 285 self.FileHandler, 286 self.DefaultResponseHandler] 287 288 self._mime_types = { 289 'crx' : 'application/x-chrome-extension', 290 'exe' : 'application/octet-stream', 291 'gif': 'image/gif', 292 'jpeg' : 'image/jpeg', 293 'jpg' : 'image/jpeg', 294 'json': 'application/json', 295 'pdf' : 'application/pdf', 296 'wav' : 'audio/wav', 297 'xml' : 'text/xml' 298 } 299 self._default_mime_type = 'text/html' 300 301 testserver_base.BasePageHandler.__init__(self, request, client_address, 302 socket_server, connect_handlers, 303 get_handlers, head_handlers, 304 post_handlers, put_handlers) 305 306 def GetMIMETypeFromName(self, file_name): 307 """Returns the mime type for the specified file_name. So far it only looks 308 at the file extension.""" 309 310 (_shortname, extension) = os.path.splitext(file_name.split("?")[0]) 311 if len(extension) == 0: 312 # no extension. 313 return self._default_mime_type 314 315 # extension starts with a dot, so we need to remove it 316 return self._mime_types.get(extension[1:], self._default_mime_type) 317 318 def NoCacheMaxAgeTimeHandler(self): 319 """This request handler yields a page with the title set to the current 320 system time, and no caching requested.""" 321 322 if not self._ShouldHandleRequest("/nocachetime/maxage"): 323 return False 324 325 self.send_response(200) 326 self.send_header('Cache-Control', 'max-age=0') 327 self.send_header('Content-Type', 'text/html') 328 self.end_headers() 329 330 self.wfile.write('<html><head><title>%s</title></head></html>' % 331 time.time()) 332 333 return True 334 335 def NoCacheTimeHandler(self): 336 """This request handler yields a page with the title set to the current 337 system time, and no caching requested.""" 338 339 if not self._ShouldHandleRequest("/nocachetime"): 340 return False 341 342 self.send_response(200) 343 self.send_header('Cache-Control', 'no-cache') 344 self.send_header('Content-Type', 'text/html') 345 self.end_headers() 346 347 self.wfile.write('<html><head><title>%s</title></head></html>' % 348 time.time()) 349 350 return True 351 352 def CacheTimeHandler(self): 353 """This request handler yields a page with the title set to the current 354 system time, and allows caching for one minute.""" 355 356 if not self._ShouldHandleRequest("/cachetime"): 357 return False 358 359 self.send_response(200) 360 self.send_header('Cache-Control', 'max-age=60') 361 self.send_header('Content-Type', 'text/html') 362 self.end_headers() 363 364 self.wfile.write('<html><head><title>%s</title></head></html>' % 365 time.time()) 366 367 return True 368 369 def CacheExpiresHandler(self): 370 """This request handler yields a page with the title set to the current 371 system time, and set the page to expire on 1 Jan 2099.""" 372 373 if not self._ShouldHandleRequest("/cache/expires"): 374 return False 375 376 self.send_response(200) 377 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT') 378 self.send_header('Content-Type', 'text/html') 379 self.end_headers() 380 381 self.wfile.write('<html><head><title>%s</title></head></html>' % 382 time.time()) 383 384 return True 385 386 def CacheProxyRevalidateHandler(self): 387 """This request handler yields a page with the title set to the current 388 system time, and allows caching for 60 seconds""" 389 390 if not self._ShouldHandleRequest("/cache/proxy-revalidate"): 391 return False 392 393 self.send_response(200) 394 self.send_header('Content-Type', 'text/html') 395 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate') 396 self.end_headers() 397 398 self.wfile.write('<html><head><title>%s</title></head></html>' % 399 time.time()) 400 401 return True 402 403 def CachePrivateHandler(self): 404 """This request handler yields a page with the title set to the current 405 system time, and allows caching for 5 seconds.""" 406 407 if not self._ShouldHandleRequest("/cache/private"): 408 return False 409 410 self.send_response(200) 411 self.send_header('Content-Type', 'text/html') 412 self.send_header('Cache-Control', 'max-age=3, private') 413 self.end_headers() 414 415 self.wfile.write('<html><head><title>%s</title></head></html>' % 416 time.time()) 417 418 return True 419 420 def CachePublicHandler(self): 421 """This request handler yields a page with the title set to the current 422 system time, and allows caching for 5 seconds.""" 423 424 if not self._ShouldHandleRequest("/cache/public"): 425 return False 426 427 self.send_response(200) 428 self.send_header('Content-Type', 'text/html') 429 self.send_header('Cache-Control', 'max-age=3, public') 430 self.end_headers() 431 432 self.wfile.write('<html><head><title>%s</title></head></html>' % 433 time.time()) 434 435 return True 436 437 def CacheSMaxAgeHandler(self): 438 """This request handler yields a page with the title set to the current 439 system time, and does not allow for caching.""" 440 441 if not self._ShouldHandleRequest("/cache/s-maxage"): 442 return False 443 444 self.send_response(200) 445 self.send_header('Content-Type', 'text/html') 446 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0') 447 self.end_headers() 448 449 self.wfile.write('<html><head><title>%s</title></head></html>' % 450 time.time()) 451 452 return True 453 454 def CacheMustRevalidateHandler(self): 455 """This request handler yields a page with the title set to the current 456 system time, and does not allow caching.""" 457 458 if not self._ShouldHandleRequest("/cache/must-revalidate"): 459 return False 460 461 self.send_response(200) 462 self.send_header('Content-Type', 'text/html') 463 self.send_header('Cache-Control', 'must-revalidate') 464 self.end_headers() 465 466 self.wfile.write('<html><head><title>%s</title></head></html>' % 467 time.time()) 468 469 return True 470 471 def CacheMustRevalidateMaxAgeHandler(self): 472 """This request handler yields a page with the title set to the current 473 system time, and does not allow caching event though max-age of 60 474 seconds is specified.""" 475 476 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"): 477 return False 478 479 self.send_response(200) 480 self.send_header('Content-Type', 'text/html') 481 self.send_header('Cache-Control', 'max-age=60, must-revalidate') 482 self.end_headers() 483 484 self.wfile.write('<html><head><title>%s</title></head></html>' % 485 time.time()) 486 487 return True 488 489 def CacheNoStoreHandler(self): 490 """This request handler yields a page with the title set to the current 491 system time, and does not allow the page to be stored.""" 492 493 if not self._ShouldHandleRequest("/cache/no-store"): 494 return False 495 496 self.send_response(200) 497 self.send_header('Content-Type', 'text/html') 498 self.send_header('Cache-Control', 'no-store') 499 self.end_headers() 500 501 self.wfile.write('<html><head><title>%s</title></head></html>' % 502 time.time()) 503 504 return True 505 506 def CacheNoStoreMaxAgeHandler(self): 507 """This request handler yields a page with the title set to the current 508 system time, and does not allow the page to be stored even though max-age 509 of 60 seconds is specified.""" 510 511 if not self._ShouldHandleRequest("/cache/no-store/max-age"): 512 return False 513 514 self.send_response(200) 515 self.send_header('Content-Type', 'text/html') 516 self.send_header('Cache-Control', 'max-age=60, no-store') 517 self.end_headers() 518 519 self.wfile.write('<html><head><title>%s</title></head></html>' % 520 time.time()) 521 522 return True 523 524 525 def CacheNoTransformHandler(self): 526 """This request handler yields a page with the title set to the current 527 system time, and does not allow the content to transformed during 528 user-agent caching""" 529 530 if not self._ShouldHandleRequest("/cache/no-transform"): 531 return False 532 533 self.send_response(200) 534 self.send_header('Content-Type', 'text/html') 535 self.send_header('Cache-Control', 'no-transform') 536 self.end_headers() 537 538 self.wfile.write('<html><head><title>%s</title></head></html>' % 539 time.time()) 540 541 return True 542 543 def EchoHeader(self): 544 """This handler echoes back the value of a specific request header.""" 545 546 return self.EchoHeaderHelper("/echoheader") 547 548 def EchoHeaderCache(self): 549 """This function echoes back the value of a specific request header while 550 allowing caching for 16 hours.""" 551 552 return self.EchoHeaderHelper("/echoheadercache") 553 554 def EchoHeaderHelper(self, echo_header): 555 """This function echoes back the value of the request header passed in.""" 556 557 if not self._ShouldHandleRequest(echo_header): 558 return False 559 560 query_char = self.path.find('?') 561 if query_char != -1: 562 header_name = self.path[query_char+1:] 563 564 self.send_response(200) 565 self.send_header('Content-Type', 'text/plain') 566 if echo_header == '/echoheadercache': 567 self.send_header('Cache-control', 'max-age=60000') 568 else: 569 self.send_header('Cache-control', 'no-cache') 570 # insert a vary header to properly indicate that the cachability of this 571 # request is subject to value of the request header being echoed. 572 if len(header_name) > 0: 573 self.send_header('Vary', header_name) 574 self.end_headers() 575 576 if len(header_name) > 0: 577 self.wfile.write(self.headers.getheader(header_name)) 578 579 return True 580 581 def ReadRequestBody(self): 582 """This function reads the body of the current HTTP request, handling 583 both plain and chunked transfer encoded requests.""" 584 585 if self.headers.getheader('transfer-encoding') != 'chunked': 586 length = int(self.headers.getheader('content-length')) 587 return self.rfile.read(length) 588 589 # Read the request body as chunks. 590 body = "" 591 while True: 592 line = self.rfile.readline() 593 length = int(line, 16) 594 if length == 0: 595 self.rfile.readline() 596 break 597 body += self.rfile.read(length) 598 self.rfile.read(2) 599 return body 600 601 def EchoHandler(self): 602 """This handler just echoes back the payload of the request, for testing 603 form submission.""" 604 605 if not self._ShouldHandleRequest("/echo"): 606 return False 607 608 self.send_response(200) 609 self.send_header('Content-Type', 'text/html') 610 self.end_headers() 611 self.wfile.write(self.ReadRequestBody()) 612 return True 613 614 def EchoTitleHandler(self): 615 """This handler is like Echo, but sets the page title to the request.""" 616 617 if not self._ShouldHandleRequest("/echotitle"): 618 return False 619 620 self.send_response(200) 621 self.send_header('Content-Type', 'text/html') 622 self.end_headers() 623 request = self.ReadRequestBody() 624 self.wfile.write('<html><head><title>') 625 self.wfile.write(request) 626 self.wfile.write('</title></head></html>') 627 return True 628 629 def EchoAllHandler(self): 630 """This handler yields a (more) human-readable page listing information 631 about the request header & contents.""" 632 633 if not self._ShouldHandleRequest("/echoall"): 634 return False 635 636 self.send_response(200) 637 self.send_header('Content-Type', 'text/html') 638 self.end_headers() 639 self.wfile.write('<html><head><style>' 640 'pre { border: 1px solid black; margin: 5px; padding: 5px }' 641 '</style></head><body>' 642 '<div style="float: right">' 643 '<a href="/echo">back to referring page</a></div>' 644 '<h1>Request Body:</h1><pre>') 645 646 if self.command == 'POST' or self.command == 'PUT': 647 qs = self.ReadRequestBody() 648 params = cgi.parse_qs(qs, keep_blank_values=1) 649 650 for param in params: 651 self.wfile.write('%s=%s\n' % (param, params[param][0])) 652 653 self.wfile.write('</pre>') 654 655 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers) 656 657 self.wfile.write('</body></html>') 658 return True 659 660 def DownloadHandler(self): 661 """This handler sends a downloadable file with or without reporting 662 the size (6K).""" 663 664 if self.path.startswith("/download-unknown-size"): 665 send_length = False 666 elif self.path.startswith("/download-known-size"): 667 send_length = True 668 else: 669 return False 670 671 # 672 # The test which uses this functionality is attempting to send 673 # small chunks of data to the client. Use a fairly large buffer 674 # so that we'll fill chrome's IO buffer enough to force it to 675 # actually write the data. 676 # See also the comments in the client-side of this test in 677 # download_uitest.cc 678 # 679 size_chunk1 = 35*1024 680 size_chunk2 = 10*1024 681 682 self.send_response(200) 683 self.send_header('Content-Type', 'application/octet-stream') 684 self.send_header('Cache-Control', 'max-age=0') 685 if send_length: 686 self.send_header('Content-Length', size_chunk1 + size_chunk2) 687 self.end_headers() 688 689 # First chunk of data: 690 self.wfile.write("*" * size_chunk1) 691 self.wfile.flush() 692 693 # handle requests until one of them clears this flag. 694 self.server.wait_for_download = True 695 while self.server.wait_for_download: 696 self.server.handle_request() 697 698 # Second chunk of data: 699 self.wfile.write("*" * size_chunk2) 700 return True 701 702 def DownloadFinishHandler(self): 703 """This handler just tells the server to finish the current download.""" 704 705 if not self._ShouldHandleRequest("/download-finish"): 706 return False 707 708 self.server.wait_for_download = False 709 self.send_response(200) 710 self.send_header('Content-Type', 'text/html') 711 self.send_header('Cache-Control', 'max-age=0') 712 self.end_headers() 713 return True 714 715 def _ReplaceFileData(self, data, query_parameters): 716 """Replaces matching substrings in a file. 717 718 If the 'replace_text' URL query parameter is present, it is expected to be 719 of the form old_text:new_text, which indicates that any old_text strings in 720 the file are replaced with new_text. Multiple 'replace_text' parameters may 721 be specified. 722 723 If the parameters are not present, |data| is returned. 724 """ 725 726 query_dict = cgi.parse_qs(query_parameters) 727 replace_text_values = query_dict.get('replace_text', []) 728 for replace_text_value in replace_text_values: 729 replace_text_args = replace_text_value.split(':') 730 if len(replace_text_args) != 2: 731 raise ValueError( 732 'replace_text must be of form old_text:new_text. Actual value: %s' % 733 replace_text_value) 734 old_text_b64, new_text_b64 = replace_text_args 735 old_text = base64.urlsafe_b64decode(old_text_b64) 736 new_text = base64.urlsafe_b64decode(new_text_b64) 737 data = data.replace(old_text, new_text) 738 return data 739 740 def ZipFileHandler(self): 741 """This handler sends the contents of the requested file in compressed form. 742 Can pass in a parameter that specifies that the content length be 743 C - the compressed size (OK), 744 U - the uncompressed size (Non-standard, but handled), 745 S - less than compressed (OK because we keep going), 746 M - larger than compressed but less than uncompressed (an error), 747 L - larger than uncompressed (an error) 748 Example: compressedfiles/Picture_1.doc?C 749 """ 750 751 prefix = "/compressedfiles/" 752 if not self.path.startswith(prefix): 753 return False 754 755 # Consume a request body if present. 756 if self.command == 'POST' or self.command == 'PUT' : 757 self.ReadRequestBody() 758 759 _, _, url_path, _, query, _ = urlparse.urlparse(self.path) 760 761 if not query in ('C', 'U', 'S', 'M', 'L'): 762 return False 763 764 sub_path = url_path[len(prefix):] 765 entries = sub_path.split('/') 766 file_path = os.path.join(self.server.data_dir, *entries) 767 if os.path.isdir(file_path): 768 file_path = os.path.join(file_path, 'index.html') 769 770 if not os.path.isfile(file_path): 771 print "File not found " + sub_path + " full path:" + file_path 772 self.send_error(404) 773 return True 774 775 f = open(file_path, "rb") 776 data = f.read() 777 uncompressed_len = len(data) 778 f.close() 779 780 # Compress the data. 781 data = zlib.compress(data) 782 compressed_len = len(data) 783 784 content_length = compressed_len 785 if query == 'U': 786 content_length = uncompressed_len 787 elif query == 'S': 788 content_length = compressed_len / 2 789 elif query == 'M': 790 content_length = (compressed_len + uncompressed_len) / 2 791 elif query == 'L': 792 content_length = compressed_len + uncompressed_len 793 794 self.send_response(200) 795 self.send_header('Content-Type', 'application/msword') 796 self.send_header('Content-encoding', 'deflate') 797 self.send_header('Connection', 'close') 798 self.send_header('Content-Length', content_length) 799 self.send_header('ETag', '\'' + file_path + '\'') 800 self.end_headers() 801 802 self.wfile.write(data) 803 804 return True 805 806 def FileHandler(self): 807 """This handler sends the contents of the requested file. Wow, it's like 808 a real webserver!""" 809 810 prefix = self.server.file_root_url 811 if not self.path.startswith(prefix): 812 return False 813 return self._FileHandlerHelper(prefix) 814 815 def PostOnlyFileHandler(self): 816 """This handler sends the contents of the requested file on a POST.""" 817 818 prefix = urlparse.urljoin(self.server.file_root_url, 'post/') 819 if not self.path.startswith(prefix): 820 return False 821 return self._FileHandlerHelper(prefix) 822 823 def _FileHandlerHelper(self, prefix): 824 request_body = '' 825 if self.command == 'POST' or self.command == 'PUT': 826 # Consume a request body if present. 827 request_body = self.ReadRequestBody() 828 829 _, _, url_path, _, query, _ = urlparse.urlparse(self.path) 830 query_dict = cgi.parse_qs(query) 831 832 expected_body = query_dict.get('expected_body', []) 833 if expected_body and request_body not in expected_body: 834 self.send_response(404) 835 self.end_headers() 836 self.wfile.write('') 837 return True 838 839 expected_headers = query_dict.get('expected_headers', []) 840 for expected_header in expected_headers: 841 header_name, expected_value = expected_header.split(':') 842 if self.headers.getheader(header_name) != expected_value: 843 self.send_response(404) 844 self.end_headers() 845 self.wfile.write('') 846 return True 847 848 sub_path = url_path[len(prefix):] 849 entries = sub_path.split('/') 850 file_path = os.path.join(self.server.data_dir, *entries) 851 if os.path.isdir(file_path): 852 file_path = os.path.join(file_path, 'index.html') 853 854 if not os.path.isfile(file_path): 855 print "File not found " + sub_path + " full path:" + file_path 856 self.send_error(404) 857 return True 858 859 f = open(file_path, "rb") 860 data = f.read() 861 f.close() 862 863 data = self._ReplaceFileData(data, query) 864 865 old_protocol_version = self.protocol_version 866 867 # If file.mock-http-headers exists, it contains the headers we 868 # should send. Read them in and parse them. 869 headers_path = file_path + '.mock-http-headers' 870 if os.path.isfile(headers_path): 871 f = open(headers_path, "r") 872 873 # "HTTP/1.1 200 OK" 874 response = f.readline() 875 http_major, http_minor, status_code = re.findall( 876 'HTTP/(\d+).(\d+) (\d+)', response)[0] 877 self.protocol_version = "HTTP/%s.%s" % (http_major, http_minor) 878 self.send_response(int(status_code)) 879 880 for line in f: 881 header_values = re.findall('(\S+):\s*(.*)', line) 882 if len(header_values) > 0: 883 # "name: value" 884 name, value = header_values[0] 885 self.send_header(name, value) 886 f.close() 887 else: 888 # Could be more generic once we support mime-type sniffing, but for 889 # now we need to set it explicitly. 890 891 range_header = self.headers.get('Range') 892 if range_header and range_header.startswith('bytes='): 893 # Note this doesn't handle all valid byte range_header values (i.e. 894 # left open ended ones), just enough for what we needed so far. 895 range_header = range_header[6:].split('-') 896 start = int(range_header[0]) 897 if range_header[1]: 898 end = int(range_header[1]) 899 else: 900 end = len(data) - 1 901 902 self.send_response(206) 903 content_range = ('bytes ' + str(start) + '-' + str(end) + '/' + 904 str(len(data))) 905 self.send_header('Content-Range', content_range) 906 data = data[start: end + 1] 907 else: 908 self.send_response(200) 909 910 self.send_header('Content-Type', self.GetMIMETypeFromName(file_path)) 911 self.send_header('Accept-Ranges', 'bytes') 912 self.send_header('Content-Length', len(data)) 913 self.send_header('ETag', '\'' + file_path + '\'') 914 self.end_headers() 915 916 if (self.command != 'HEAD'): 917 self.wfile.write(data) 918 919 self.protocol_version = old_protocol_version 920 return True 921 922 def SetCookieHandler(self): 923 """This handler just sets a cookie, for testing cookie handling.""" 924 925 if not self._ShouldHandleRequest("/set-cookie"): 926 return False 927 928 query_char = self.path.find('?') 929 if query_char != -1: 930 cookie_values = self.path[query_char + 1:].split('&') 931 else: 932 cookie_values = ("",) 933 self.send_response(200) 934 self.send_header('Content-Type', 'text/html') 935 for cookie_value in cookie_values: 936 self.send_header('Set-Cookie', '%s' % cookie_value) 937 self.end_headers() 938 for cookie_value in cookie_values: 939 self.wfile.write('%s' % cookie_value) 940 return True 941 942 def SetManyCookiesHandler(self): 943 """This handler just sets a given number of cookies, for testing handling 944 of large numbers of cookies.""" 945 946 if not self._ShouldHandleRequest("/set-many-cookies"): 947 return False 948 949 query_char = self.path.find('?') 950 if query_char != -1: 951 num_cookies = int(self.path[query_char + 1:]) 952 else: 953 num_cookies = 0 954 self.send_response(200) 955 self.send_header('', 'text/html') 956 for _i in range(0, num_cookies): 957 self.send_header('Set-Cookie', 'a=') 958 self.end_headers() 959 self.wfile.write('%d cookies were sent' % num_cookies) 960 return True 961 962 def ExpectAndSetCookieHandler(self): 963 """Expects some cookies to be sent, and if they are, sets more cookies. 964 965 The expect parameter specifies a required cookie. May be specified multiple 966 times. 967 The set parameter specifies a cookie to set if all required cookies are 968 preset. May be specified multiple times. 969 The data parameter specifies the response body data to be returned.""" 970 971 if not self._ShouldHandleRequest("/expect-and-set-cookie"): 972 return False 973 974 _, _, _, _, query, _ = urlparse.urlparse(self.path) 975 query_dict = cgi.parse_qs(query) 976 cookies = set() 977 if 'Cookie' in self.headers: 978 cookie_header = self.headers.getheader('Cookie') 979 cookies.update([s.strip() for s in cookie_header.split(';')]) 980 got_all_expected_cookies = True 981 for expected_cookie in query_dict.get('expect', []): 982 if expected_cookie not in cookies: 983 got_all_expected_cookies = False 984 self.send_response(200) 985 self.send_header('Content-Type', 'text/html') 986 if got_all_expected_cookies: 987 for cookie_value in query_dict.get('set', []): 988 self.send_header('Set-Cookie', '%s' % cookie_value) 989 self.end_headers() 990 for data_value in query_dict.get('data', []): 991 self.wfile.write(data_value) 992 return True 993 994 def SetHeaderHandler(self): 995 """This handler sets a response header. Parameters are in the 996 key%3A%20value&key2%3A%20value2 format.""" 997 998 if not self._ShouldHandleRequest("/set-header"): 999 return False 1000 1001 query_char = self.path.find('?') 1002 if query_char != -1: 1003 headers_values = self.path[query_char + 1:].split('&') 1004 else: 1005 headers_values = ("",) 1006 self.send_response(200) 1007 self.send_header('Content-Type', 'text/html') 1008 for header_value in headers_values: 1009 header_value = urllib.unquote(header_value) 1010 (key, value) = header_value.split(': ', 1) 1011 self.send_header(key, value) 1012 self.end_headers() 1013 for header_value in headers_values: 1014 self.wfile.write('%s' % header_value) 1015 return True 1016 1017 def AuthBasicHandler(self): 1018 """This handler tests 'Basic' authentication. It just sends a page with 1019 title 'user/pass' if you succeed.""" 1020 1021 if not self._ShouldHandleRequest("/auth-basic"): 1022 return False 1023 1024 username = userpass = password = b64str = "" 1025 expected_password = 'secret' 1026 realm = 'testrealm' 1027 set_cookie_if_challenged = False 1028 1029 _, _, url_path, _, query, _ = urlparse.urlparse(self.path) 1030 query_params = cgi.parse_qs(query, True) 1031 if 'set-cookie-if-challenged' in query_params: 1032 set_cookie_if_challenged = True 1033 if 'password' in query_params: 1034 expected_password = query_params['password'][0] 1035 if 'realm' in query_params: 1036 realm = query_params['realm'][0] 1037 1038 auth = self.headers.getheader('authorization') 1039 try: 1040 if not auth: 1041 raise Exception('no auth') 1042 b64str = re.findall(r'Basic (\S+)', auth)[0] 1043 userpass = base64.b64decode(b64str) 1044 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0] 1045 if password != expected_password: 1046 raise Exception('wrong password') 1047 except Exception, e: 1048 # Authentication failed. 1049 self.send_response(401) 1050 self.send_header('WWW-Authenticate', 'Basic realm="%s"' % realm) 1051 self.send_header('Content-Type', 'text/html') 1052 if set_cookie_if_challenged: 1053 self.send_header('Set-Cookie', 'got_challenged=true') 1054 self.end_headers() 1055 self.wfile.write('<html><head>') 1056 self.wfile.write('<title>Denied: %s</title>' % e) 1057 self.wfile.write('</head><body>') 1058 self.wfile.write('auth=%s<p>' % auth) 1059 self.wfile.write('b64str=%s<p>' % b64str) 1060 self.wfile.write('username: %s<p>' % username) 1061 self.wfile.write('userpass: %s<p>' % userpass) 1062 self.wfile.write('password: %s<p>' % password) 1063 self.wfile.write('You sent:<br>%s<p>' % self.headers) 1064 self.wfile.write('</body></html>') 1065 return True 1066 1067 # Authentication successful. (Return a cachable response to allow for 1068 # testing cached pages that require authentication.) 1069 old_protocol_version = self.protocol_version 1070 self.protocol_version = "HTTP/1.1" 1071 1072 if_none_match = self.headers.getheader('if-none-match') 1073 if if_none_match == "abc": 1074 self.send_response(304) 1075 self.end_headers() 1076 elif url_path.endswith(".gif"): 1077 # Using chrome/test/data/google/logo.gif as the test image 1078 test_image_path = ['google', 'logo.gif'] 1079 gif_path = os.path.join(self.server.data_dir, *test_image_path) 1080 if not os.path.isfile(gif_path): 1081 self.send_error(404) 1082 self.protocol_version = old_protocol_version 1083 return True 1084 1085 f = open(gif_path, "rb") 1086 data = f.read() 1087 f.close() 1088 1089 self.send_response(200) 1090 self.send_header('Content-Type', 'image/gif') 1091 self.send_header('Cache-control', 'max-age=60000') 1092 self.send_header('Etag', 'abc') 1093 self.end_headers() 1094 self.wfile.write(data) 1095 else: 1096 self.send_response(200) 1097 self.send_header('Content-Type', 'text/html') 1098 self.send_header('Cache-control', 'max-age=60000') 1099 self.send_header('Etag', 'abc') 1100 self.end_headers() 1101 self.wfile.write('<html><head>') 1102 self.wfile.write('<title>%s/%s</title>' % (username, password)) 1103 self.wfile.write('</head><body>') 1104 self.wfile.write('auth=%s<p>' % auth) 1105 self.wfile.write('You sent:<br>%s<p>' % self.headers) 1106 self.wfile.write('</body></html>') 1107 1108 self.protocol_version = old_protocol_version 1109 return True 1110 1111 def GetNonce(self, force_reset=False): 1112 """Returns a nonce that's stable per request path for the server's lifetime. 1113 This is a fake implementation. A real implementation would only use a given 1114 nonce a single time (hence the name n-once). However, for the purposes of 1115 unittesting, we don't care about the security of the nonce. 1116 1117 Args: 1118 force_reset: Iff set, the nonce will be changed. Useful for testing the 1119 "stale" response. 1120 """ 1121 1122 if force_reset or not self.server.nonce_time: 1123 self.server.nonce_time = time.time() 1124 return hashlib.md5('privatekey%s%d' % 1125 (self.path, self.server.nonce_time)).hexdigest() 1126 1127 def AuthDigestHandler(self): 1128 """This handler tests 'Digest' authentication. 1129 1130 It just sends a page with title 'user/pass' if you succeed. 1131 1132 A stale response is sent iff "stale" is present in the request path. 1133 """ 1134 1135 if not self._ShouldHandleRequest("/auth-digest"): 1136 return False 1137 1138 stale = 'stale' in self.path 1139 nonce = self.GetNonce(force_reset=stale) 1140 opaque = hashlib.md5('opaque').hexdigest() 1141 password = 'secret' 1142 realm = 'testrealm' 1143 1144 auth = self.headers.getheader('authorization') 1145 pairs = {} 1146 try: 1147 if not auth: 1148 raise Exception('no auth') 1149 if not auth.startswith('Digest'): 1150 raise Exception('not digest') 1151 # Pull out all the name="value" pairs as a dictionary. 1152 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth)) 1153 1154 # Make sure it's all valid. 1155 if pairs['nonce'] != nonce: 1156 raise Exception('wrong nonce') 1157 if pairs['opaque'] != opaque: 1158 raise Exception('wrong opaque') 1159 1160 # Check the 'response' value and make sure it matches our magic hash. 1161 # See http://www.ietf.org/rfc/rfc2617.txt 1162 hash_a1 = hashlib.md5( 1163 ':'.join([pairs['username'], realm, password])).hexdigest() 1164 hash_a2 = hashlib.md5(':'.join([self.command, pairs['uri']])).hexdigest() 1165 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs: 1166 response = hashlib.md5(':'.join([hash_a1, nonce, pairs['nc'], 1167 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest() 1168 else: 1169 response = hashlib.md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest() 1170 1171 if pairs['response'] != response: 1172 raise Exception('wrong password') 1173 except Exception, e: 1174 # Authentication failed. 1175 self.send_response(401) 1176 hdr = ('Digest ' 1177 'realm="%s", ' 1178 'domain="/", ' 1179 'qop="auth", ' 1180 'algorithm=MD5, ' 1181 'nonce="%s", ' 1182 'opaque="%s"') % (realm, nonce, opaque) 1183 if stale: 1184 hdr += ', stale="TRUE"' 1185 self.send_header('WWW-Authenticate', hdr) 1186 self.send_header('Content-Type', 'text/html') 1187 self.end_headers() 1188 self.wfile.write('<html><head>') 1189 self.wfile.write('<title>Denied: %s</title>' % e) 1190 self.wfile.write('</head><body>') 1191 self.wfile.write('auth=%s<p>' % auth) 1192 self.wfile.write('pairs=%s<p>' % pairs) 1193 self.wfile.write('You sent:<br>%s<p>' % self.headers) 1194 self.wfile.write('We are replying:<br>%s<p>' % hdr) 1195 self.wfile.write('</body></html>') 1196 return True 1197 1198 # Authentication successful. 1199 self.send_response(200) 1200 self.send_header('Content-Type', 'text/html') 1201 self.end_headers() 1202 self.wfile.write('<html><head>') 1203 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password)) 1204 self.wfile.write('</head><body>') 1205 self.wfile.write('auth=%s<p>' % auth) 1206 self.wfile.write('pairs=%s<p>' % pairs) 1207 self.wfile.write('</body></html>') 1208 1209 return True 1210 1211 def SlowServerHandler(self): 1212 """Wait for the user suggested time before responding. The syntax is 1213 /slow?0.5 to wait for half a second.""" 1214 1215 if not self._ShouldHandleRequest("/slow"): 1216 return False 1217 query_char = self.path.find('?') 1218 wait_sec = 1.0 1219 if query_char >= 0: 1220 try: 1221 wait_sec = int(self.path[query_char + 1:]) 1222 except ValueError: 1223 pass 1224 time.sleep(wait_sec) 1225 self.send_response(200) 1226 self.send_header('Content-Type', 'text/plain') 1227 self.end_headers() 1228 self.wfile.write("waited %d seconds" % wait_sec) 1229 return True 1230 1231 def ChunkedServerHandler(self): 1232 """Send chunked response. Allows to specify chunks parameters: 1233 - waitBeforeHeaders - ms to wait before sending headers 1234 - waitBetweenChunks - ms to wait between chunks 1235 - chunkSize - size of each chunk in bytes 1236 - chunksNumber - number of chunks 1237 Example: /chunked?waitBeforeHeaders=1000&chunkSize=5&chunksNumber=5 1238 waits one second, then sends headers and five chunks five bytes each.""" 1239 1240 if not self._ShouldHandleRequest("/chunked"): 1241 return False 1242 query_char = self.path.find('?') 1243 chunkedSettings = {'waitBeforeHeaders' : 0, 1244 'waitBetweenChunks' : 0, 1245 'chunkSize' : 5, 1246 'chunksNumber' : 5} 1247 if query_char >= 0: 1248 params = self.path[query_char + 1:].split('&') 1249 for param in params: 1250 keyValue = param.split('=') 1251 if len(keyValue) == 2: 1252 try: 1253 chunkedSettings[keyValue[0]] = int(keyValue[1]) 1254 except ValueError: 1255 pass 1256 time.sleep(0.001 * chunkedSettings['waitBeforeHeaders']) 1257 self.protocol_version = 'HTTP/1.1' # Needed for chunked encoding 1258 self.send_response(200) 1259 self.send_header('Content-Type', 'text/plain') 1260 self.send_header('Connection', 'close') 1261 self.send_header('Transfer-Encoding', 'chunked') 1262 self.end_headers() 1263 # Chunked encoding: sending all chunks, then final zero-length chunk and 1264 # then final CRLF. 1265 for i in range(0, chunkedSettings['chunksNumber']): 1266 if i > 0: 1267 time.sleep(0.001 * chunkedSettings['waitBetweenChunks']) 1268 self.sendChunkHelp('*' * chunkedSettings['chunkSize']) 1269 self.wfile.flush() # Keep in mind that we start flushing only after 1kb. 1270 self.sendChunkHelp('') 1271 return True 1272 1273 def ContentTypeHandler(self): 1274 """Returns a string of html with the given content type. E.g., 1275 /contenttype?text/css returns an html file with the Content-Type 1276 header set to text/css.""" 1277 1278 if not self._ShouldHandleRequest("/contenttype"): 1279 return False 1280 query_char = self.path.find('?') 1281 content_type = self.path[query_char + 1:].strip() 1282 if not content_type: 1283 content_type = 'text/html' 1284 self.send_response(200) 1285 self.send_header('Content-Type', content_type) 1286 self.end_headers() 1287 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n") 1288 return True 1289 1290 def NoContentHandler(self): 1291 """Returns a 204 No Content response.""" 1292 1293 if not self._ShouldHandleRequest("/nocontent"): 1294 return False 1295 self.send_response(204) 1296 self.end_headers() 1297 return True 1298 1299 def ServerRedirectHandler(self): 1300 """Sends a server redirect to the given URL. The syntax is 1301 '/server-redirect?http://foo.bar/asdf' to redirect to 1302 'http://foo.bar/asdf'""" 1303 1304 test_name = "/server-redirect" 1305 if not self._ShouldHandleRequest(test_name): 1306 return False 1307 1308 query_char = self.path.find('?') 1309 if query_char < 0 or len(self.path) <= query_char + 1: 1310 self.sendRedirectHelp(test_name) 1311 return True 1312 dest = self.path[query_char + 1:] 1313 1314 self.send_response(301) # moved permanently 1315 self.send_header('Location', dest) 1316 self.send_header('Content-Type', 'text/html') 1317 self.end_headers() 1318 self.wfile.write('<html><head>') 1319 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest) 1320 1321 return True 1322 1323 def ClientRedirectHandler(self): 1324 """Sends a client redirect to the given URL. The syntax is 1325 '/client-redirect?http://foo.bar/asdf' to redirect to 1326 'http://foo.bar/asdf'""" 1327 1328 test_name = "/client-redirect" 1329 if not self._ShouldHandleRequest(test_name): 1330 return False 1331 1332 query_char = self.path.find('?') 1333 if query_char < 0 or len(self.path) <= query_char + 1: 1334 self.sendRedirectHelp(test_name) 1335 return True 1336 dest = self.path[query_char + 1:] 1337 1338 self.send_response(200) 1339 self.send_header('Content-Type', 'text/html') 1340 self.end_headers() 1341 self.wfile.write('<html><head>') 1342 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest) 1343 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest) 1344 1345 return True 1346 1347 def MultipartHandler(self): 1348 """Send a multipart response (10 text/html pages).""" 1349 1350 test_name = '/multipart' 1351 if not self._ShouldHandleRequest(test_name): 1352 return False 1353 1354 num_frames = 10 1355 bound = '12345' 1356 self.send_response(200) 1357 self.send_header('Content-Type', 1358 'multipart/x-mixed-replace;boundary=' + bound) 1359 self.end_headers() 1360 1361 for i in xrange(num_frames): 1362 self.wfile.write('--' + bound + '\r\n') 1363 self.wfile.write('Content-Type: text/html\r\n\r\n') 1364 self.wfile.write('<title>page ' + str(i) + '</title>') 1365 self.wfile.write('page ' + str(i)) 1366 1367 self.wfile.write('--' + bound + '--') 1368 return True 1369 1370 def MultipartSlowHandler(self): 1371 """Send a multipart response (3 text/html pages) with a slight delay 1372 between each page. This is similar to how some pages show status using 1373 multipart.""" 1374 1375 test_name = '/multipart-slow' 1376 if not self._ShouldHandleRequest(test_name): 1377 return False 1378 1379 num_frames = 3 1380 bound = '12345' 1381 self.send_response(200) 1382 self.send_header('Content-Type', 1383 'multipart/x-mixed-replace;boundary=' + bound) 1384 self.end_headers() 1385 1386 for i in xrange(num_frames): 1387 self.wfile.write('--' + bound + '\r\n') 1388 self.wfile.write('Content-Type: text/html\r\n\r\n') 1389 time.sleep(0.25) 1390 if i == 2: 1391 self.wfile.write('<title>PASS</title>') 1392 else: 1393 self.wfile.write('<title>page ' + str(i) + '</title>') 1394 self.wfile.write('page ' + str(i) + '<!-- ' + ('x' * 2048) + '-->') 1395 1396 self.wfile.write('--' + bound + '--') 1397 return True 1398 1399 def GetSSLSessionCacheHandler(self): 1400 """Send a reply containing a log of the session cache operations.""" 1401 1402 if not self._ShouldHandleRequest('/ssl-session-cache'): 1403 return False 1404 1405 self.send_response(200) 1406 self.send_header('Content-Type', 'text/plain') 1407 self.end_headers() 1408 try: 1409 for (action, sessionID) in self.server.session_cache.log: 1410 self.wfile.write('%s\t%s\n' % (action, sessionID.encode('hex'))) 1411 except AttributeError: 1412 self.wfile.write('Pass --https-record-resume in order to use' + 1413 ' this request') 1414 return True 1415 1416 def SSLManySmallRecords(self): 1417 """Sends a reply consisting of a variety of small writes. These will be 1418 translated into a series of small SSL records when used over an HTTPS 1419 server.""" 1420 1421 if not self._ShouldHandleRequest('/ssl-many-small-records'): 1422 return False 1423 1424 self.send_response(200) 1425 self.send_header('Content-Type', 'text/plain') 1426 self.end_headers() 1427 1428 # Write ~26K of data, in 1350 byte chunks 1429 for i in xrange(20): 1430 self.wfile.write('*' * 1350) 1431 self.wfile.flush() 1432 return True 1433 1434 def GetChannelID(self): 1435 """Send a reply containing the hashed ChannelID that the client provided.""" 1436 1437 if not self._ShouldHandleRequest('/channel-id'): 1438 return False 1439 1440 self.send_response(200) 1441 self.send_header('Content-Type', 'text/plain') 1442 self.end_headers() 1443 channel_id = self.server.tlsConnection.channel_id.tostring() 1444 self.wfile.write(hashlib.sha256(channel_id).digest().encode('base64')) 1445 return True 1446 1447 def CloseSocketHandler(self): 1448 """Closes the socket without sending anything.""" 1449 1450 if not self._ShouldHandleRequest('/close-socket'): 1451 return False 1452 1453 self.wfile.close() 1454 return True 1455 1456 def RangeResetHandler(self): 1457 """Send data broken up by connection resets every N (default 4K) bytes. 1458 Support range requests. If the data requested doesn't straddle a reset 1459 boundary, it will all be sent. Used for testing resuming downloads.""" 1460 1461 def DataForRange(start, end): 1462 """Data to be provided for a particular range of bytes.""" 1463 # Offset and scale to avoid too obvious (and hence potentially 1464 # collidable) data. 1465 return ''.join([chr(y % 256) 1466 for y in range(start * 2 + 15, end * 2 + 15, 2)]) 1467 1468 if not self._ShouldHandleRequest('/rangereset'): 1469 return False 1470 1471 _, _, url_path, _, query, _ = urlparse.urlparse(self.path) 1472 1473 # Defaults 1474 size = 8000 1475 # Note that the rst is sent just before sending the rst_boundary byte. 1476 rst_boundary = 4000 1477 respond_to_range = True 1478 hold_for_signal = False 1479 rst_limit = -1 1480 token = 'DEFAULT' 1481 fail_precondition = 0 1482 send_verifiers = True 1483 1484 # Parse the query 1485 qdict = urlparse.parse_qs(query, True) 1486 if 'size' in qdict: 1487 size = int(qdict['size'][0]) 1488 if 'rst_boundary' in qdict: 1489 rst_boundary = int(qdict['rst_boundary'][0]) 1490 if 'token' in qdict: 1491 # Identifying token for stateful tests. 1492 token = qdict['token'][0] 1493 if 'rst_limit' in qdict: 1494 # Max number of rsts for a given token. 1495 rst_limit = int(qdict['rst_limit'][0]) 1496 if 'bounce_range' in qdict: 1497 respond_to_range = False 1498 if 'hold' in qdict: 1499 # Note that hold_for_signal will not work with null range requests; 1500 # see TODO below. 1501 hold_for_signal = True 1502 if 'no_verifiers' in qdict: 1503 send_verifiers = False 1504 if 'fail_precondition' in qdict: 1505 fail_precondition = int(qdict['fail_precondition'][0]) 1506 1507 # Record already set information, or set it. 1508 rst_limit = TestPageHandler.rst_limits.setdefault(token, rst_limit) 1509 if rst_limit != 0: 1510 TestPageHandler.rst_limits[token] -= 1 1511 fail_precondition = TestPageHandler.fail_precondition.setdefault( 1512 token, fail_precondition) 1513 if fail_precondition != 0: 1514 TestPageHandler.fail_precondition[token] -= 1 1515 1516 first_byte = 0 1517 last_byte = size - 1 1518 1519 # Does that define what we want to return, or do we need to apply 1520 # a range? 1521 range_response = False 1522 range_header = self.headers.getheader('range') 1523 if range_header and respond_to_range: 1524 mo = re.match("bytes=(\d*)-(\d*)", range_header) 1525 if mo.group(1): 1526 first_byte = int(mo.group(1)) 1527 if mo.group(2): 1528 last_byte = int(mo.group(2)) 1529 if last_byte > size - 1: 1530 last_byte = size - 1 1531 range_response = True 1532 if last_byte < first_byte: 1533 return False 1534 1535 if (fail_precondition and 1536 (self.headers.getheader('If-Modified-Since') or 1537 self.headers.getheader('If-Match'))): 1538 self.send_response(412) 1539 self.end_headers() 1540 return True 1541 1542 if range_response: 1543 self.send_response(206) 1544 self.send_header('Content-Range', 1545 'bytes %d-%d/%d' % (first_byte, last_byte, size)) 1546 else: 1547 self.send_response(200) 1548 self.send_header('Content-Type', 'application/octet-stream') 1549 self.send_header('Content-Length', last_byte - first_byte + 1) 1550 if send_verifiers: 1551 self.send_header('Etag', '"XYZZY"') 1552 self.send_header('Last-Modified', 'Tue, 19 Feb 2013 14:32 EST') 1553 self.end_headers() 1554 1555 if hold_for_signal: 1556 # TODO(rdsmith/phajdan.jr): http://crbug.com/169519: Without writing 1557 # a single byte, the self.server.handle_request() below hangs 1558 # without processing new incoming requests. 1559 self.wfile.write(DataForRange(first_byte, first_byte + 1)) 1560 first_byte = first_byte + 1 1561 # handle requests until one of them clears this flag. 1562 self.server.wait_for_download = True 1563 while self.server.wait_for_download: 1564 self.server.handle_request() 1565 1566 possible_rst = ((first_byte / rst_boundary) + 1) * rst_boundary 1567 if possible_rst >= last_byte or rst_limit == 0: 1568 # No RST has been requested in this range, so we don't need to 1569 # do anything fancy; just write the data and let the python 1570 # infrastructure close the connection. 1571 self.wfile.write(DataForRange(first_byte, last_byte + 1)) 1572 self.wfile.flush() 1573 return True 1574 1575 # We're resetting the connection part way in; go to the RST 1576 # boundary and then send an RST. 1577 # Because socket semantics do not guarantee that all the data will be 1578 # sent when using the linger semantics to hard close a socket, 1579 # we send the data and then wait for our peer to release us 1580 # before sending the reset. 1581 data = DataForRange(first_byte, possible_rst) 1582 self.wfile.write(data) 1583 self.wfile.flush() 1584 self.server.wait_for_download = True 1585 while self.server.wait_for_download: 1586 self.server.handle_request() 1587 l_onoff = 1 # Linger is active. 1588 l_linger = 0 # Seconds to linger for. 1589 self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, 1590 struct.pack('ii', l_onoff, l_linger)) 1591 1592 # Close all duplicates of the underlying socket to force the RST. 1593 self.wfile.close() 1594 self.rfile.close() 1595 self.connection.close() 1596 1597 return True 1598 1599 def DefaultResponseHandler(self): 1600 """This is the catch-all response handler for requests that aren't handled 1601 by one of the special handlers above. 1602 Note that we specify the content-length as without it the https connection 1603 is not closed properly (and the browser keeps expecting data).""" 1604 1605 contents = "Default response given for path: " + self.path 1606 self.send_response(200) 1607 self.send_header('Content-Type', 'text/html') 1608 self.send_header('Content-Length', len(contents)) 1609 self.end_headers() 1610 if (self.command != 'HEAD'): 1611 self.wfile.write(contents) 1612 return True 1613 1614 def RedirectConnectHandler(self): 1615 """Sends a redirect to the CONNECT request for www.redirect.com. This 1616 response is not specified by the RFC, so the browser should not follow 1617 the redirect.""" 1618 1619 if (self.path.find("www.redirect.com") < 0): 1620 return False 1621 1622 dest = "http://www.destination.com/foo.js" 1623 1624 self.send_response(302) # moved temporarily 1625 self.send_header('Location', dest) 1626 self.send_header('Connection', 'close') 1627 self.end_headers() 1628 return True 1629 1630 def ServerAuthConnectHandler(self): 1631 """Sends a 401 to the CONNECT request for www.server-auth.com. This 1632 response doesn't make sense because the proxy server cannot request 1633 server authentication.""" 1634 1635 if (self.path.find("www.server-auth.com") < 0): 1636 return False 1637 1638 challenge = 'Basic realm="WallyWorld"' 1639 1640 self.send_response(401) # unauthorized 1641 self.send_header('WWW-Authenticate', challenge) 1642 self.send_header('Connection', 'close') 1643 self.end_headers() 1644 return True 1645 1646 def DefaultConnectResponseHandler(self): 1647 """This is the catch-all response handler for CONNECT requests that aren't 1648 handled by one of the special handlers above. Real Web servers respond 1649 with 400 to CONNECT requests.""" 1650 1651 contents = "Your client has issued a malformed or illegal request." 1652 self.send_response(400) # bad request 1653 self.send_header('Content-Type', 'text/html') 1654 self.send_header('Content-Length', len(contents)) 1655 self.end_headers() 1656 self.wfile.write(contents) 1657 return True 1658 1659 # called by the redirect handling function when there is no parameter 1660 def sendRedirectHelp(self, redirect_name): 1661 self.send_response(200) 1662 self.send_header('Content-Type', 'text/html') 1663 self.end_headers() 1664 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>') 1665 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name) 1666 self.wfile.write('</body></html>') 1667 1668 # called by chunked handling function 1669 def sendChunkHelp(self, chunk): 1670 # Each chunk consists of: chunk size (hex), CRLF, chunk body, CRLF 1671 self.wfile.write('%X\r\n' % len(chunk)) 1672 self.wfile.write(chunk) 1673 self.wfile.write('\r\n') 1674 1675 1676class OCSPHandler(testserver_base.BasePageHandler): 1677 def __init__(self, request, client_address, socket_server): 1678 handlers = [self.OCSPResponse] 1679 self.ocsp_response = socket_server.ocsp_response 1680 testserver_base.BasePageHandler.__init__(self, request, client_address, 1681 socket_server, [], handlers, [], 1682 handlers, []) 1683 1684 def OCSPResponse(self): 1685 self.send_response(200) 1686 self.send_header('Content-Type', 'application/ocsp-response') 1687 self.send_header('Content-Length', str(len(self.ocsp_response))) 1688 self.end_headers() 1689 1690 self.wfile.write(self.ocsp_response) 1691 1692 1693class TCPEchoHandler(SocketServer.BaseRequestHandler): 1694 """The RequestHandler class for TCP echo server. 1695 1696 It is instantiated once per connection to the server, and overrides the 1697 handle() method to implement communication to the client. 1698 """ 1699 1700 def handle(self): 1701 """Handles the request from the client and constructs a response.""" 1702 1703 data = self.request.recv(65536).strip() 1704 # Verify the "echo request" message received from the client. Send back 1705 # "echo response" message if "echo request" message is valid. 1706 try: 1707 return_data = echo_message.GetEchoResponseData(data) 1708 if not return_data: 1709 return 1710 except ValueError: 1711 return 1712 1713 self.request.send(return_data) 1714 1715 1716class UDPEchoHandler(SocketServer.BaseRequestHandler): 1717 """The RequestHandler class for UDP echo server. 1718 1719 It is instantiated once per connection to the server, and overrides the 1720 handle() method to implement communication to the client. 1721 """ 1722 1723 def handle(self): 1724 """Handles the request from the client and constructs a response.""" 1725 1726 data = self.request[0].strip() 1727 request_socket = self.request[1] 1728 # Verify the "echo request" message received from the client. Send back 1729 # "echo response" message if "echo request" message is valid. 1730 try: 1731 return_data = echo_message.GetEchoResponseData(data) 1732 if not return_data: 1733 return 1734 except ValueError: 1735 return 1736 request_socket.sendto(return_data, self.client_address) 1737 1738 1739class BasicAuthProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 1740 """A request handler that behaves as a proxy server which requires 1741 basic authentication. Only CONNECT, GET and HEAD is supported for now. 1742 """ 1743 1744 _AUTH_CREDENTIAL = 'Basic Zm9vOmJhcg==' # foo:bar 1745 1746 def parse_request(self): 1747 """Overrides parse_request to check credential.""" 1748 1749 if not BaseHTTPServer.BaseHTTPRequestHandler.parse_request(self): 1750 return False 1751 1752 auth = self.headers.getheader('Proxy-Authorization') 1753 if auth != self._AUTH_CREDENTIAL: 1754 self.send_response(407) 1755 self.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"') 1756 self.end_headers() 1757 return False 1758 1759 return True 1760 1761 def _start_read_write(self, sock): 1762 sock.setblocking(0) 1763 self.request.setblocking(0) 1764 rlist = [self.request, sock] 1765 while True: 1766 ready_sockets, _unused, errors = select.select(rlist, [], []) 1767 if errors: 1768 self.send_response(500) 1769 self.end_headers() 1770 return 1771 for s in ready_sockets: 1772 received = s.recv(1024) 1773 if len(received) == 0: 1774 return 1775 if s == self.request: 1776 other = sock 1777 else: 1778 other = self.request 1779 other.send(received) 1780 1781 def _do_common_method(self): 1782 url = urlparse.urlparse(self.path) 1783 port = url.port 1784 if not port: 1785 if url.scheme == 'http': 1786 port = 80 1787 elif url.scheme == 'https': 1788 port = 443 1789 if not url.hostname or not port: 1790 self.send_response(400) 1791 self.end_headers() 1792 return 1793 1794 if len(url.path) == 0: 1795 path = '/' 1796 else: 1797 path = url.path 1798 if len(url.query) > 0: 1799 path = '%s?%s' % (url.path, url.query) 1800 1801 sock = None 1802 try: 1803 sock = socket.create_connection((url.hostname, port)) 1804 sock.send('%s %s %s\r\n' % ( 1805 self.command, path, self.protocol_version)) 1806 for header in self.headers.headers: 1807 header = header.strip() 1808 if (header.lower().startswith('connection') or 1809 header.lower().startswith('proxy')): 1810 continue 1811 sock.send('%s\r\n' % header) 1812 sock.send('\r\n') 1813 self._start_read_write(sock) 1814 except Exception: 1815 self.send_response(500) 1816 self.end_headers() 1817 finally: 1818 if sock is not None: 1819 sock.close() 1820 1821 def do_CONNECT(self): 1822 try: 1823 pos = self.path.rfind(':') 1824 host = self.path[:pos] 1825 port = int(self.path[pos+1:]) 1826 except Exception: 1827 self.send_response(400) 1828 self.end_headers() 1829 1830 try: 1831 sock = socket.create_connection((host, port)) 1832 self.send_response(200, 'Connection established') 1833 self.end_headers() 1834 self._start_read_write(sock) 1835 except Exception: 1836 self.send_response(500) 1837 self.end_headers() 1838 finally: 1839 sock.close() 1840 1841 def do_GET(self): 1842 self._do_common_method() 1843 1844 def do_HEAD(self): 1845 self._do_common_method() 1846 1847 1848class ServerRunner(testserver_base.TestServerRunner): 1849 """TestServerRunner for the net test servers.""" 1850 1851 def __init__(self): 1852 super(ServerRunner, self).__init__() 1853 self.__ocsp_server = None 1854 1855 def __make_data_dir(self): 1856 if self.options.data_dir: 1857 if not os.path.isdir(self.options.data_dir): 1858 raise testserver_base.OptionError('specified data dir not found: ' + 1859 self.options.data_dir + ' exiting...') 1860 my_data_dir = self.options.data_dir 1861 else: 1862 # Create the default path to our data dir, relative to the exe dir. 1863 my_data_dir = os.path.join(BASE_DIR, "..", "..", "..", "..", 1864 "test", "data") 1865 1866 #TODO(ibrar): Must use Find* funtion defined in google\tools 1867 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data") 1868 1869 return my_data_dir 1870 1871 def create_server(self, server_data): 1872 port = self.options.port 1873 host = self.options.host 1874 1875 if self.options.server_type == SERVER_HTTP: 1876 if self.options.https: 1877 pem_cert_and_key = None 1878 if self.options.cert_and_key_file: 1879 if not os.path.isfile(self.options.cert_and_key_file): 1880 raise testserver_base.OptionError( 1881 'specified server cert file not found: ' + 1882 self.options.cert_and_key_file + ' exiting...') 1883 pem_cert_and_key = file(self.options.cert_and_key_file, 'r').read() 1884 else: 1885 # generate a new certificate and run an OCSP server for it. 1886 self.__ocsp_server = OCSPServer((host, 0), OCSPHandler) 1887 print ('OCSP server started on %s:%d...' % 1888 (host, self.__ocsp_server.server_port)) 1889 1890 ocsp_der = None 1891 ocsp_state = None 1892 1893 if self.options.ocsp == 'ok': 1894 ocsp_state = minica.OCSP_STATE_GOOD 1895 elif self.options.ocsp == 'revoked': 1896 ocsp_state = minica.OCSP_STATE_REVOKED 1897 elif self.options.ocsp == 'invalid': 1898 ocsp_state = minica.OCSP_STATE_INVALID 1899 elif self.options.ocsp == 'unauthorized': 1900 ocsp_state = minica.OCSP_STATE_UNAUTHORIZED 1901 elif self.options.ocsp == 'unknown': 1902 ocsp_state = minica.OCSP_STATE_UNKNOWN 1903 else: 1904 raise testserver_base.OptionError('unknown OCSP status: ' + 1905 self.options.ocsp_status) 1906 1907 (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP( 1908 subject = "127.0.0.1", 1909 ocsp_url = ("http://%s:%d/ocsp" % 1910 (host, self.__ocsp_server.server_port)), 1911 ocsp_state = ocsp_state) 1912 1913 self.__ocsp_server.ocsp_response = ocsp_der 1914 1915 for ca_cert in self.options.ssl_client_ca: 1916 if not os.path.isfile(ca_cert): 1917 raise testserver_base.OptionError( 1918 'specified trusted client CA file not found: ' + ca_cert + 1919 ' exiting...') 1920 server = HTTPSServer((host, port), TestPageHandler, pem_cert_and_key, 1921 self.options.ssl_client_auth, 1922 self.options.ssl_client_ca, 1923 self.options.ssl_bulk_cipher, 1924 self.options.record_resume, 1925 self.options.tls_intolerant) 1926 print 'HTTPS server started on %s:%d...' % (host, server.server_port) 1927 else: 1928 server = HTTPServer((host, port), TestPageHandler) 1929 print 'HTTP server started on %s:%d...' % (host, server.server_port) 1930 1931 server.data_dir = self.__make_data_dir() 1932 server.file_root_url = self.options.file_root_url 1933 server_data['port'] = server.server_port 1934 elif self.options.server_type == SERVER_WEBSOCKET: 1935 # Launch pywebsocket via WebSocketServer. 1936 logger = logging.getLogger() 1937 logger.addHandler(logging.StreamHandler()) 1938 # TODO(toyoshim): Remove following os.chdir. Currently this operation 1939 # is required to work correctly. It should be fixed from pywebsocket side. 1940 os.chdir(self.__make_data_dir()) 1941 websocket_options = WebSocketOptions(host, port, '.') 1942 if self.options.cert_and_key_file: 1943 websocket_options.use_tls = True 1944 websocket_options.private_key = self.options.cert_and_key_file 1945 websocket_options.certificate = self.options.cert_and_key_file 1946 if self.options.ssl_client_auth: 1947 websocket_options.tls_client_auth = True 1948 if len(self.options.ssl_client_ca) != 1: 1949 raise testserver_base.OptionError( 1950 'one trusted client CA file should be specified') 1951 if not os.path.isfile(self.options.ssl_client_ca[0]): 1952 raise testserver_base.OptionError( 1953 'specified trusted client CA file not found: ' + 1954 self.options.ssl_client_ca[0] + ' exiting...') 1955 websocket_options.tls_client_ca = self.options.ssl_client_ca[0] 1956 server = WebSocketServer(websocket_options) 1957 print 'WebSocket server started on %s:%d...' % (host, server.server_port) 1958 server_data['port'] = server.server_port 1959 elif self.options.server_type == SERVER_TCP_ECHO: 1960 # Used for generating the key (randomly) that encodes the "echo request" 1961 # message. 1962 random.seed() 1963 server = TCPEchoServer((host, port), TCPEchoHandler) 1964 print 'Echo TCP server started on port %d...' % server.server_port 1965 server_data['port'] = server.server_port 1966 elif self.options.server_type == SERVER_UDP_ECHO: 1967 # Used for generating the key (randomly) that encodes the "echo request" 1968 # message. 1969 random.seed() 1970 server = UDPEchoServer((host, port), UDPEchoHandler) 1971 print 'Echo UDP server started on port %d...' % server.server_port 1972 server_data['port'] = server.server_port 1973 elif self.options.server_type == SERVER_BASIC_AUTH_PROXY: 1974 server = HTTPServer((host, port), BasicAuthProxyRequestHandler) 1975 print 'BasicAuthProxy server started on port %d...' % server.server_port 1976 server_data['port'] = server.server_port 1977 elif self.options.server_type == SERVER_FTP: 1978 my_data_dir = self.__make_data_dir() 1979 1980 # Instantiate a dummy authorizer for managing 'virtual' users 1981 authorizer = pyftpdlib.ftpserver.DummyAuthorizer() 1982 1983 # Define a new user having full r/w permissions and a read-only 1984 # anonymous user 1985 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw') 1986 1987 authorizer.add_anonymous(my_data_dir) 1988 1989 # Instantiate FTP handler class 1990 ftp_handler = pyftpdlib.ftpserver.FTPHandler 1991 ftp_handler.authorizer = authorizer 1992 1993 # Define a customized banner (string returned when client connects) 1994 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." % 1995 pyftpdlib.ftpserver.__ver__) 1996 1997 # Instantiate FTP server class and listen to address:port 1998 server = pyftpdlib.ftpserver.FTPServer((host, port), ftp_handler) 1999 server_data['port'] = server.socket.getsockname()[1] 2000 print 'FTP server started on port %d...' % server_data['port'] 2001 else: 2002 raise testserver_base.OptionError('unknown server type' + 2003 self.options.server_type) 2004 2005 return server 2006 2007 def run_server(self): 2008 if self.__ocsp_server: 2009 self.__ocsp_server.serve_forever_on_thread() 2010 2011 testserver_base.TestServerRunner.run_server(self) 2012 2013 if self.__ocsp_server: 2014 self.__ocsp_server.stop_serving() 2015 2016 def add_options(self): 2017 testserver_base.TestServerRunner.add_options(self) 2018 self.option_parser.add_option('-f', '--ftp', action='store_const', 2019 const=SERVER_FTP, default=SERVER_HTTP, 2020 dest='server_type', 2021 help='start up an FTP server.') 2022 self.option_parser.add_option('--tcp-echo', action='store_const', 2023 const=SERVER_TCP_ECHO, default=SERVER_HTTP, 2024 dest='server_type', 2025 help='start up a tcp echo server.') 2026 self.option_parser.add_option('--udp-echo', action='store_const', 2027 const=SERVER_UDP_ECHO, default=SERVER_HTTP, 2028 dest='server_type', 2029 help='start up a udp echo server.') 2030 self.option_parser.add_option('--basic-auth-proxy', action='store_const', 2031 const=SERVER_BASIC_AUTH_PROXY, 2032 default=SERVER_HTTP, dest='server_type', 2033 help='start up a proxy server which requires ' 2034 'basic authentication.') 2035 self.option_parser.add_option('--websocket', action='store_const', 2036 const=SERVER_WEBSOCKET, default=SERVER_HTTP, 2037 dest='server_type', 2038 help='start up a WebSocket server.') 2039 self.option_parser.add_option('--https', action='store_true', 2040 dest='https', help='Specify that https ' 2041 'should be used.') 2042 self.option_parser.add_option('--cert-and-key-file', 2043 dest='cert_and_key_file', help='specify the ' 2044 'path to the file containing the certificate ' 2045 'and private key for the server in PEM ' 2046 'format') 2047 self.option_parser.add_option('--ocsp', dest='ocsp', default='ok', 2048 help='The type of OCSP response generated ' 2049 'for the automatically generated ' 2050 'certificate. One of [ok,revoked,invalid]') 2051 self.option_parser.add_option('--tls-intolerant', dest='tls_intolerant', 2052 default='0', type='int', 2053 help='If nonzero, certain TLS connections ' 2054 'will be aborted in order to test version ' 2055 'fallback. 1 means all TLS versions will be ' 2056 'aborted. 2 means TLS 1.1 or higher will be ' 2057 'aborted. 3 means TLS 1.2 or higher will be ' 2058 'aborted.') 2059 self.option_parser.add_option('--https-record-resume', 2060 dest='record_resume', const=True, 2061 default=False, action='store_const', 2062 help='Record resumption cache events rather ' 2063 'than resuming as normal. Allows the use of ' 2064 'the /ssl-session-cache request') 2065 self.option_parser.add_option('--ssl-client-auth', action='store_true', 2066 help='Require SSL client auth on every ' 2067 'connection.') 2068 self.option_parser.add_option('--ssl-client-ca', action='append', 2069 default=[], help='Specify that the client ' 2070 'certificate request should include the CA ' 2071 'named in the subject of the DER-encoded ' 2072 'certificate contained in the specified ' 2073 'file. This option may appear multiple ' 2074 'times, indicating multiple CA names should ' 2075 'be sent in the request.') 2076 self.option_parser.add_option('--ssl-bulk-cipher', action='append', 2077 help='Specify the bulk encryption ' 2078 'algorithm(s) that will be accepted by the ' 2079 'SSL server. Valid values are "aes256", ' 2080 '"aes128", "3des", "rc4". If omitted, all ' 2081 'algorithms will be used. This option may ' 2082 'appear multiple times, indicating ' 2083 'multiple algorithms should be enabled.'); 2084 self.option_parser.add_option('--file-root-url', default='/files/', 2085 help='Specify a root URL for files served.') 2086 2087 2088if __name__ == '__main__': 2089 sys.exit(ServerRunner().main()) 2090