testserver.py revision 513209b27ff55e2841eac0e4120199c23acce758
1#!/usr/bin/python2.4 2# Copyright (c) 2006-2010 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 server used for testing Chrome. 7 8It supports several test URLs, as specified by the handlers in TestPageHandler. 9It defaults to living on localhost:8888. 10It can use https if you specify the flag --https=CERT where CERT is the path 11to a pem file containing the certificate and private key that should be used. 12""" 13 14import base64 15import BaseHTTPServer 16import cgi 17import optparse 18import os 19import re 20import shutil 21import SocketServer 22import sys 23import time 24import urlparse 25import warnings 26 27# Ignore deprecation warnings, they make our output more cluttered. 28warnings.filterwarnings("ignore", category=DeprecationWarning) 29 30import pyftpdlib.ftpserver 31import tlslite 32import tlslite.api 33 34try: 35 import hashlib 36 _new_md5 = hashlib.md5 37except ImportError: 38 import md5 39 _new_md5 = md5.new 40 41if sys.platform == 'win32': 42 import msvcrt 43 44SERVER_HTTP = 0 45SERVER_FTP = 1 46 47debug_output = sys.stderr 48def debug(str): 49 debug_output.write(str + "\n") 50 debug_output.flush() 51 52class StoppableHTTPServer(BaseHTTPServer.HTTPServer): 53 """This is a specialization of of BaseHTTPServer to allow it 54 to be exited cleanly (by setting its "stop" member to True).""" 55 56 def serve_forever(self): 57 self.stop = False 58 self.nonce_time = None 59 while not self.stop: 60 self.handle_request() 61 self.socket.close() 62 63class HTTPSServer(tlslite.api.TLSSocketServerMixIn, StoppableHTTPServer): 64 """This is a specialization of StoppableHTTPerver that add https support.""" 65 66 def __init__(self, server_address, request_hander_class, cert_path, 67 ssl_client_auth, ssl_client_cas, ssl_bulk_ciphers): 68 s = open(cert_path).read() 69 x509 = tlslite.api.X509() 70 x509.parse(s) 71 self.cert_chain = tlslite.api.X509CertChain([x509]) 72 s = open(cert_path).read() 73 self.private_key = tlslite.api.parsePEMKey(s, private=True) 74 self.ssl_client_auth = ssl_client_auth 75 self.ssl_client_cas = [] 76 for ca_file in ssl_client_cas: 77 s = open(ca_file).read() 78 x509 = tlslite.api.X509() 79 x509.parse(s) 80 self.ssl_client_cas.append(x509.subject) 81 self.ssl_handshake_settings = tlslite.api.HandshakeSettings() 82 if ssl_bulk_ciphers is not None: 83 self.ssl_handshake_settings.cipherNames = ssl_bulk_ciphers 84 85 self.session_cache = tlslite.api.SessionCache() 86 StoppableHTTPServer.__init__(self, server_address, request_hander_class) 87 88 def handshake(self, tlsConnection): 89 """Creates the SSL connection.""" 90 try: 91 tlsConnection.handshakeServer(certChain=self.cert_chain, 92 privateKey=self.private_key, 93 sessionCache=self.session_cache, 94 reqCert=self.ssl_client_auth, 95 settings=self.ssl_handshake_settings, 96 reqCAs=self.ssl_client_cas) 97 tlsConnection.ignoreAbruptClose = True 98 return True 99 except tlslite.api.TLSAbruptCloseError: 100 # Ignore abrupt close. 101 return True 102 except tlslite.api.TLSError, error: 103 print "Handshake failure:", str(error) 104 return False 105 106class TestPageHandler(BaseHTTPServer.BaseHTTPRequestHandler): 107 108 def __init__(self, request, client_address, socket_server): 109 self._connect_handlers = [ 110 self.RedirectConnectHandler, 111 self.ServerAuthConnectHandler, 112 self.DefaultConnectResponseHandler] 113 self._get_handlers = [ 114 self.NoCacheMaxAgeTimeHandler, 115 self.NoCacheTimeHandler, 116 self.CacheTimeHandler, 117 self.CacheExpiresHandler, 118 self.CacheProxyRevalidateHandler, 119 self.CachePrivateHandler, 120 self.CachePublicHandler, 121 self.CacheSMaxAgeHandler, 122 self.CacheMustRevalidateHandler, 123 self.CacheMustRevalidateMaxAgeHandler, 124 self.CacheNoStoreHandler, 125 self.CacheNoStoreMaxAgeHandler, 126 self.CacheNoTransformHandler, 127 self.DownloadHandler, 128 self.DownloadFinishHandler, 129 self.EchoHeader, 130 self.EchoHeaderOverride, 131 self.EchoAllHandler, 132 self.FileHandler, 133 self.RealFileWithCommonHeaderHandler, 134 self.RealBZ2FileWithCommonHeaderHandler, 135 self.SetCookieHandler, 136 self.AuthBasicHandler, 137 self.AuthDigestHandler, 138 self.SlowServerHandler, 139 self.ContentTypeHandler, 140 self.ServerRedirectHandler, 141 self.ClientRedirectHandler, 142 self.ChromiumSyncTimeHandler, 143 self.MultipartHandler, 144 self.DefaultResponseHandler] 145 self._post_handlers = [ 146 self.EchoTitleHandler, 147 self.EchoAllHandler, 148 self.ChromiumSyncCommandHandler, 149 self.EchoHandler] + self._get_handlers 150 self._put_handlers = [ 151 self.EchoTitleHandler, 152 self.EchoAllHandler, 153 self.EchoHandler] + self._get_handlers 154 155 self._mime_types = { 156 'crx' : 'application/x-chrome-extension', 157 'gif': 'image/gif', 158 'jpeg' : 'image/jpeg', 159 'jpg' : 'image/jpeg', 160 'xml' : 'text/xml' 161 } 162 self._default_mime_type = 'text/html' 163 164 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, 165 client_address, 166 socket_server) 167 168 def log_request(self, *args, **kwargs): 169 # Disable request logging to declutter test log output. 170 pass 171 172 def _ShouldHandleRequest(self, handler_name): 173 """Determines if the path can be handled by the handler. 174 175 We consider a handler valid if the path begins with the 176 handler name. It can optionally be followed by "?*", "/*". 177 """ 178 179 pattern = re.compile('%s($|\?|/).*' % handler_name) 180 return pattern.match(self.path) 181 182 def GetMIMETypeFromName(self, file_name): 183 """Returns the mime type for the specified file_name. So far it only looks 184 at the file extension.""" 185 186 (shortname, extension) = os.path.splitext(file_name.split("?")[0]) 187 if len(extension) == 0: 188 # no extension. 189 return self._default_mime_type 190 191 # extension starts with a dot, so we need to remove it 192 return self._mime_types.get(extension[1:], self._default_mime_type) 193 194 def NoCacheMaxAgeTimeHandler(self): 195 """This request handler yields a page with the title set to the current 196 system time, and no caching requested.""" 197 198 if not self._ShouldHandleRequest("/nocachetime/maxage"): 199 return False 200 201 self.send_response(200) 202 self.send_header('Cache-Control', 'max-age=0') 203 self.send_header('Content-type', 'text/html') 204 self.end_headers() 205 206 self.wfile.write('<html><head><title>%s</title></head></html>' % 207 time.time()) 208 209 return True 210 211 def NoCacheTimeHandler(self): 212 """This request handler yields a page with the title set to the current 213 system time, and no caching requested.""" 214 215 if not self._ShouldHandleRequest("/nocachetime"): 216 return False 217 218 self.send_response(200) 219 self.send_header('Cache-Control', 'no-cache') 220 self.send_header('Content-type', 'text/html') 221 self.end_headers() 222 223 self.wfile.write('<html><head><title>%s</title></head></html>' % 224 time.time()) 225 226 return True 227 228 def CacheTimeHandler(self): 229 """This request handler yields a page with the title set to the current 230 system time, and allows caching for one minute.""" 231 232 if not self._ShouldHandleRequest("/cachetime"): 233 return False 234 235 self.send_response(200) 236 self.send_header('Cache-Control', 'max-age=60') 237 self.send_header('Content-type', 'text/html') 238 self.end_headers() 239 240 self.wfile.write('<html><head><title>%s</title></head></html>' % 241 time.time()) 242 243 return True 244 245 def CacheExpiresHandler(self): 246 """This request handler yields a page with the title set to the current 247 system time, and set the page to expire on 1 Jan 2099.""" 248 249 if not self._ShouldHandleRequest("/cache/expires"): 250 return False 251 252 self.send_response(200) 253 self.send_header('Expires', 'Thu, 1 Jan 2099 00:00:00 GMT') 254 self.send_header('Content-type', 'text/html') 255 self.end_headers() 256 257 self.wfile.write('<html><head><title>%s</title></head></html>' % 258 time.time()) 259 260 return True 261 262 def CacheProxyRevalidateHandler(self): 263 """This request handler yields a page with the title set to the current 264 system time, and allows caching for 60 seconds""" 265 266 if not self._ShouldHandleRequest("/cache/proxy-revalidate"): 267 return False 268 269 self.send_response(200) 270 self.send_header('Content-type', 'text/html') 271 self.send_header('Cache-Control', 'max-age=60, proxy-revalidate') 272 self.end_headers() 273 274 self.wfile.write('<html><head><title>%s</title></head></html>' % 275 time.time()) 276 277 return True 278 279 def CachePrivateHandler(self): 280 """This request handler yields a page with the title set to the current 281 system time, and allows caching for 5 seconds.""" 282 283 if not self._ShouldHandleRequest("/cache/private"): 284 return False 285 286 self.send_response(200) 287 self.send_header('Content-type', 'text/html') 288 self.send_header('Cache-Control', 'max-age=3, private') 289 self.end_headers() 290 291 self.wfile.write('<html><head><title>%s</title></head></html>' % 292 time.time()) 293 294 return True 295 296 def CachePublicHandler(self): 297 """This request handler yields a page with the title set to the current 298 system time, and allows caching for 5 seconds.""" 299 300 if not self._ShouldHandleRequest("/cache/public"): 301 return False 302 303 self.send_response(200) 304 self.send_header('Content-type', 'text/html') 305 self.send_header('Cache-Control', 'max-age=3, public') 306 self.end_headers() 307 308 self.wfile.write('<html><head><title>%s</title></head></html>' % 309 time.time()) 310 311 return True 312 313 def CacheSMaxAgeHandler(self): 314 """This request handler yields a page with the title set to the current 315 system time, and does not allow for caching.""" 316 317 if not self._ShouldHandleRequest("/cache/s-maxage"): 318 return False 319 320 self.send_response(200) 321 self.send_header('Content-type', 'text/html') 322 self.send_header('Cache-Control', 'public, s-maxage = 60, max-age = 0') 323 self.end_headers() 324 325 self.wfile.write('<html><head><title>%s</title></head></html>' % 326 time.time()) 327 328 return True 329 330 def CacheMustRevalidateHandler(self): 331 """This request handler yields a page with the title set to the current 332 system time, and does not allow caching.""" 333 334 if not self._ShouldHandleRequest("/cache/must-revalidate"): 335 return False 336 337 self.send_response(200) 338 self.send_header('Content-type', 'text/html') 339 self.send_header('Cache-Control', 'must-revalidate') 340 self.end_headers() 341 342 self.wfile.write('<html><head><title>%s</title></head></html>' % 343 time.time()) 344 345 return True 346 347 def CacheMustRevalidateMaxAgeHandler(self): 348 """This request handler yields a page with the title set to the current 349 system time, and does not allow caching event though max-age of 60 350 seconds is specified.""" 351 352 if not self._ShouldHandleRequest("/cache/must-revalidate/max-age"): 353 return False 354 355 self.send_response(200) 356 self.send_header('Content-type', 'text/html') 357 self.send_header('Cache-Control', 'max-age=60, must-revalidate') 358 self.end_headers() 359 360 self.wfile.write('<html><head><title>%s</title></head></html>' % 361 time.time()) 362 363 return True 364 365 def CacheNoStoreHandler(self): 366 """This request handler yields a page with the title set to the current 367 system time, and does not allow the page to be stored.""" 368 369 if not self._ShouldHandleRequest("/cache/no-store"): 370 return False 371 372 self.send_response(200) 373 self.send_header('Content-type', 'text/html') 374 self.send_header('Cache-Control', 'no-store') 375 self.end_headers() 376 377 self.wfile.write('<html><head><title>%s</title></head></html>' % 378 time.time()) 379 380 return True 381 382 def CacheNoStoreMaxAgeHandler(self): 383 """This request handler yields a page with the title set to the current 384 system time, and does not allow the page to be stored even though max-age 385 of 60 seconds is specified.""" 386 387 if not self._ShouldHandleRequest("/cache/no-store/max-age"): 388 return False 389 390 self.send_response(200) 391 self.send_header('Content-type', 'text/html') 392 self.send_header('Cache-Control', 'max-age=60, no-store') 393 self.end_headers() 394 395 self.wfile.write('<html><head><title>%s</title></head></html>' % 396 time.time()) 397 398 return True 399 400 401 def CacheNoTransformHandler(self): 402 """This request handler yields a page with the title set to the current 403 system time, and does not allow the content to transformed during 404 user-agent caching""" 405 406 if not self._ShouldHandleRequest("/cache/no-transform"): 407 return False 408 409 self.send_response(200) 410 self.send_header('Content-type', 'text/html') 411 self.send_header('Cache-Control', 'no-transform') 412 self.end_headers() 413 414 self.wfile.write('<html><head><title>%s</title></head></html>' % 415 time.time()) 416 417 return True 418 419 def EchoHeader(self): 420 """This handler echoes back the value of a specific request header.""" 421 """The only difference between this function and the EchoHeaderOverride""" 422 """function is in the parameter being passed to the helper function""" 423 return self.EchoHeaderHelper("/echoheader") 424 425 def EchoHeaderOverride(self): 426 """This handler echoes back the value of a specific request header.""" 427 """The UrlRequest unit tests also execute for ChromeFrame which uses""" 428 """IE to issue HTTP requests using the host network stack.""" 429 """The Accept and Charset tests which expect the server to echo back""" 430 """the corresponding headers fail here as IE returns cached responses""" 431 """The EchoHeaderOverride parameter is an easy way to ensure that IE""" 432 """treats this request as a new request and does not cache it.""" 433 return self.EchoHeaderHelper("/echoheaderoverride") 434 435 def EchoHeaderHelper(self, echo_header): 436 """This function echoes back the value of the request header passed in.""" 437 if not self._ShouldHandleRequest(echo_header): 438 return False 439 440 query_char = self.path.find('?') 441 if query_char != -1: 442 header_name = self.path[query_char+1:] 443 444 self.send_response(200) 445 self.send_header('Content-type', 'text/plain') 446 self.send_header('Cache-control', 'max-age=60000') 447 # insert a vary header to properly indicate that the cachability of this 448 # request is subject to value of the request header being echoed. 449 if len(header_name) > 0: 450 self.send_header('Vary', header_name) 451 self.end_headers() 452 453 if len(header_name) > 0: 454 self.wfile.write(self.headers.getheader(header_name)) 455 456 return True 457 458 def EchoHandler(self): 459 """This handler just echoes back the payload of the request, for testing 460 form submission.""" 461 462 if not self._ShouldHandleRequest("/echo"): 463 return False 464 465 self.send_response(200) 466 self.send_header('Content-type', 'text/html') 467 self.end_headers() 468 length = int(self.headers.getheader('content-length')) 469 request = self.rfile.read(length) 470 self.wfile.write(request) 471 return True 472 473 def EchoTitleHandler(self): 474 """This handler is like Echo, but sets the page title to the request.""" 475 476 if not self._ShouldHandleRequest("/echotitle"): 477 return False 478 479 self.send_response(200) 480 self.send_header('Content-type', 'text/html') 481 self.end_headers() 482 length = int(self.headers.getheader('content-length')) 483 request = self.rfile.read(length) 484 self.wfile.write('<html><head><title>') 485 self.wfile.write(request) 486 self.wfile.write('</title></head></html>') 487 return True 488 489 def EchoAllHandler(self): 490 """This handler yields a (more) human-readable page listing information 491 about the request header & contents.""" 492 493 if not self._ShouldHandleRequest("/echoall"): 494 return False 495 496 self.send_response(200) 497 self.send_header('Content-type', 'text/html') 498 self.end_headers() 499 self.wfile.write('<html><head><style>' 500 'pre { border: 1px solid black; margin: 5px; padding: 5px }' 501 '</style></head><body>' 502 '<div style="float: right">' 503 '<a href="http://localhost:8888/echo">back to referring page</a></div>' 504 '<h1>Request Body:</h1><pre>') 505 506 if self.command == 'POST' or self.command == 'PUT': 507 length = int(self.headers.getheader('content-length')) 508 qs = self.rfile.read(length) 509 params = cgi.parse_qs(qs, keep_blank_values=1) 510 511 for param in params: 512 self.wfile.write('%s=%s\n' % (param, params[param][0])) 513 514 self.wfile.write('</pre>') 515 516 self.wfile.write('<h1>Request Headers:</h1><pre>%s</pre>' % self.headers) 517 518 self.wfile.write('</body></html>') 519 return True 520 521 def DownloadHandler(self): 522 """This handler sends a downloadable file with or without reporting 523 the size (6K).""" 524 525 if self.path.startswith("/download-unknown-size"): 526 send_length = False 527 elif self.path.startswith("/download-known-size"): 528 send_length = True 529 else: 530 return False 531 532 # 533 # The test which uses this functionality is attempting to send 534 # small chunks of data to the client. Use a fairly large buffer 535 # so that we'll fill chrome's IO buffer enough to force it to 536 # actually write the data. 537 # See also the comments in the client-side of this test in 538 # download_uitest.cc 539 # 540 size_chunk1 = 35*1024 541 size_chunk2 = 10*1024 542 543 self.send_response(200) 544 self.send_header('Content-type', 'application/octet-stream') 545 self.send_header('Cache-Control', 'max-age=0') 546 if send_length: 547 self.send_header('Content-Length', size_chunk1 + size_chunk2) 548 self.end_headers() 549 550 # First chunk of data: 551 self.wfile.write("*" * size_chunk1) 552 self.wfile.flush() 553 554 # handle requests until one of them clears this flag. 555 self.server.waitForDownload = True 556 while self.server.waitForDownload: 557 self.server.handle_request() 558 559 # Second chunk of data: 560 self.wfile.write("*" * size_chunk2) 561 return True 562 563 def DownloadFinishHandler(self): 564 """This handler just tells the server to finish the current download.""" 565 566 if not self._ShouldHandleRequest("/download-finish"): 567 return False 568 569 self.server.waitForDownload = False 570 self.send_response(200) 571 self.send_header('Content-type', 'text/html') 572 self.send_header('Cache-Control', 'max-age=0') 573 self.end_headers() 574 return True 575 576 def _ReplaceFileData(self, data, query_parameters): 577 """Replaces matching substrings in a file. 578 579 If the 'replace_orig' and 'replace_new' URL query parameters are present, 580 a new string is returned with all occasions of the 'replace_orig' value 581 replaced by the 'replace_new' value. 582 583 If the parameters are not present, |data| is returned. 584 """ 585 query_dict = cgi.parse_qs(query_parameters) 586 orig_values = query_dict.get('replace_orig', []) 587 new_values = query_dict.get('replace_new', []) 588 if not orig_values or not new_values: 589 return data 590 orig_value = orig_values[0] 591 new_value = new_values[0] 592 return data.replace(orig_value, new_value) 593 594 def FileHandler(self): 595 """This handler sends the contents of the requested file. Wow, it's like 596 a real webserver!""" 597 598 prefix = self.server.file_root_url 599 if not self.path.startswith(prefix): 600 return False 601 602 # Consume a request body if present. 603 if self.command == 'POST' or self.command == 'PUT' : 604 self.rfile.read(int(self.headers.getheader('content-length'))) 605 606 _, _, url_path, _, query, _ = urlparse.urlparse(self.path) 607 sub_path = url_path[len(prefix):] 608 entries = sub_path.split('/') 609 file_path = os.path.join(self.server.data_dir, *entries) 610 if os.path.isdir(file_path): 611 file_path = os.path.join(file_path, 'index.html') 612 613 if not os.path.isfile(file_path): 614 print "File not found " + sub_path + " full path:" + file_path 615 self.send_error(404) 616 return True 617 618 f = open(file_path, "rb") 619 data = f.read() 620 f.close() 621 622 data = self._ReplaceFileData(data, query) 623 624 # If file.mock-http-headers exists, it contains the headers we 625 # should send. Read them in and parse them. 626 headers_path = file_path + '.mock-http-headers' 627 if os.path.isfile(headers_path): 628 f = open(headers_path, "r") 629 630 # "HTTP/1.1 200 OK" 631 response = f.readline() 632 status_code = re.findall('HTTP/\d+.\d+ (\d+)', response)[0] 633 self.send_response(int(status_code)) 634 635 for line in f: 636 header_values = re.findall('(\S+):\s*(.*)', line) 637 if len(header_values) > 0: 638 # "name: value" 639 name, value = header_values[0] 640 self.send_header(name, value) 641 f.close() 642 else: 643 # Could be more generic once we support mime-type sniffing, but for 644 # now we need to set it explicitly. 645 self.send_response(200) 646 self.send_header('Content-type', self.GetMIMETypeFromName(file_path)) 647 self.send_header('Content-Length', len(data)) 648 self.end_headers() 649 650 self.wfile.write(data) 651 652 return True 653 654 def RealFileWithCommonHeaderHandler(self): 655 """This handler sends the contents of the requested file without the pseudo 656 http head!""" 657 658 prefix='/realfiles/' 659 if not self.path.startswith(prefix): 660 return False 661 662 file = self.path[len(prefix):] 663 path = os.path.join(self.server.data_dir, file) 664 665 try: 666 f = open(path, "rb") 667 data = f.read() 668 f.close() 669 670 # just simply set the MIME as octal stream 671 self.send_response(200) 672 self.send_header('Content-type', 'application/octet-stream') 673 self.end_headers() 674 675 self.wfile.write(data) 676 except: 677 self.send_error(404) 678 679 return True 680 681 def RealBZ2FileWithCommonHeaderHandler(self): 682 """This handler sends the bzip2 contents of the requested file with 683 corresponding Content-Encoding field in http head!""" 684 685 prefix='/realbz2files/' 686 if not self.path.startswith(prefix): 687 return False 688 689 parts = self.path.split('?') 690 file = parts[0][len(prefix):] 691 path = os.path.join(self.server.data_dir, file) + '.bz2' 692 693 if len(parts) > 1: 694 options = parts[1] 695 else: 696 options = '' 697 698 try: 699 self.send_response(200) 700 accept_encoding = self.headers.get("Accept-Encoding") 701 if accept_encoding.find("bzip2") != -1: 702 f = open(path, "rb") 703 data = f.read() 704 f.close() 705 self.send_header('Content-Encoding', 'bzip2') 706 self.send_header('Content-type', 'application/x-bzip2') 707 self.end_headers() 708 if options == 'incremental-header': 709 self.wfile.write(data[:1]) 710 self.wfile.flush() 711 time.sleep(1.0) 712 self.wfile.write(data[1:]) 713 else: 714 self.wfile.write(data) 715 else: 716 """client do not support bzip2 format, send pseudo content 717 """ 718 self.send_header('Content-type', 'text/html; charset=ISO-8859-1') 719 self.end_headers() 720 self.wfile.write("you do not support bzip2 encoding") 721 except: 722 self.send_error(404) 723 724 return True 725 726 def SetCookieHandler(self): 727 """This handler just sets a cookie, for testing cookie handling.""" 728 729 if not self._ShouldHandleRequest("/set-cookie"): 730 return False 731 732 query_char = self.path.find('?') 733 if query_char != -1: 734 cookie_values = self.path[query_char + 1:].split('&') 735 else: 736 cookie_values = ("",) 737 self.send_response(200) 738 self.send_header('Content-type', 'text/html') 739 for cookie_value in cookie_values: 740 self.send_header('Set-Cookie', '%s' % cookie_value) 741 self.end_headers() 742 for cookie_value in cookie_values: 743 self.wfile.write('%s' % cookie_value) 744 return True 745 746 def AuthBasicHandler(self): 747 """This handler tests 'Basic' authentication. It just sends a page with 748 title 'user/pass' if you succeed.""" 749 750 if not self._ShouldHandleRequest("/auth-basic"): 751 return False 752 753 username = userpass = password = b64str = "" 754 755 set_cookie_if_challenged = self.path.find('?set-cookie-if-challenged') > 0 756 757 auth = self.headers.getheader('authorization') 758 try: 759 if not auth: 760 raise Exception('no auth') 761 b64str = re.findall(r'Basic (\S+)', auth)[0] 762 userpass = base64.b64decode(b64str) 763 username, password = re.findall(r'([^:]+):(\S+)', userpass)[0] 764 if password != 'secret': 765 raise Exception('wrong password') 766 except Exception, e: 767 # Authentication failed. 768 self.send_response(401) 769 self.send_header('WWW-Authenticate', 'Basic realm="testrealm"') 770 self.send_header('Content-type', 'text/html') 771 if set_cookie_if_challenged: 772 self.send_header('Set-Cookie', 'got_challenged=true') 773 self.end_headers() 774 self.wfile.write('<html><head>') 775 self.wfile.write('<title>Denied: %s</title>' % e) 776 self.wfile.write('</head><body>') 777 self.wfile.write('auth=%s<p>' % auth) 778 self.wfile.write('b64str=%s<p>' % b64str) 779 self.wfile.write('username: %s<p>' % username) 780 self.wfile.write('userpass: %s<p>' % userpass) 781 self.wfile.write('password: %s<p>' % password) 782 self.wfile.write('You sent:<br>%s<p>' % self.headers) 783 self.wfile.write('</body></html>') 784 return True 785 786 # Authentication successful. (Return a cachable response to allow for 787 # testing cached pages that require authentication.) 788 if_none_match = self.headers.getheader('if-none-match') 789 if if_none_match == "abc": 790 self.send_response(304) 791 self.end_headers() 792 else: 793 self.send_response(200) 794 self.send_header('Content-type', 'text/html') 795 self.send_header('Cache-control', 'max-age=60000') 796 self.send_header('Etag', 'abc') 797 self.end_headers() 798 self.wfile.write('<html><head>') 799 self.wfile.write('<title>%s/%s</title>' % (username, password)) 800 self.wfile.write('</head><body>') 801 self.wfile.write('auth=%s<p>' % auth) 802 self.wfile.write('You sent:<br>%s<p>' % self.headers) 803 self.wfile.write('</body></html>') 804 805 return True 806 807 def GetNonce(self, force_reset=False): 808 """Returns a nonce that's stable per request path for the server's lifetime. 809 810 This is a fake implementation. A real implementation would only use a given 811 nonce a single time (hence the name n-once). However, for the purposes of 812 unittesting, we don't care about the security of the nonce. 813 814 Args: 815 force_reset: Iff set, the nonce will be changed. Useful for testing the 816 "stale" response. 817 """ 818 if force_reset or not self.server.nonce_time: 819 self.server.nonce_time = time.time() 820 return _new_md5('privatekey%s%d' % 821 (self.path, self.server.nonce_time)).hexdigest() 822 823 def AuthDigestHandler(self): 824 """This handler tests 'Digest' authentication. 825 826 It just sends a page with title 'user/pass' if you succeed. 827 828 A stale response is sent iff "stale" is present in the request path. 829 """ 830 if not self._ShouldHandleRequest("/auth-digest"): 831 return False 832 833 stale = 'stale' in self.path 834 nonce = self.GetNonce(force_reset=stale) 835 opaque = _new_md5('opaque').hexdigest() 836 password = 'secret' 837 realm = 'testrealm' 838 839 auth = self.headers.getheader('authorization') 840 pairs = {} 841 try: 842 if not auth: 843 raise Exception('no auth') 844 if not auth.startswith('Digest'): 845 raise Exception('not digest') 846 # Pull out all the name="value" pairs as a dictionary. 847 pairs = dict(re.findall(r'(\b[^ ,=]+)="?([^",]+)"?', auth)) 848 849 # Make sure it's all valid. 850 if pairs['nonce'] != nonce: 851 raise Exception('wrong nonce') 852 if pairs['opaque'] != opaque: 853 raise Exception('wrong opaque') 854 855 # Check the 'response' value and make sure it matches our magic hash. 856 # See http://www.ietf.org/rfc/rfc2617.txt 857 hash_a1 = _new_md5( 858 ':'.join([pairs['username'], realm, password])).hexdigest() 859 hash_a2 = _new_md5(':'.join([self.command, pairs['uri']])).hexdigest() 860 if 'qop' in pairs and 'nc' in pairs and 'cnonce' in pairs: 861 response = _new_md5(':'.join([hash_a1, nonce, pairs['nc'], 862 pairs['cnonce'], pairs['qop'], hash_a2])).hexdigest() 863 else: 864 response = _new_md5(':'.join([hash_a1, nonce, hash_a2])).hexdigest() 865 866 if pairs['response'] != response: 867 raise Exception('wrong password') 868 except Exception, e: 869 # Authentication failed. 870 self.send_response(401) 871 hdr = ('Digest ' 872 'realm="%s", ' 873 'domain="/", ' 874 'qop="auth", ' 875 'algorithm=MD5, ' 876 'nonce="%s", ' 877 'opaque="%s"') % (realm, nonce, opaque) 878 if stale: 879 hdr += ', stale="TRUE"' 880 self.send_header('WWW-Authenticate', hdr) 881 self.send_header('Content-type', 'text/html') 882 self.end_headers() 883 self.wfile.write('<html><head>') 884 self.wfile.write('<title>Denied: %s</title>' % e) 885 self.wfile.write('</head><body>') 886 self.wfile.write('auth=%s<p>' % auth) 887 self.wfile.write('pairs=%s<p>' % pairs) 888 self.wfile.write('You sent:<br>%s<p>' % self.headers) 889 self.wfile.write('We are replying:<br>%s<p>' % hdr) 890 self.wfile.write('</body></html>') 891 return True 892 893 # Authentication successful. 894 self.send_response(200) 895 self.send_header('Content-type', 'text/html') 896 self.end_headers() 897 self.wfile.write('<html><head>') 898 self.wfile.write('<title>%s/%s</title>' % (pairs['username'], password)) 899 self.wfile.write('</head><body>') 900 self.wfile.write('auth=%s<p>' % auth) 901 self.wfile.write('pairs=%s<p>' % pairs) 902 self.wfile.write('</body></html>') 903 904 return True 905 906 def SlowServerHandler(self): 907 """Wait for the user suggested time before responding. The syntax is 908 /slow?0.5 to wait for half a second.""" 909 if not self._ShouldHandleRequest("/slow"): 910 return False 911 query_char = self.path.find('?') 912 wait_sec = 1.0 913 if query_char >= 0: 914 try: 915 wait_sec = int(self.path[query_char + 1:]) 916 except ValueError: 917 pass 918 time.sleep(wait_sec) 919 self.send_response(200) 920 self.send_header('Content-type', 'text/plain') 921 self.end_headers() 922 self.wfile.write("waited %d seconds" % wait_sec) 923 return True 924 925 def ContentTypeHandler(self): 926 """Returns a string of html with the given content type. E.g., 927 /contenttype?text/css returns an html file with the Content-Type 928 header set to text/css.""" 929 if not self._ShouldHandleRequest("/contenttype"): 930 return False 931 query_char = self.path.find('?') 932 content_type = self.path[query_char + 1:].strip() 933 if not content_type: 934 content_type = 'text/html' 935 self.send_response(200) 936 self.send_header('Content-Type', content_type) 937 self.end_headers() 938 self.wfile.write("<html>\n<body>\n<p>HTML text</p>\n</body>\n</html>\n"); 939 return True 940 941 def ServerRedirectHandler(self): 942 """Sends a server redirect to the given URL. The syntax is 943 '/server-redirect?http://foo.bar/asdf' to redirect to 944 'http://foo.bar/asdf'""" 945 946 test_name = "/server-redirect" 947 if not self._ShouldHandleRequest(test_name): 948 return False 949 950 query_char = self.path.find('?') 951 if query_char < 0 or len(self.path) <= query_char + 1: 952 self.sendRedirectHelp(test_name) 953 return True 954 dest = self.path[query_char + 1:] 955 956 self.send_response(301) # moved permanently 957 self.send_header('Location', dest) 958 self.send_header('Content-type', 'text/html') 959 self.end_headers() 960 self.wfile.write('<html><head>') 961 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest) 962 963 return True 964 965 def ClientRedirectHandler(self): 966 """Sends a client redirect to the given URL. The syntax is 967 '/client-redirect?http://foo.bar/asdf' to redirect to 968 'http://foo.bar/asdf'""" 969 970 test_name = "/client-redirect" 971 if not self._ShouldHandleRequest(test_name): 972 return False 973 974 query_char = self.path.find('?'); 975 if query_char < 0 or len(self.path) <= query_char + 1: 976 self.sendRedirectHelp(test_name) 977 return True 978 dest = self.path[query_char + 1:] 979 980 self.send_response(200) 981 self.send_header('Content-type', 'text/html') 982 self.end_headers() 983 self.wfile.write('<html><head>') 984 self.wfile.write('<meta http-equiv="refresh" content="0;url=%s">' % dest) 985 self.wfile.write('</head><body>Redirecting to %s</body></html>' % dest) 986 987 return True 988 989 def ChromiumSyncTimeHandler(self): 990 """Handle Chromium sync .../time requests. 991 992 The syncer sometimes checks server reachability by examining /time. 993 """ 994 test_name = "/chromiumsync/time" 995 if not self._ShouldHandleRequest(test_name): 996 return False 997 998 self.send_response(200) 999 self.send_header('Content-type', 'text/html') 1000 self.end_headers() 1001 return True 1002 1003 def ChromiumSyncCommandHandler(self): 1004 """Handle a chromiumsync command arriving via http. 1005 1006 This covers all sync protocol commands: authentication, getupdates, and 1007 commit. 1008 """ 1009 test_name = "/chromiumsync/command" 1010 if not self._ShouldHandleRequest(test_name): 1011 return False 1012 1013 length = int(self.headers.getheader('content-length')) 1014 raw_request = self.rfile.read(length) 1015 1016 if not self.server._sync_handler: 1017 import chromiumsync 1018 self.server._sync_handler = chromiumsync.TestServer() 1019 http_response, raw_reply = self.server._sync_handler.HandleCommand( 1020 self.path, raw_request) 1021 self.send_response(http_response) 1022 self.end_headers() 1023 self.wfile.write(raw_reply) 1024 return True 1025 1026 def MultipartHandler(self): 1027 """Send a multipart response (10 text/html pages).""" 1028 test_name = "/multipart" 1029 if not self._ShouldHandleRequest(test_name): 1030 return False 1031 1032 num_frames = 10 1033 bound = '12345' 1034 self.send_response(200) 1035 self.send_header('Content-type', 1036 'multipart/x-mixed-replace;boundary=' + bound) 1037 self.end_headers() 1038 1039 for i in xrange(num_frames): 1040 self.wfile.write('--' + bound + '\r\n') 1041 self.wfile.write('Content-type: text/html\r\n\r\n') 1042 self.wfile.write('<title>page ' + str(i) + '</title>') 1043 self.wfile.write('page ' + str(i)) 1044 1045 self.wfile.write('--' + bound + '--') 1046 return True 1047 1048 def DefaultResponseHandler(self): 1049 """This is the catch-all response handler for requests that aren't handled 1050 by one of the special handlers above. 1051 Note that we specify the content-length as without it the https connection 1052 is not closed properly (and the browser keeps expecting data).""" 1053 1054 contents = "Default response given for path: " + self.path 1055 self.send_response(200) 1056 self.send_header('Content-type', 'text/html') 1057 self.send_header("Content-Length", len(contents)) 1058 self.end_headers() 1059 self.wfile.write(contents) 1060 return True 1061 1062 def RedirectConnectHandler(self): 1063 """Sends a redirect to the CONNECT request for www.redirect.com. This 1064 response is not specified by the RFC, so the browser should not follow 1065 the redirect.""" 1066 1067 if (self.path.find("www.redirect.com") < 0): 1068 return False 1069 1070 dest = "http://www.destination.com/foo.js" 1071 1072 self.send_response(302) # moved temporarily 1073 self.send_header('Location', dest) 1074 self.send_header('Connection', 'close') 1075 self.end_headers() 1076 return True 1077 1078 def ServerAuthConnectHandler(self): 1079 """Sends a 401 to the CONNECT request for www.server-auth.com. This 1080 response doesn't make sense because the proxy server cannot request 1081 server authentication.""" 1082 1083 if (self.path.find("www.server-auth.com") < 0): 1084 return False 1085 1086 challenge = 'Basic realm="WallyWorld"' 1087 1088 self.send_response(401) # unauthorized 1089 self.send_header('WWW-Authenticate', challenge) 1090 self.send_header('Connection', 'close') 1091 self.end_headers() 1092 return True 1093 1094 def DefaultConnectResponseHandler(self): 1095 """This is the catch-all response handler for CONNECT requests that aren't 1096 handled by one of the special handlers above. Real Web servers respond 1097 with 400 to CONNECT requests.""" 1098 1099 contents = "Your client has issued a malformed or illegal request." 1100 self.send_response(400) # bad request 1101 self.send_header('Content-type', 'text/html') 1102 self.send_header("Content-Length", len(contents)) 1103 self.end_headers() 1104 self.wfile.write(contents) 1105 return True 1106 1107 def do_CONNECT(self): 1108 for handler in self._connect_handlers: 1109 if handler(): 1110 return 1111 1112 def do_GET(self): 1113 for handler in self._get_handlers: 1114 if handler(): 1115 return 1116 1117 def do_POST(self): 1118 for handler in self._post_handlers: 1119 if handler(): 1120 return 1121 1122 def do_PUT(self): 1123 for handler in self._put_handlers: 1124 if handler(): 1125 return 1126 1127 # called by the redirect handling function when there is no parameter 1128 def sendRedirectHelp(self, redirect_name): 1129 self.send_response(200) 1130 self.send_header('Content-type', 'text/html') 1131 self.end_headers() 1132 self.wfile.write('<html><body><h1>Error: no redirect destination</h1>') 1133 self.wfile.write('Use <pre>%s?http://dest...</pre>' % redirect_name) 1134 self.wfile.write('</body></html>') 1135 1136def MakeDataDir(): 1137 if options.data_dir: 1138 if not os.path.isdir(options.data_dir): 1139 print 'specified data dir not found: ' + options.data_dir + ' exiting...' 1140 return None 1141 my_data_dir = options.data_dir 1142 else: 1143 # Create the default path to our data dir, relative to the exe dir. 1144 my_data_dir = os.path.dirname(sys.argv[0]) 1145 my_data_dir = os.path.join(my_data_dir, "..", "..", "..", "..", 1146 "test", "data") 1147 1148 #TODO(ibrar): Must use Find* funtion defined in google\tools 1149 #i.e my_data_dir = FindUpward(my_data_dir, "test", "data") 1150 1151 return my_data_dir 1152 1153class FileMultiplexer: 1154 def __init__(self, fd1, fd2) : 1155 self.__fd1 = fd1 1156 self.__fd2 = fd2 1157 1158 def __del__(self) : 1159 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr: 1160 self.__fd1.close() 1161 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr: 1162 self.__fd2.close() 1163 1164 def write(self, text) : 1165 self.__fd1.write(text) 1166 self.__fd2.write(text) 1167 1168 def flush(self) : 1169 self.__fd1.flush() 1170 self.__fd2.flush() 1171 1172def main(options, args): 1173 logfile = open('testserver.log', 'w') 1174 sys.stdout = FileMultiplexer(sys.stdout, logfile) 1175 sys.stderr = FileMultiplexer(sys.stderr, logfile) 1176 1177 port = options.port 1178 1179 if options.server_type == SERVER_HTTP: 1180 if options.cert: 1181 # let's make sure the cert file exists. 1182 if not os.path.isfile(options.cert): 1183 print 'specified server cert file not found: ' + options.cert + \ 1184 ' exiting...' 1185 return 1186 for ca_cert in options.ssl_client_ca: 1187 if not os.path.isfile(ca_cert): 1188 print 'specified trusted client CA file not found: ' + ca_cert + \ 1189 ' exiting...' 1190 return 1191 server = HTTPSServer(('127.0.0.1', port), TestPageHandler, options.cert, 1192 options.ssl_client_auth, options.ssl_client_ca, 1193 options.ssl_bulk_cipher) 1194 print 'HTTPS server started on port %d...' % port 1195 else: 1196 server = StoppableHTTPServer(('127.0.0.1', port), TestPageHandler) 1197 print 'HTTP server started on port %d...' % port 1198 1199 server.data_dir = MakeDataDir() 1200 server.file_root_url = options.file_root_url 1201 server._sync_handler = None 1202 1203 # means FTP Server 1204 else: 1205 my_data_dir = MakeDataDir() 1206 1207 # Instantiate a dummy authorizer for managing 'virtual' users 1208 authorizer = pyftpdlib.ftpserver.DummyAuthorizer() 1209 1210 # Define a new user having full r/w permissions and a read-only 1211 # anonymous user 1212 authorizer.add_user('chrome', 'chrome', my_data_dir, perm='elradfmw') 1213 1214 authorizer.add_anonymous(my_data_dir) 1215 1216 # Instantiate FTP handler class 1217 ftp_handler = pyftpdlib.ftpserver.FTPHandler 1218 ftp_handler.authorizer = authorizer 1219 1220 # Define a customized banner (string returned when client connects) 1221 ftp_handler.banner = ("pyftpdlib %s based ftpd ready." % 1222 pyftpdlib.ftpserver.__ver__) 1223 1224 # Instantiate FTP server class and listen to 127.0.0.1:port 1225 address = ('127.0.0.1', port) 1226 server = pyftpdlib.ftpserver.FTPServer(address, ftp_handler) 1227 print 'FTP server started on port %d...' % port 1228 1229 # Notify the parent that we've started. (BaseServer subclasses 1230 # bind their sockets on construction.) 1231 if options.startup_pipe is not None: 1232 if sys.platform == 'win32': 1233 fd = msvcrt.open_osfhandle(options.startup_pipe, 0) 1234 else: 1235 fd = options.startup_pipe 1236 startup_pipe = os.fdopen(fd, "w") 1237 startup_pipe.write("READY") 1238 startup_pipe.close() 1239 1240 try: 1241 server.serve_forever() 1242 except KeyboardInterrupt: 1243 print 'shutting down server' 1244 server.stop = True 1245 1246if __name__ == '__main__': 1247 option_parser = optparse.OptionParser() 1248 option_parser.add_option("-f", '--ftp', action='store_const', 1249 const=SERVER_FTP, default=SERVER_HTTP, 1250 dest='server_type', 1251 help='FTP or HTTP server: default is HTTP.') 1252 option_parser.add_option('', '--port', default='8888', type='int', 1253 help='Port used by the server.') 1254 option_parser.add_option('', '--data-dir', dest='data_dir', 1255 help='Directory from which to read the files.') 1256 option_parser.add_option('', '--https', dest='cert', 1257 help='Specify that https should be used, specify ' 1258 'the path to the cert containing the private key ' 1259 'the server should use.') 1260 option_parser.add_option('', '--ssl-client-auth', action='store_true', 1261 help='Require SSL client auth on every connection.') 1262 option_parser.add_option('', '--ssl-client-ca', action='append', default=[], 1263 help='Specify that the client certificate request ' 1264 'should include the CA named in the subject of ' 1265 'the DER-encoded certificate contained in the ' 1266 'specified file. This option may appear multiple ' 1267 'times, indicating multiple CA names should be ' 1268 'sent in the request.') 1269 option_parser.add_option('', '--ssl-bulk-cipher', action='append', 1270 help='Specify the bulk encryption algorithm(s)' 1271 'that will be accepted by the SSL server. Valid ' 1272 'values are "aes256", "aes128", "3des", "rc4". If ' 1273 'omitted, all algorithms will be used. This ' 1274 'option may appear multiple times, indicating ' 1275 'multiple algorithms should be enabled.'); 1276 option_parser.add_option('', '--file-root-url', default='/files/', 1277 help='Specify a root URL for files served.') 1278 option_parser.add_option('', '--startup-pipe', type='int', 1279 dest='startup_pipe', 1280 help='File handle of pipe to parent process') 1281 options, args = option_parser.parse_args() 1282 1283 sys.exit(main(options, args)) 1284