OLD | NEW |
(Empty) | |
| 1 import BaseHTTPServer |
| 2 import errno |
| 3 import os |
| 4 import re |
| 5 import socket |
| 6 from SocketServer import ThreadingMixIn |
| 7 import ssl |
| 8 import sys |
| 9 import threading |
| 10 import time |
| 11 import traceback |
| 12 import types |
| 13 import urlparse |
| 14 |
| 15 import routes as default_routes |
| 16 from logger import get_logger |
| 17 from request import Server, Request |
| 18 from response import Response |
| 19 from router import Router |
| 20 from utils import HTTPException |
| 21 |
| 22 |
| 23 """HTTP server designed for testing purposes. |
| 24 |
| 25 The server is designed to provide flexibility in the way that |
| 26 requests are handled, and to provide control both of exactly |
| 27 what bytes are put on the wire for the response, and in the |
| 28 timing of sending those bytes. |
| 29 |
| 30 The server is based on the stdlib HTTPServer, but with some |
| 31 notable differences in the way that requests are processed. |
| 32 Overall processing is handled by a WebTestRequestHandler, |
| 33 which is a subclass of BaseHTTPRequestHandler. This is responsible |
| 34 for parsing the incoming request. A RequestRewriter is then |
| 35 applied and may change the request data if it matches a |
| 36 supplied rule. |
| 37 |
| 38 Once the request data had been finalised, Request and Reponse |
| 39 objects are constructed. These are used by the other parts of the |
| 40 system to read information about the request and manipulate the |
| 41 response. |
| 42 |
| 43 Each request is handled by a particular handler function. The |
| 44 mapping between Request and the appropriate handler is determined |
| 45 by a Router. By default handlers are installed to interpret files |
| 46 under the document root with .py extensions as executable python |
| 47 files (see handlers.py for the api for such files), .asis files as |
| 48 bytestreams to be sent literally and all other files to be served |
| 49 statically. |
| 50 |
| 51 The handler functions are responsible for either populating the |
| 52 fields of the response object, which will then be written when the |
| 53 handler returns, or for directly writing to the output stream. |
| 54 """ |
| 55 |
| 56 |
| 57 class RequestRewriter(object): |
| 58 def __init__(self, rules): |
| 59 """Object for rewriting the request path. |
| 60 |
| 61 :param rules: Initial rules to add; a list of three item tuples |
| 62 (method, input_path, output_path), defined as for |
| 63 register() |
| 64 """ |
| 65 self.rules = {} |
| 66 for rule in reversed(rules): |
| 67 self.register(*rule) |
| 68 self.logger = get_logger() |
| 69 |
| 70 def register(self, methods, input_path, output_path): |
| 71 """Register a rewrite rule. |
| 72 |
| 73 :param methods: Set of methods this should match. "*" is a |
| 74 special value indicating that all methods should |
| 75 be matched. |
| 76 |
| 77 :param input_path: Path to match for the initial request. |
| 78 |
| 79 :param output_path: Path to replace the input path with in |
| 80 the request. |
| 81 """ |
| 82 if type(methods) in types.StringTypes: |
| 83 methods = [methods] |
| 84 self.rules[input_path] = (methods, output_path) |
| 85 |
| 86 def rewrite(self, request_handler): |
| 87 """Rewrite the path in a BaseHTTPRequestHandler instance, if |
| 88 it matches a rule. |
| 89 |
| 90 :param request_handler: BaseHTTPRequestHandler for which to |
| 91 rewrite the request. |
| 92 """ |
| 93 split_url = urlparse.urlsplit(request_handler.path) |
| 94 if split_url.path in self.rules: |
| 95 methods, destination = self.rules[split_url.path] |
| 96 if "*" in methods or request_handler.command in methods: |
| 97 self.logger.debug("Rewriting request path %s to %s" % |
| 98 (request_handler.path, destination)) |
| 99 new_url = list(split_url) |
| 100 new_url[2] = destination |
| 101 new_url = urlparse.urlunsplit(new_url) |
| 102 request_handler.path = new_url |
| 103 |
| 104 |
| 105 class WebTestServer(ThreadingMixIn, BaseHTTPServer.HTTPServer): |
| 106 allow_reuse_address = True |
| 107 acceptable_errors = (errno.EPIPE, errno.ECONNABORTED) |
| 108 request_queue_size = 2000 |
| 109 |
| 110 # Ensure that we don't hang on shutdown waiting for requests |
| 111 daemon_threads = True |
| 112 |
| 113 def __init__(self, server_address, RequestHandlerClass, router, rewriter, bi
nd_hostname, |
| 114 config=None, use_ssl=False, key_file=None, certificate=None, |
| 115 encrypt_after_connect=False, latency=None, **kwargs): |
| 116 """Server for HTTP(s) Requests |
| 117 |
| 118 :param server_address: tuple of (server_name, port) |
| 119 |
| 120 :param RequestHandlerClass: BaseHTTPRequestHandler-like class to use for |
| 121 handling requests. |
| 122 |
| 123 :param router: Router instance to use for matching requests to handler |
| 124 functions |
| 125 |
| 126 :param rewriter: RequestRewriter-like instance to use for preprocessing |
| 127 requests before they are routed |
| 128 |
| 129 :param config: Dictionary holding environment configuration settings for |
| 130 handlers to read, or None to use the default values. |
| 131 |
| 132 :param use_ssl: Boolean indicating whether the server should use SSL |
| 133 |
| 134 :param key_file: Path to key file to use if SSL is enabled. |
| 135 |
| 136 :param certificate: Path to certificate to use if SSL is enabled. |
| 137 |
| 138 :param encrypt_after_connect: For each connection, don't start encryptio
n |
| 139 until a CONNECT message has been received. |
| 140 This enables the server to act as a |
| 141 self-proxy. |
| 142 |
| 143 :param bind_hostname True to bind the server to both the hostname and |
| 144 port specified in the server_address parameter. |
| 145 False to bind the server only to the port in the |
| 146 server_address parameter, but not to the hostname. |
| 147 :param latency: Delay in ms to wait before seving each response, or |
| 148 callable that returns a delay in ms |
| 149 """ |
| 150 self.router = router |
| 151 self.rewriter = rewriter |
| 152 |
| 153 self.scheme = "https" if use_ssl else "http" |
| 154 self.logger = get_logger() |
| 155 |
| 156 self.latency = latency |
| 157 |
| 158 if bind_hostname: |
| 159 hostname_port = server_address |
| 160 else: |
| 161 hostname_port = ("",server_address[1]) |
| 162 |
| 163 #super doesn't work here because BaseHTTPServer.HTTPServer is old-style |
| 164 BaseHTTPServer.HTTPServer.__init__(self, hostname_port, RequestHandlerCl
ass, **kwargs) |
| 165 |
| 166 if config is not None: |
| 167 Server.config = config |
| 168 else: |
| 169 self.logger.debug("Using default configuration") |
| 170 Server.config = {"host": server_address[0], |
| 171 "domains": {"": server_address[0]}, |
| 172 "ports": {"http": [self.server_address[1]]}} |
| 173 |
| 174 |
| 175 self.key_file = key_file |
| 176 self.certificate = certificate |
| 177 self.encrypt_after_connect = use_ssl and encrypt_after_connect |
| 178 |
| 179 if use_ssl and not encrypt_after_connect: |
| 180 self.socket = ssl.wrap_socket(self.socket, |
| 181 keyfile=self.key_file, |
| 182 certfile=self.certificate, |
| 183 server_side=True) |
| 184 |
| 185 def handle_error(self, request, client_address): |
| 186 error = sys.exc_value |
| 187 |
| 188 if ((isinstance(error, socket.error) and |
| 189 isinstance(error.args, tuple) and |
| 190 error.args[0] in self.acceptable_errors) |
| 191 or |
| 192 (isinstance(error, IOError) and |
| 193 error.errno in self.acceptable_errors)): |
| 194 pass # remote hang up before the result is sent |
| 195 else: |
| 196 self.logger.error(traceback.format_exc()) |
| 197 |
| 198 |
| 199 class WebTestRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
| 200 """RequestHandler for WebTestHttpd""" |
| 201 |
| 202 protocol_version = "HTTP/1.1" |
| 203 |
| 204 def handle_one_request(self): |
| 205 response = None |
| 206 self.logger = get_logger() |
| 207 try: |
| 208 self.close_connection = False |
| 209 request_line_is_valid = self.get_request_line() |
| 210 |
| 211 if self.close_connection: |
| 212 return |
| 213 |
| 214 request_is_valid = self.parse_request() |
| 215 if not request_is_valid: |
| 216 #parse_request() actually sends its own error responses |
| 217 return |
| 218 |
| 219 self.server.rewriter.rewrite(self) |
| 220 |
| 221 request = Request(self) |
| 222 response = Response(self, request) |
| 223 |
| 224 if request.method == "CONNECT": |
| 225 self.handle_connect(response) |
| 226 return |
| 227 |
| 228 if not request_line_is_valid: |
| 229 response.set_error(414) |
| 230 response.write() |
| 231 return |
| 232 |
| 233 self.logger.debug("%s %s" % (request.method, request.request_path)) |
| 234 handler = self.server.router.get_handler(request) |
| 235 |
| 236 if self.server.latency is not None: |
| 237 if callable(self.server.latency): |
| 238 latency = self.server.latency() |
| 239 else: |
| 240 latency = self.server.latency |
| 241 self.logger.warning("Latency enabled. Sleeping %i ms" % latency) |
| 242 time.sleep(latency / 1000.) |
| 243 |
| 244 if handler is None: |
| 245 response.set_error(404) |
| 246 else: |
| 247 try: |
| 248 handler(request, response) |
| 249 except HTTPException as e: |
| 250 response.set_error(e.code, e.message) |
| 251 except Exception as e: |
| 252 if e.message: |
| 253 err = [e.message] |
| 254 else: |
| 255 err = [] |
| 256 err.append(traceback.format_exc()) |
| 257 response.set_error(500, "\n".join(err)) |
| 258 self.logger.debug("%i %s %s (%s) %i" % (response.status[0], |
| 259 request.method, |
| 260 request.request_path, |
| 261 request.headers.get('Referer
'), |
| 262 request.raw_input.length)) |
| 263 |
| 264 if not response.writer.content_written: |
| 265 response.write() |
| 266 |
| 267 # If we want to remove this in the future, a solution is needed for |
| 268 # scripts that produce a non-string iterable of content, since these |
| 269 # can't set a Content-Length header. A notable example of this kind
of |
| 270 # problem is with the trickle pipe i.e. foo.js?pipe=trickle(d1) |
| 271 if response.close_connection: |
| 272 self.close_connection = True |
| 273 |
| 274 if not self.close_connection: |
| 275 # Ensure that the whole request has been read from the socket |
| 276 request.raw_input.read() |
| 277 |
| 278 except socket.timeout, e: |
| 279 self.log_error("Request timed out: %r", e) |
| 280 self.close_connection = True |
| 281 return |
| 282 |
| 283 except Exception as e: |
| 284 err = traceback.format_exc() |
| 285 if response: |
| 286 response.set_error(500, err) |
| 287 response.write() |
| 288 self.logger.error(err) |
| 289 |
| 290 def get_request_line(self): |
| 291 try: |
| 292 self.raw_requestline = self.rfile.readline(65537) |
| 293 except socket.error: |
| 294 self.close_connection = True |
| 295 return False |
| 296 if len(self.raw_requestline) > 65536: |
| 297 self.requestline = '' |
| 298 self.request_version = '' |
| 299 self.command = '' |
| 300 return False |
| 301 if not self.raw_requestline: |
| 302 self.close_connection = True |
| 303 return True |
| 304 |
| 305 def handle_connect(self, response): |
| 306 self.logger.debug("Got CONNECT") |
| 307 response.status = 200 |
| 308 response.write() |
| 309 if self.server.encrypt_after_connect: |
| 310 self.logger.debug("Enabling SSL for connection") |
| 311 self.request = ssl.wrap_socket(self.connection, |
| 312 keyfile=self.server.key_file, |
| 313 certfile=self.server.certificate, |
| 314 server_side=True) |
| 315 self.setup() |
| 316 return |
| 317 |
| 318 |
| 319 class WebTestHttpd(object): |
| 320 """ |
| 321 :param host: Host from which to serve (default: 127.0.0.1) |
| 322 :param port: Port from which to serve (default: 8000) |
| 323 :param server_cls: Class to use for the server (default depends on ssl vs no
n-ssl) |
| 324 :param handler_cls: Class to use for the RequestHandler |
| 325 :param use_ssl: Use a SSL server if no explicit server_cls is supplied |
| 326 :param key_file: Path to key file to use if ssl is enabled |
| 327 :param certificate: Path to certificate file to use if ssl is enabled |
| 328 :param encrypt_after_connect: For each connection, don't start encryption |
| 329 until a CONNECT message has been received. |
| 330 This enables the server to act as a |
| 331 self-proxy. |
| 332 :param router_cls: Router class to use when matching URLs to handlers |
| 333 :param doc_root: Document root for serving files |
| 334 :param routes: List of routes with which to initialize the router |
| 335 :param rewriter_cls: Class to use for request rewriter |
| 336 :param rewrites: List of rewrites with which to initialize the rewriter_cls |
| 337 :param config: Dictionary holding environment configuration settings for |
| 338 handlers to read, or None to use the default values. |
| 339 :param bind_hostname: Boolean indicating whether to bind server to hostname. |
| 340 :param latency: Delay in ms to wait before seving each response, or |
| 341 callable that returns a delay in ms |
| 342 |
| 343 HTTP server designed for testing scenarios. |
| 344 |
| 345 Takes a router class which provides one method get_handler which takes a Req
uest |
| 346 and returns a handler function. |
| 347 |
| 348 .. attribute:: host |
| 349 |
| 350 The host name or ip address of the server |
| 351 |
| 352 .. attribute:: port |
| 353 |
| 354 The port on which the server is running |
| 355 |
| 356 .. attribute:: router |
| 357 |
| 358 The Router object used to associate requests with resources for this serve
r |
| 359 |
| 360 .. attribute:: rewriter |
| 361 |
| 362 The Rewriter object used for URL rewriting |
| 363 |
| 364 .. attribute:: use_ssl |
| 365 |
| 366 Boolean indicating whether the server is using ssl |
| 367 |
| 368 .. attribute:: started |
| 369 |
| 370 Boolean indictaing whether the server is running |
| 371 |
| 372 """ |
| 373 def __init__(self, host="127.0.0.1", port=8000, |
| 374 server_cls=None, handler_cls=WebTestRequestHandler, |
| 375 use_ssl=False, key_file=None, certificate=None, encrypt_after_c
onnect=False, |
| 376 router_cls=Router, doc_root=os.curdir, routes=None, |
| 377 rewriter_cls=RequestRewriter, bind_hostname=True, rewrites=None
, |
| 378 latency=None, config=None): |
| 379 |
| 380 if routes is None: |
| 381 routes = default_routes.routes |
| 382 |
| 383 self.host = host |
| 384 |
| 385 self.router = router_cls(doc_root, routes) |
| 386 self.rewriter = rewriter_cls(rewrites if rewrites is not None else []) |
| 387 |
| 388 self.use_ssl = use_ssl |
| 389 self.logger = get_logger() |
| 390 |
| 391 if server_cls is None: |
| 392 server_cls = WebTestServer |
| 393 |
| 394 if use_ssl: |
| 395 if key_file is not None: |
| 396 assert os.path.exists(key_file) |
| 397 assert certificate is not None and os.path.exists(certificate) |
| 398 |
| 399 try: |
| 400 self.httpd = server_cls((host, port), |
| 401 handler_cls, |
| 402 self.router, |
| 403 self.rewriter, |
| 404 config=config, |
| 405 bind_hostname=bind_hostname, |
| 406 use_ssl=use_ssl, |
| 407 key_file=key_file, |
| 408 certificate=certificate, |
| 409 encrypt_after_connect=encrypt_after_connect, |
| 410 latency=latency) |
| 411 self.started = False |
| 412 |
| 413 _host, self.port = self.httpd.socket.getsockname() |
| 414 except Exception: |
| 415 self.logger.error('Init failed! You may need to modify your hosts fi
le. Refer to README.md.'); |
| 416 raise |
| 417 |
| 418 def start(self, block=False): |
| 419 """Start the server. |
| 420 |
| 421 :param block: True to run the server on the current thread, blocking, |
| 422 False to run on a separate thread.""" |
| 423 self.logger.info("Starting http server on %s:%s" % (self.host, self.port
)) |
| 424 self.started = True |
| 425 if block: |
| 426 self.httpd.serve_forever() |
| 427 else: |
| 428 self.server_thread = threading.Thread(target=self.httpd.serve_foreve
r) |
| 429 self.server_thread.setDaemon(True) # don't hang on exit |
| 430 self.server_thread.start() |
| 431 |
| 432 def stop(self): |
| 433 """ |
| 434 Stops the server. |
| 435 |
| 436 If the server is not running, this method has no effect. |
| 437 """ |
| 438 if self.started: |
| 439 try: |
| 440 self.httpd.shutdown() |
| 441 self.httpd.server_close() |
| 442 self.server_thread.join() |
| 443 self.server_thread = None |
| 444 self.logger.info("Stopped http server on %s:%s" % (self.host, se
lf.port)) |
| 445 except AttributeError: |
| 446 pass |
| 447 self.started = False |
| 448 self.httpd = None |
| 449 |
| 450 def get_url(self, path="/", query=None, fragment=None): |
| 451 if not self.started: |
| 452 return None |
| 453 |
| 454 return urlparse.urlunsplit(("http" if not self.use_ssl else "https", |
| 455 "%s:%s" % (self.host, self.port), |
| 456 path, query, fragment)) |
OLD | NEW |