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