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