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