mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-23 04:43:07 +09:00
461 lines
18 KiB
Python
461 lines
18 KiB
Python
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))
|