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