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