Last active
November 21, 2019 16:46
-
-
Save Leedehai/0e63736d2aec37c90d061ebfabee6b93 to your computer and use it in GitHub Desktop.
Simple HTTP server, enhanced
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# Copyright holder unknown, from https://www.acmesystems.it/python_http | |
# Licensed under the CC-BY-SA 4.0 License. | |
# Copyright holder velis, from https://stackoverflow.com/a/17064025/8385554 | |
# Licensed under the CC-BY-SA 4.0 License. | |
# Copyright (c) 2019 Leedehai. All rights reserved. | |
# Licensed under the MIT License. For details, refer to LICENSE.txt | |
# under the project root. | |
# | |
# File: http-server.py | |
# --------------------------- | |
# Simple HTTP static server. | |
# For usage: ./http-server.py --help | |
# | |
# In rare cases, attempting to shutdown the server using KeyboardInterrupt | |
# will result in an socket error (https://bugs.python.org/issue31639), but | |
# the Python patch may not be available in your Python installation. | |
import os, sys | |
import mimetypes | |
import time, datetime | |
import argparse | |
import gzip, io | |
if sys.version_info.major == 2: | |
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer | |
from urlparse import urlparse | |
else: | |
from http.server import BaseHTTPRequestHandler, HTTPServer | |
from urllib.parse import urlparse | |
# https://www.iana.org/assignments/media-types/media-types.xhtml | |
def getMimeType(pathname): | |
mimeType = mimetypes.guess_type(pathname)[0] | |
if mimeType != None: | |
return mimeType | |
# "application/octet-stream" triggers a file download: not advised | |
return "text/plain" | |
def gzip_enc(content_bytes): | |
out = io.BytesIO() | |
with gzip.GzipFile(fileobj=out, mode='wb', compresslevel=5) as f: | |
f.write(content_bytes) | |
return out.getvalue() | |
def extract_path(s): | |
return urlparse(s).path | |
# 'state': a ServerHandlerClass instance reference | |
def log(quiet, state, result): | |
if quiet == True: | |
return | |
t = datetime.datetime.now() | |
sys.stdout.write("\r{time} {http_version} \"\x1b[36m{cmd} {path}\x1b[0m\" : {result}\n".format( | |
time = "%02d:%02d:%04.1f" % (t.hour, t.minute, t.second + t.microsecond / 1e6), | |
http_version = state.protocol_version, | |
cmd = state.command, | |
path = state.path, | |
result = result | |
)) | |
sys.stdout.write("Ready.. ") # no newline | |
sys.stdout.flush() | |
def httpServerHandlerFactory(server_base, fallback_file, gzip_limit, cache_time, quiet): | |
class ServerHandlerClass(BaseHTTPRequestHandler): | |
def log_message(self, format, *args): # override | |
return | |
def do_GET(self): # override: GET requests | |
start_time = time.time() | |
pathname = os.path.relpath(os.path.normpath( | |
os.path.join(server_base, "./%s" % extract_path(self.path)))) | |
if not os.path.isfile(pathname): | |
pathname = os.path.join(pathname, "index.html") | |
mimeType = getMimeType(pathname) | |
use_fallback = False | |
if not os.path.isfile(pathname): | |
if fallback_file: | |
pathname = os.path.join(server_base, fallback_file) | |
mimeType, use_fallback = getMimeType(pathname), True | |
else: | |
self.send_error(404, "Resource Not Found: \"%s\"" % self.path) | |
log(quiet, self, | |
"\x1b[33m404 NotFound\x1b[0m \x1b[37;2m(%.2f ms)\x1b[0m" % ( | |
1e3 * (time.time() - start_time))) | |
return | |
with open(pathname, 'rb') as f: | |
content_bytes = f.read() | |
content_len = len(content_bytes) | |
self.send_response(200) | |
self.send_header("Content-Type", mimeType) | |
self.send_header("Max-Age", cache_time) | |
if gzip_limit >= 0 and content_len > gzip_limit: | |
if "gzip" in self.headers.get("Accept-Encoding", []): | |
content_bytes = gzip_enc(content_bytes) | |
content_len = len(content_bytes) | |
self.send_header("Content-Encoding", "gzip") | |
self.send_header("Content-Length", str(content_len)) | |
self.end_headers() | |
self.wfile.write(content_bytes) | |
message = "200 OK \x1b[37;2m(%.2f KB, %.2f ms)\x1b[0m" % ( | |
content_len / 1024.0, 1e3 * (time.time() - start_time)) | |
log(quiet, self, message + ((" with %s" % os.path.basename(fallback_file)) if use_fallback else "")) | |
return | |
return ServerHandlerClass | |
def main(): | |
parser = argparse.ArgumentParser(description = "Simple HTTP server, enhanced", | |
epilog="Shut down server: Ctrl+C") | |
parser.add_argument("--quiet", action="store_true", | |
help="do not print logs") | |
parser.add_argument("-r", "--root", metavar="PATH", type=str, default=".", | |
help="the root path, default: .") | |
parser.add_argument("-a", "--addr", type=str, default="localhost", | |
help="the host address, default: localhost") | |
parser.add_argument("-p", "--port", type=int, default=8080, | |
help="the host port, default: 8080") | |
parser.add_argument("--cache", metavar="TIME", type=int, default=0, | |
help="browser cache time in seconds, default: 0") | |
parser.add_argument("--fallback", metavar="FILE", type=str, default="", | |
help="if resource is not found, serve this file under root") | |
parser.add_argument("--gzip", metavar="SIZE", type=int, default=None, | |
help="if resource is larger than SIZE bytes, gzip it") | |
args = parser.parse_args() | |
if not os.path.isdir(args.root): | |
sys.exit("[Error] root path not found: %s" % args.root) | |
server_base = os.path.normpath(os.path.join(os.getcwd(), args.root)) | |
fallback_file = os.path.join(server_base, args.fallback) if len(args.fallback) else None | |
if fallback_file and not os.path.isfile(fallback_file): | |
sys.exit("[Error] fallback file not found: %s" % os.path.relpath(fallback_file)) | |
if args.cache < 0: | |
sys.exit("[Exit] cache time (sec) for '--cache' should be non-negative") | |
if args.gzip != None and args.gzip < 0: | |
sys.exit("[Exit] size threshold (bytes) for '--gzip' should be non-negative") | |
ServerHandlerClass = httpServerHandlerFactory( | |
server_base, fallback_file, args.gzip, args.cache, args.quiet) | |
try: | |
serverd = HTTPServer((args.addr, args.port), ServerHandlerClass) | |
except Exception as e: | |
sys.exit(str(e)) | |
print("Serving %s at \x1b[32mhttp://%s:%s\x1b[0m" % ( | |
os.path.relpath(server_base), args.addr, args.port)) | |
print("\tcache control: max-age %d sec" % args.cache) | |
print("\tresource fallback: %s" % (os.path.relpath(fallback_file) if fallback_file else "N/A")) | |
print("\tgzip threshold: " + (("%d bytes" % args.gzip) if args.gzip != None else "N/A")) | |
sys.stdout.write("Ready.. ") # no newline | |
sys.stdout.flush() | |
try: | |
serverd.serve_forever() | |
except KeyboardInterrupt: | |
print(" \x1b[1mServer shut down.\x1b[0m") | |
serverd.shutdown() | |
serverd.socket.close() | |
if __name__ == "__main__": | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Suitable to test Single Page Application with history-based client-side routing, because it provides a resource fallback mechanism: 404.html's script could redirect the browser back to index.html (or other locations), optionally with parameters.


