| #!/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 BaseHTTPServer |
| import SimpleHTTPServer |
| import errno |
| import logging |
| import threading |
| import posixpath |
| import socket |
| import sys |
| import os |
| import urllib |
| import urlparse |
| import re |
| import moznetwork |
| import time |
| from SocketServer import ThreadingMixIn |
| |
| class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer): |
| allow_reuse_address = True |
| acceptable_errors = (errno.EPIPE, errno.ECONNABORTED) |
| |
| 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: |
| logging.error(error) |
| |
| |
| 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 = urlparse.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(SimpleHTTPServer.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): |
| SimpleHTTPServer.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 headerdict.iteritems(): |
| self.send_header(keyword, value) |
| self.end_headers() |
| self.wfile.write(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 = filter(None, self.request.path.split('/')) |
| for prefix, disk_path in self.path_mappings.iteritems(): |
| prefix_components = 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 = SimpleHTTPServer.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 |
| SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) |
| else: |
| self.send_response(404) |
| self.end_headers() |
| self.wfile.write('') |
| |
| 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('') |
| |
| 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('') |
| |
| 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(urllib.unquote(self.path)) |
| words = path.split('/') |
| words = 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 |
| http://192.168.1.3:4321/foobar.html). Default is `/`. |
| """ |
| 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 |
| from optparse import OptionParser |
| parser = OptionParser() |
| parser.add_option('-p', '--port', dest='port', |
| type="int", default=8888, |
| help="port to run the server on [DEFAULT: %default]") |
| parser.add_option('-H', '--host', dest='host', |
| default='127.0.0.1', |
| help="host [DEFAULT: %default]") |
| parser.add_option('-i', '--external-ip', action="store_true", |
| dest='external_ip', default=False, |
| help="find and use external ip for host") |
| parser.add_option('-d', '--docroot', dest='docroot', |
| default=os.getcwd(), |
| help="directory to serve files from [DEFAULT: %default]") |
| options, args = parser.parse_args(args) |
| if args: |
| parser.error("mozhttpd does not take any arguments") |
| |
| if options.external_ip: |
| host = moznetwork.get_lan_ip() |
| else: |
| host = options.host |
| |
| # create the server |
| server = MozHttpd(host=host, port=options.port, docroot=options.docroot) |
| |
| print "Serving '%s' at %s:%s" % (server.docroot, server.host, server.port) |
| server.start(block=True) |
| |
| if __name__ == '__main__': |
| main() |