Source code

Revision control

Copy as Markdown

Other Tools

#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import errno
import logging
import os
import posixpath
import re
import socket
import sys
import threading
import time
import traceback
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
import moznetwork
from six import ensure_binary, iteritems
from six.moves.BaseHTTPServer import HTTPServer
from six.moves.SimpleHTTPServer import SimpleHTTPRequestHandler
from six.moves.socketserver import ThreadingMixIn
from six.moves.urllib.parse import unquote, urlsplit
class EasyServer(ThreadingMixIn, HTTPServer):
allow_reuse_address = True
acceptable_errors = (errno.EPIPE, errno.ECONNABORTED)
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:
logging.error(error)
# The error can be ambiguous just the short description is logged, so we
# dump a stack trace to discover its origin.
traceback.print_exc()
class Request(object):
"""Details of a request."""
# attributes from urlsplit that this class also sets
uri_attrs = ("scheme", "netloc", "path", "query", "fragment")
def __init__(self, uri, headers, rfile=None):
self.uri = uri
self.headers = headers
parsed = urlsplit(uri)
for i, attr in enumerate(self.uri_attrs):
setattr(self, attr, parsed[i])
try:
body_len = int(self.headers.get("Content-length", 0))
except ValueError:
body_len = 0
if body_len and rfile:
self.body = rfile.read(body_len)
else:
self.body = None
class RequestHandler(SimpleHTTPRequestHandler):
docroot = os.getcwd() # current working directory at time of import
proxy_host_dirs = False
request_log = []
log_requests = False
request = None
def __init__(self, *args, **kwargs):
SimpleHTTPRequestHandler.__init__(self, *args, **kwargs)
self.extensions_map[".svg"] = "image/svg+xml"
def _try_handler(self, method):
if self.log_requests:
self.request_log.append(
{"method": method, "path": self.request.path, "time": time.time()}
)
handlers = [
handler for handler in self.urlhandlers if handler["method"] == method
]
for handler in handlers:
m = re.match(handler["path"], self.request.path)
if m:
(response_code, headerdict, data) = handler["function"](
self.request, *m.groups()
)
self.send_response(response_code)
for keyword, value in iteritems(headerdict):
self.send_header(keyword, value)
self.end_headers()
self.wfile.write(ensure_binary(data))
return True
return False
def _find_path(self):
"""Find the on-disk path to serve this request from,
using self.path_mappings and self.docroot.
Return (url_path, disk_path)."""
path_components = list(filter(None, self.request.path.split("/")))
for prefix, disk_path in iteritems(self.path_mappings):
prefix_components = list(filter(None, prefix.split("/")))
if len(path_components) < len(prefix_components):
continue
if path_components[: len(prefix_components)] == prefix_components:
return ("/".join(path_components[len(prefix_components) :]), disk_path)
if self.docroot:
return self.request.path, self.docroot
return None
def parse_request(self):
retval = SimpleHTTPRequestHandler.parse_request(self)
self.request = Request(self.path, self.headers, self.rfile)
return retval
def do_GET(self):
if not self._try_handler("GET"):
res = self._find_path()
if res:
self.path, self.disk_root = res
# don't include query string and fragment, and prepend
# host directory if required.
if self.request.netloc and self.proxy_host_dirs:
self.path = "/" + self.request.netloc + self.path
SimpleHTTPRequestHandler.do_GET(self)
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b"")
def do_POST(self):
# if we don't have a match, we always fall through to 404 (this may
# not be "technically" correct if we have a local file at the same
# path as the resource but... meh)
if not self._try_handler("POST"):
self.send_response(404)
self.end_headers()
self.wfile.write(b"")
def do_DEL(self):
# if we don't have a match, we always fall through to 404 (this may
# not be "technically" correct if we have a local file at the same
# path as the resource but... meh)
if not self._try_handler("DEL"):
self.send_response(404)
self.end_headers()
self.wfile.write(b"")
def translate_path(self, path):
# this is taken from SimpleHTTPRequestHandler.translate_path(),
# except we serve from self.docroot instead of os.getcwd(), and
# parse_request()/do_GET() have already stripped the query string and
# fragment and mangled the path for proxying, if required.
path = posixpath.normpath(unquote(self.path))
words = path.split("/")
words = list(filter(None, words))
path = self.disk_root
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
if word in (os.curdir, os.pardir):
continue
path = os.path.join(path, word)
return path
# I found on my local network that calls to this were timing out
# I believe all of these calls are from log_message
def address_string(self):
return "a.b.c.d"
# This produces a LOT of noise
def log_message(self, format, *args):
pass
class MozHttpd(object):
"""
:param host: Host from which to serve (default 127.0.0.1)
:param port: Port from which to serve (default 8888)
:param docroot: Server root (default os.getcwd())
:param urlhandlers: Handlers to specify behavior against method and path match (default None)
:param path_mappings: A dict mapping URL prefixes to additional on-disk paths.
:param proxy_host_dirs: Toggle proxy behavior (default False)
:param log_requests: Toggle logging behavior (default False)
Very basic HTTP server class. Takes a docroot (path on the filesystem)
and a set of urlhandler dictionaries of the form:
::
{
'method': HTTP method (string): GET, POST, or DEL,
'path': PATH_INFO (regular expression string),
'function': function of form fn(arg1, arg2, arg3, ..., request)
}
and serves HTTP. For each request, MozHttpd will either return a file
off the docroot, or dispatch to a handler function (if both path and
method match).
Note that one of docroot or urlhandlers may be None (in which case no
local files or handlers, respectively, will be used). If both docroot or
urlhandlers are None then MozHttpd will default to serving just the local
directory.
MozHttpd also handles proxy requests (i.e. with a full URI on the request
line). By default files are served from docroot according to the request
URI's path component, but if proxy_host_dirs is True, files are served
from <self.docroot>/<host>/.
For example, the request "GET http://foo.bar/dir/file.html" would
(assuming no handlers match) serve <docroot>/dir/file.html if
proxy_host_dirs is False, or <docroot>/foo.bar/dir/file.html if it is
True.
"""
def __init__(
self,
host="127.0.0.1",
port=0,
docroot=None,
urlhandlers=None,
path_mappings=None,
proxy_host_dirs=False,
log_requests=False,
):
self.host = host
self.port = int(port)
self.docroot = docroot
if not (urlhandlers or docroot or path_mappings):
self.docroot = os.getcwd()
self.proxy_host_dirs = proxy_host_dirs
self.httpd = None
self.urlhandlers = urlhandlers or []
self.path_mappings = path_mappings or {}
self.log_requests = log_requests
self.request_log = []
class RequestHandlerInstance(RequestHandler):
docroot = self.docroot
urlhandlers = self.urlhandlers
path_mappings = self.path_mappings
proxy_host_dirs = self.proxy_host_dirs
request_log = self.request_log
log_requests = self.log_requests
self.handler_class = RequestHandlerInstance
def start(self, block=False):
"""
Starts the server.
If `block` is True, the call will not return. If `block` is False, the
server will be started on a separate thread that can be terminated by
a call to stop().
"""
self.httpd = EasyServer((self.host, self.port), self.handler_class)
if block:
self.httpd.serve_forever()
else:
self.server = threading.Thread(target=self.httpd.serve_forever)
self.server.setDaemon(True) # don't hang on exit
self.server.start()
def stop(self):
"""
Stops the server.
If the server is not running, this method has no effect.
"""
if self.httpd:
# FIXME: There is no shutdown() method in Python 2.4...
try:
self.httpd.shutdown()
except AttributeError:
pass
self.httpd = None
def get_url(self, path="/"):
"""
Returns a URL that can be used for accessing the server (e.g. http://192.168.1.3:4321/)
:param path: Path to append to URL (e.g. if path were /foobar.html you would get a URL like
"""
if not self.httpd:
return None
return "http://%s:%s%s" % (self.host, self.httpd.server_port, path)
__del__ = stop
def main(args=sys.argv[1:]):
# parse command line options
parser = ArgumentParser(
description="Basic python webserver.",
formatter_class=ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8888,
help="port to run the server on",
)
parser.add_argument(
"-H", "--host", dest="host", default="127.0.0.1", help="host address"
)
parser.add_argument(
"-i",
"--external-ip",
action="store_true",
dest="external_ip",
default=False,
help="find and use external ip for host",
)
parser.add_argument(
"-d",
"--docroot",
dest="docroot",
default=os.getcwd(),
help="directory to serve files from",
)
args = parser.parse_args()
if args.external_ip:
host = moznetwork.get_lan_ip()
else:
host = args.host
# create the server
server = MozHttpd(host=host, port=args.port, docroot=args.docroot)
print("Serving '%s' at %s:%s" % (server.docroot, server.host, server.port))
server.start(block=True)
if __name__ == "__main__":
main()