| Index: Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/server.py
|
| diff --git a/Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/server.py b/Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/server.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..78b982c49702bf3b546fa949cce70e93c044eef6
|
| --- /dev/null
|
| +++ b/Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/server.py
|
| @@ -0,0 +1,456 @@
|
| +import BaseHTTPServer
|
| +import errno
|
| +import os
|
| +import re
|
| +import socket
|
| +from SocketServer import ThreadingMixIn
|
| +import ssl
|
| +import sys
|
| +import threading
|
| +import time
|
| +import traceback
|
| +import types
|
| +import urlparse
|
| +
|
| +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_value
|
| +
|
| + 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 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, 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))
|
|
|