DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (cea47a93ea3f)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
#!/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/.

from __future__ import absolute_import, print_function


import errno
import logging
import threading
import posixpath
import socket
import sys
import os
import re
import moznetwork
import time

from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from six import iteritems
from six.moves.socketserver import ThreadingMixIn
from six.moves.BaseHTTPServer import HTTPServer

from six.moves.urllib.parse import (
    urlsplit,
    unquote,
)
from six.moves.SimpleHTTPServer import SimpleHTTPRequestHandler


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)


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(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('')

    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(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
                     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
    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()