diff options
Diffstat (limited to 'testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py')
-rwxr-xr-x | testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py new file mode 100755 index 000000000..4ca0847d2 --- /dev/null +++ b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py @@ -0,0 +1,330 @@ +#!/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() |