diff options
Diffstat (limited to 'testing/web-platform/tests/tools/wptserve/wptserve/server.py')
-rw-r--r-- | testing/web-platform/tests/tools/wptserve/wptserve/server.py | 461 |
1 files changed, 461 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/wptserve/wptserve/server.py b/testing/web-platform/tests/tools/wptserve/wptserve/server.py new file mode 100644 index 000000000..31929efd6 --- /dev/null +++ b/testing/web-platform/tests/tools/wptserve/wptserve/server.py @@ -0,0 +1,461 @@ +import BaseHTTPServer +import errno +import os +import socket +from SocketServer import ThreadingMixIn +import ssl +import sys +import threading +import time +import traceback +import types +import urlparse + +from . import routes as default_routes +from .logger import get_logger +from .request import Server, Request +from .response import Response +from .router import Router +from .utils import HTTPException + + +"""HTTP server designed for testing purposes. + +The server is designed to provide flexibility in the way that +requests are handled, and to provide control both of exactly +what bytes are put on the wire for the response, and in the +timing of sending those bytes. + +The server is based on the stdlib HTTPServer, but with some +notable differences in the way that requests are processed. +Overall processing is handled by a WebTestRequestHandler, +which is a subclass of BaseHTTPRequestHandler. This is responsible +for parsing the incoming request. A RequestRewriter is then +applied and may change the request data if it matches a +supplied rule. + +Once the request data had been finalised, Request and Reponse +objects are constructed. These are used by the other parts of the +system to read information about the request and manipulate the +response. + +Each request is handled by a particular handler function. The +mapping between Request and the appropriate handler is determined +by a Router. By default handlers are installed to interpret files +under the document root with .py extensions as executable python +files (see handlers.py for the api for such files), .asis files as +bytestreams to be sent literally and all other files to be served +statically. + +The handler functions are responsible for either populating the +fields of the response object, which will then be written when the +handler returns, or for directly writing to the output stream. +""" + + +class RequestRewriter(object): + def __init__(self, rules): + """Object for rewriting the request path. + + :param rules: Initial rules to add; a list of three item tuples + (method, input_path, output_path), defined as for + register() + """ + self.rules = {} + for rule in reversed(rules): + self.register(*rule) + self.logger = get_logger() + + def register(self, methods, input_path, output_path): + """Register a rewrite rule. + + :param methods: Set of methods this should match. "*" is a + special value indicating that all methods should + be matched. + + :param input_path: Path to match for the initial request. + + :param output_path: Path to replace the input path with in + the request. + """ + if type(methods) in types.StringTypes: + methods = [methods] + self.rules[input_path] = (methods, output_path) + + def rewrite(self, request_handler): + """Rewrite the path in a BaseHTTPRequestHandler instance, if + it matches a rule. + + :param request_handler: BaseHTTPRequestHandler for which to + rewrite the request. + """ + split_url = urlparse.urlsplit(request_handler.path) + if split_url.path in self.rules: + methods, destination = self.rules[split_url.path] + if "*" in methods or request_handler.command in methods: + self.logger.debug("Rewriting request path %s to %s" % + (request_handler.path, destination)) + new_url = list(split_url) + new_url[2] = destination + new_url = urlparse.urlunsplit(new_url) + request_handler.path = new_url + + +class WebTestServer(ThreadingMixIn, BaseHTTPServer.HTTPServer): + allow_reuse_address = True + acceptable_errors = (errno.EPIPE, errno.ECONNABORTED) + request_queue_size = 2000 + + # Ensure that we don't hang on shutdown waiting for requests + daemon_threads = True + + def __init__(self, server_address, RequestHandlerClass, router, rewriter, bind_hostname, + config=None, use_ssl=False, key_file=None, certificate=None, + encrypt_after_connect=False, latency=None, **kwargs): + """Server for HTTP(s) Requests + + :param server_address: tuple of (server_name, port) + + :param RequestHandlerClass: BaseHTTPRequestHandler-like class to use for + handling requests. + + :param router: Router instance to use for matching requests to handler + functions + + :param rewriter: RequestRewriter-like instance to use for preprocessing + requests before they are routed + + :param config: Dictionary holding environment configuration settings for + handlers to read, or None to use the default values. + + :param use_ssl: Boolean indicating whether the server should use SSL + + :param key_file: Path to key file to use if SSL is enabled. + + :param certificate: Path to certificate to use if SSL is enabled. + + :param encrypt_after_connect: For each connection, don't start encryption + until a CONNECT message has been received. + This enables the server to act as a + self-proxy. + + :param bind_hostname True to bind the server to both the hostname and + port specified in the server_address parameter. + False to bind the server only to the port in the + server_address parameter, but not to the hostname. + :param latency: Delay in ms to wait before seving each response, or + callable that returns a delay in ms + """ + self.router = router + self.rewriter = rewriter + + self.scheme = "https" if use_ssl else "http" + self.logger = get_logger() + + self.latency = latency + + if bind_hostname: + hostname_port = server_address + else: + hostname_port = ("",server_address[1]) + + #super doesn't work here because BaseHTTPServer.HTTPServer is old-style + BaseHTTPServer.HTTPServer.__init__(self, hostname_port, RequestHandlerClass, **kwargs) + + if config is not None: + Server.config = config + else: + self.logger.debug("Using default configuration") + Server.config = {"host": server_address[0], + "domains": {"": server_address[0]}, + "ports": {"http": [self.server_address[1]]}} + + + self.key_file = key_file + self.certificate = certificate + self.encrypt_after_connect = use_ssl and encrypt_after_connect + + if use_ssl and not encrypt_after_connect: + self.socket = ssl.wrap_socket(self.socket, + keyfile=self.key_file, + certfile=self.certificate, + server_side=True) + + def handle_error(self, request, client_address): + error = sys.exc_info()[1] + + if ((isinstance(error, socket.error) and + isinstance(error.args, tuple) and + error.args[0] in self.acceptable_errors) or + (isinstance(error, IOError) and + error.errno in self.acceptable_errors)): + pass # remote hang up before the result is sent + else: + self.logger.error(traceback.format_exc()) + + +class WebTestRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """RequestHandler for WebTestHttpd""" + + protocol_version = "HTTP/1.1" + + def handle_one_request(self): + response = None + self.logger = get_logger() + try: + self.close_connection = False + request_line_is_valid = self.get_request_line() + + if self.close_connection: + return + + request_is_valid = self.parse_request() + if not request_is_valid: + #parse_request() actually sends its own error responses + return + + self.server.rewriter.rewrite(self) + + request = Request(self) + response = Response(self, request) + + if request.method == "CONNECT": + self.handle_connect(response) + return + + if not request_line_is_valid: + response.set_error(414) + response.write() + return + + self.logger.debug("%s %s" % (request.method, request.request_path)) + handler = self.server.router.get_handler(request) + + # If the handler we used for the request had a non-default base path + # set update the doc_root of the request to reflect this + if hasattr(handler, "base_path") and handler.base_path: + request.doc_root = handler.base_path + if hasattr(handler, "url_base") and handler.url_base != "/": + request.url_base = handler.url_base + + if self.server.latency is not None: + if callable(self.server.latency): + latency = self.server.latency() + else: + latency = self.server.latency + self.logger.warning("Latency enabled. Sleeping %i ms" % latency) + time.sleep(latency / 1000.) + + if handler is None: + response.set_error(404) + else: + try: + handler(request, response) + except HTTPException as e: + response.set_error(e.code, e.message) + except Exception as e: + if e.message: + err = [e.message] + else: + err = [] + err.append(traceback.format_exc()) + response.set_error(500, "\n".join(err)) + self.logger.debug("%i %s %s (%s) %i" % (response.status[0], + request.method, + request.request_path, + request.headers.get('Referer'), + request.raw_input.length)) + + if not response.writer.content_written: + response.write() + + # If we want to remove this in the future, a solution is needed for + # scripts that produce a non-string iterable of content, since these + # can't set a Content-Length header. A notable example of this kind of + # problem is with the trickle pipe i.e. foo.js?pipe=trickle(d1) + if response.close_connection: + self.close_connection = True + + if not self.close_connection: + # Ensure that the whole request has been read from the socket + request.raw_input.read() + + except socket.timeout as e: + self.log_error("Request timed out: %r", e) + self.close_connection = True + return + + except Exception as e: + err = traceback.format_exc() + if response: + response.set_error(500, err) + response.write() + self.logger.error(err) + + def get_request_line(self): + try: + self.raw_requestline = self.rfile.readline(65537) + except socket.error: + self.close_connection = True + return False + if len(self.raw_requestline) > 65536: + self.requestline = '' + self.request_version = '' + self.command = '' + return False + if not self.raw_requestline: + self.close_connection = True + return True + + def handle_connect(self, response): + self.logger.debug("Got CONNECT") + response.status = 200 + response.write() + if self.server.encrypt_after_connect: + self.logger.debug("Enabling SSL for connection") + self.request = ssl.wrap_socket(self.connection, + keyfile=self.server.key_file, + certfile=self.server.certificate, + server_side=True) + self.setup() + return + + +class WebTestHttpd(object): + """ + :param host: Host from which to serve (default: 127.0.0.1) + :param port: Port from which to serve (default: 8000) + :param server_cls: Class to use for the server (default depends on ssl vs non-ssl) + :param handler_cls: Class to use for the RequestHandler + :param use_ssl: Use a SSL server if no explicit server_cls is supplied + :param key_file: Path to key file to use if ssl is enabled + :param certificate: Path to certificate file to use if ssl is enabled + :param encrypt_after_connect: For each connection, don't start encryption + until a CONNECT message has been received. + This enables the server to act as a + self-proxy. + :param router_cls: Router class to use when matching URLs to handlers + :param doc_root: Document root for serving files + :param routes: List of routes with which to initialize the router + :param rewriter_cls: Class to use for request rewriter + :param rewrites: List of rewrites with which to initialize the rewriter_cls + :param config: Dictionary holding environment configuration settings for + handlers to read, or None to use the default values. + :param bind_hostname: Boolean indicating whether to bind server to hostname. + :param latency: Delay in ms to wait before seving each response, or + callable that returns a delay in ms + + HTTP server designed for testing scenarios. + + Takes a router class which provides one method get_handler which takes a Request + and returns a handler function. + + .. attribute:: host + + The host name or ip address of the server + + .. attribute:: port + + The port on which the server is running + + .. attribute:: router + + The Router object used to associate requests with resources for this server + + .. attribute:: rewriter + + The Rewriter object used for URL rewriting + + .. attribute:: use_ssl + + Boolean indicating whether the server is using ssl + + .. attribute:: started + + Boolean indictaing whether the server is running + + """ + def __init__(self, host="127.0.0.1", port=8000, + server_cls=None, handler_cls=WebTestRequestHandler, + use_ssl=False, key_file=None, certificate=None, encrypt_after_connect=False, + router_cls=Router, doc_root=os.curdir, routes=None, + rewriter_cls=RequestRewriter, bind_hostname=True, rewrites=None, + latency=None, config=None): + + if routes is None: + routes = default_routes.routes + + self.host = host + + self.router = router_cls(doc_root, routes) + self.rewriter = rewriter_cls(rewrites if rewrites is not None else []) + + self.use_ssl = use_ssl + self.logger = get_logger() + + if server_cls is None: + server_cls = WebTestServer + + if use_ssl: + if key_file is not None: + assert os.path.exists(key_file) + assert certificate is not None and os.path.exists(certificate) + + try: + self.httpd = server_cls((host, port), + handler_cls, + self.router, + self.rewriter, + config=config, + bind_hostname=bind_hostname, + use_ssl=use_ssl, + key_file=key_file, + certificate=certificate, + encrypt_after_connect=encrypt_after_connect, + latency=latency) + self.started = False + + _host, self.port = self.httpd.socket.getsockname() + except Exception: + self.logger.error('Init failed! You may need to modify your hosts file. Refer to README.md.') + raise + + def start(self, block=False): + """Start the server. + + :param block: True to run the server on the current thread, blocking, + False to run on a separate thread.""" + self.logger.info("Starting http server on %s:%s" % (self.host, self.port)) + self.started = True + if block: + self.httpd.serve_forever() + else: + self.server_thread = threading.Thread(target=self.httpd.serve_forever) + self.server_thread.setDaemon(True) # don't hang on exit + self.server_thread.start() + + def stop(self): + """ + Stops the server. + + If the server is not running, this method has no effect. + """ + if self.started: + try: + self.httpd.shutdown() + self.httpd.server_close() + self.server_thread.join() + self.server_thread = None + self.logger.info("Stopped http server on %s:%s" % (self.host, self.port)) + except AttributeError: + pass + self.started = False + self.httpd = None + + def get_url(self, path="/", query=None, fragment=None): + if not self.started: + return None + + return urlparse.urlunsplit(("http" if not self.use_ssl else "https", + "%s:%s" % (self.host, self.port), + path, query, fragment)) |