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