1# Copyright 2013 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import BaseHTTPServer 6import errno 7import json 8import optparse 9import os 10import re 11import socket 12import SocketServer 13import struct 14import sys 15import warnings 16 17import tlslite.errors 18 19# Ignore deprecation warnings, they make our output more cluttered. 20warnings.filterwarnings("ignore", category=DeprecationWarning) 21 22if sys.platform == 'win32': 23 import msvcrt 24 25# Using debug() seems to cause hangs on XP: see http://crbug.com/64515. 26debug_output = sys.stderr 27def debug(string): 28 debug_output.write(string + "\n") 29 debug_output.flush() 30 31 32class Error(Exception): 33 """Error class for this module.""" 34 35 36class OptionError(Error): 37 """Error for bad command line options.""" 38 39 40class FileMultiplexer(object): 41 def __init__(self, fd1, fd2) : 42 self.__fd1 = fd1 43 self.__fd2 = fd2 44 45 def __del__(self) : 46 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr: 47 self.__fd1.close() 48 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr: 49 self.__fd2.close() 50 51 def write(self, text) : 52 self.__fd1.write(text) 53 self.__fd2.write(text) 54 55 def flush(self) : 56 self.__fd1.flush() 57 self.__fd2.flush() 58 59 60class ClientRestrictingServerMixIn: 61 """Implements verify_request to limit connections to our configured IP 62 address.""" 63 64 def verify_request(self, _request, client_address): 65 return client_address[0] == self.server_address[0] 66 67 68class BrokenPipeHandlerMixIn: 69 """Allows the server to deal with "broken pipe" errors (which happen if the 70 browser quits with outstanding requests, like for the favicon). This mix-in 71 requires the class to derive from SocketServer.BaseServer and not override its 72 handle_error() method. """ 73 74 def handle_error(self, request, client_address): 75 value = sys.exc_info()[1] 76 if isinstance(value, tlslite.errors.TLSClosedConnectionError): 77 print "testserver.py: Closed connection" 78 return 79 if isinstance(value, socket.error): 80 err = value.args[0] 81 if sys.platform in ('win32', 'cygwin'): 82 # "An established connection was aborted by the software in your host." 83 pipe_err = 10053 84 else: 85 pipe_err = errno.EPIPE 86 if err == pipe_err: 87 print "testserver.py: Broken pipe" 88 return 89 if err == errno.ECONNRESET: 90 print "testserver.py: Connection reset by peer" 91 return 92 SocketServer.BaseServer.handle_error(self, request, client_address) 93 94 95class StoppableHTTPServer(BaseHTTPServer.HTTPServer): 96 """This is a specialization of BaseHTTPServer to allow it 97 to be exited cleanly (by setting its "stop" member to True).""" 98 99 def serve_forever(self): 100 self.stop = False 101 self.nonce_time = None 102 while not self.stop: 103 self.handle_request() 104 self.socket.close() 105 106 107def MultiplexerHack(std_fd, log_fd): 108 """Creates a FileMultiplexer that will write to both specified files. 109 110 When running on Windows XP bots, stdout and stderr will be invalid file 111 handles, so log_fd will be returned directly. (This does not occur if you 112 run the test suite directly from a console, but only if the output of the 113 test executable is redirected.) 114 """ 115 if std_fd.fileno() <= 0: 116 return log_fd 117 return FileMultiplexer(std_fd, log_fd) 118 119 120class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler): 121 122 def __init__(self, request, client_address, socket_server, 123 connect_handlers, get_handlers, head_handlers, post_handlers, 124 put_handlers): 125 self._connect_handlers = connect_handlers 126 self._get_handlers = get_handlers 127 self._head_handlers = head_handlers 128 self._post_handlers = post_handlers 129 self._put_handlers = put_handlers 130 BaseHTTPServer.BaseHTTPRequestHandler.__init__( 131 self, request, client_address, socket_server) 132 133 def log_request(self, *args, **kwargs): 134 # Disable request logging to declutter test log output. 135 pass 136 137 def _ShouldHandleRequest(self, handler_name): 138 """Determines if the path can be handled by the handler. 139 140 We consider a handler valid if the path begins with the 141 handler name. It can optionally be followed by "?*", "/*". 142 """ 143 144 pattern = re.compile('%s($|\?|/).*' % handler_name) 145 return pattern.match(self.path) 146 147 def do_CONNECT(self): 148 for handler in self._connect_handlers: 149 if handler(): 150 return 151 152 def do_GET(self): 153 for handler in self._get_handlers: 154 if handler(): 155 return 156 157 def do_HEAD(self): 158 for handler in self._head_handlers: 159 if handler(): 160 return 161 162 def do_POST(self): 163 for handler in self._post_handlers: 164 if handler(): 165 return 166 167 def do_PUT(self): 168 for handler in self._put_handlers: 169 if handler(): 170 return 171 172 173class TestServerRunner(object): 174 """Runs a test server and communicates with the controlling C++ test code. 175 176 Subclasses should override the create_server method to create their server 177 object, and the add_options method to add their own options. 178 """ 179 180 def __init__(self): 181 self.option_parser = optparse.OptionParser() 182 self.add_options() 183 184 def main(self): 185 self.options, self.args = self.option_parser.parse_args() 186 187 logfile = open(self.options.log_file, 'w') 188 sys.stderr = MultiplexerHack(sys.stderr, logfile) 189 if self.options.log_to_console: 190 sys.stdout = MultiplexerHack(sys.stdout, logfile) 191 else: 192 sys.stdout = logfile 193 194 server_data = { 195 'host': self.options.host, 196 } 197 self.server = self.create_server(server_data) 198 self._notify_startup_complete(server_data) 199 self.run_server() 200 201 def create_server(self, server_data): 202 """Creates a server object and returns it. 203 204 Must populate server_data['port'], and can set additional server_data 205 elements if desired.""" 206 raise NotImplementedError() 207 208 def run_server(self): 209 try: 210 self.server.serve_forever() 211 except KeyboardInterrupt: 212 print 'shutting down server' 213 self.server.stop = True 214 215 def add_options(self): 216 self.option_parser.add_option('--startup-pipe', type='int', 217 dest='startup_pipe', 218 help='File handle of pipe to parent process') 219 self.option_parser.add_option('--log-to-console', action='store_const', 220 const=True, default=False, 221 dest='log_to_console', 222 help='Enables or disables sys.stdout logging ' 223 'to the console.') 224 self.option_parser.add_option('--log-file', default='testserver.log', 225 dest='log_file', 226 help='The name of the server log file.') 227 self.option_parser.add_option('--port', default=0, type='int', 228 help='Port used by the server. If ' 229 'unspecified, the server will listen on an ' 230 'ephemeral port.') 231 self.option_parser.add_option('--host', default='127.0.0.1', 232 dest='host', 233 help='Hostname or IP upon which the server ' 234 'will listen. Client connections will also ' 235 'only be allowed from this address.') 236 self.option_parser.add_option('--data-dir', dest='data_dir', 237 help='Directory from which to read the ' 238 'files.') 239 240 def _notify_startup_complete(self, server_data): 241 # Notify the parent that we've started. (BaseServer subclasses 242 # bind their sockets on construction.) 243 if self.options.startup_pipe is not None: 244 server_data_json = json.dumps(server_data) 245 server_data_len = len(server_data_json) 246 print 'sending server_data: %s (%d bytes)' % ( 247 server_data_json, server_data_len) 248 if sys.platform == 'win32': 249 fd = msvcrt.open_osfhandle(self.options.startup_pipe, 0) 250 else: 251 fd = self.options.startup_pipe 252 startup_pipe = os.fdopen(fd, "w") 253 # First write the data length as an unsigned 4-byte value. This 254 # is _not_ using network byte ordering since the other end of the 255 # pipe is on the same machine. 256 startup_pipe.write(struct.pack('=L', server_data_len)) 257 startup_pipe.write(server_data_json) 258 startup_pipe.close() 259