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