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