import argparse import logging import os import posixpath import ssl import sys import urllib from http import server try: from magic import from_file as magic_from_file except ImportError: magic_from_file = None from pelican.log import console # noqa: F401 from pelican.log import init as init_logging logger = logging.getLogger(__name__) def parse_arguments(): parser = argparse.ArgumentParser( description="Pelican Development Server", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "port", default=8000, type=int, nargs="?", help="Port to Listen On" ) parser.add_argument("server", default="", nargs="?", help="Interface to Listen On") parser.add_argument("--ssl", action="store_true", help="Activate SSL listener") parser.add_argument( "--cert", default="./cert.pem", nargs="?", help="Path to certificate file. " + "Relative to current directory", ) parser.add_argument( "--key", default="./key.pem", nargs="?", help="Path to certificate key file. " + "Relative to current directory", ) parser.add_argument( "--path", default=".", help="Path to pelican source directory to serve. " + "Relative to current directory", ) return parser.parse_args() class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler): SUFFIXES = [".html", "/index.html", "/", ""] extensions_map = { **server.SimpleHTTPRequestHandler.extensions_map, **{ # web fonts ".oft": "font/oft", ".sfnt": "font/sfnt", ".ttf": "font/ttf", ".woff": "font/woff", ".woff2": "font/woff2", }, } def translate_path(self, path): # abandon query parameters path = path.split("?", 1)[0] path = path.split("#", 1)[0] # Don't forget explicit trailing slash when normalizing. Issue17324 trailing_slash = path.rstrip().endswith("/") path = urllib.parse.unquote(path) path = posixpath.normpath(path) words = path.split("/") words = filter(None, words) path = self.base_path for word in words: if os.path.dirname(word) or word in (os.curdir, os.pardir): # Ignore components that are not a simple file/directory name continue path = os.path.join(path, word) if trailing_slash: path += "/" return path def do_GET(self): # cut off a query string original_path = self.path.split("?", 1)[0] # try to find file self.path = self.get_path_that_exists(original_path) if not self.path: return server.SimpleHTTPRequestHandler.do_GET(self) def get_path_that_exists(self, original_path): # Try to strip trailing slash trailing_slash = original_path.endswith("/") original_path = original_path.rstrip("/") # Try to detect file by applying various suffixes tries = [] for suffix in self.SUFFIXES: if not trailing_slash and suffix == "/": # if original request does not have trailing slash, skip the '/' suffix # so that base class can redirect if needed continue path = original_path + suffix if os.path.exists(self.translate_path(path)): return path tries.append(path) logger.warning( "Unable to find `%s` or variations:\n%s", original_path, "\n".join(tries) ) return None def guess_type(self, path): """Guess at the mime type for the specified file.""" mimetype = server.SimpleHTTPRequestHandler.guess_type(self, path) # If the default guess is too generic, try the python-magic library if mimetype == "application/octet-stream" and magic_from_file: mimetype = magic_from_file(path, mime=True) return mimetype def log_message(self, format, *args): logger.info(format, *args) class RootedHTTPServer(server.HTTPServer): def __init__(self, base_path, *args, **kwargs): server.HTTPServer.__init__(self, *args, **kwargs) self.RequestHandlerClass.base_path = base_path if __name__ == "__main__": init_logging(level=logging.INFO) logger.warning( "'python -m pelican.server' is deprecated.\nThe " "Pelican development server should be run via " "'pelican --listen' or 'pelican -l'.\nThis can be combined " "with regeneration as 'pelican -lr'.\nRerun 'pelican-" "quickstart' to get new Makefile and tasks.py files." ) args = parse_arguments() RootedHTTPServer.allow_reuse_address = True try: httpd = RootedHTTPServer( args.path, (args.server, args.port), ComplexHTTPRequestHandler ) if args.ssl: httpd.socket = ssl.wrap_socket( httpd.socket, keyfile=args.key, certfile=args.cert, server_side=True ) except ssl.SSLError as e: logger.error( "Couldn't open certificate file %s or key file %s", args.cert, args.key ) logger.error("Could not listen on port %s, server %s.", args.port, args.server) sys.exit(getattr(e, "exitcode", 1)) logger.info("Serving at port %s, server %s.", args.port, args.server) try: httpd.serve_forever() except KeyboardInterrupt: logger.info("Shutting down server.") httpd.socket.close()