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