Last active
June 19, 2024 21:51
-
-
Save brunobord/f9b5c57b4b10d4cb95d3410c004a0f40 to your computer and use it in GitHub Desktop.
Simple static server in Python
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/env python3 | |
""" | |
Usage: | |
./static.py # serve current directory on port 8080 | |
./static.py --port 9090 /path/to/serve | |
Why that? because `python -m http.server` can't serve a specified directory. | |
What's the use case? Let's say we're building a Sphinx documentation, situated | |
in the `build/html` directory. With the standard http.server lib, I **have** | |
to move to `build/html` directory and then run my HTTP server. And what happens | |
if I delete this directory, **and** recreate it by building the docs while my | |
server is on? It fails to serve files, it's lost. And `ctrl-c` it does not work | |
better, the shell is still lost in oblivion. | |
Can't stand this. | |
Works with Python 3, tested with Python 3.4, 3.5, 3.6. | |
Uses only the standard lib. | |
To use via HTTPS, use the ``--ssl`` option to point at a ".pem" file path. | |
You can create a self-signed file using the following command: | |
openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes | |
Warning: You'll probably have an "unsecure connection" error page in your | |
browser, with the code "SEC_ERROR_UNKNOWN_ISSUER". You can add an exception | |
(permanent or temporary) to access the static pages. | |
""" | |
import sys | |
import argparse | |
from contextlib import contextmanager | |
from os import getcwd, chdir | |
from os.path import abspath, isdir, isfile | |
from http.server import HTTPServer, HTTPStatus | |
from http.server import SimpleHTTPRequestHandler as BaseHTTPRequestHandler | |
import ssl | |
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): | |
OKBLUE = '\033[94m' | |
OKGREEN = '\033[92m' | |
FAIL = '\033[91m' | |
ENDC = '\033[0m' | |
def log_request(self, code='-', size='-'): | |
"""Log an accepted request. | |
This is called by send_response(). | |
""" | |
error = None | |
if isinstance(code, HTTPStatus): | |
code = code.value | |
if 100 <= code < 300: | |
error = False | |
elif 300 <= code < 400: | |
error = None | |
else: | |
error = True | |
self.log_message('"%s" %s %s', | |
self.requestline, str(code), str(size), | |
error=error) | |
def log_error(self, format, *args, error=None): | |
if format == "code %d, message %s": | |
return | |
self.log_message(format, *args, error=True) | |
def log_message(self, format, *args, error=None): | |
message = "%s - - [%s] %s\n" % ( | |
self.address_string(), | |
self.log_date_time_string(), | |
format % args) | |
if error is None: | |
# can't see a case where it would happend, but we don't know | |
color = self.OKBLUE | |
else: | |
if error: | |
color = self.FAIL | |
else: | |
color = self.OKGREEN | |
message = "{}{}{}".format(color, message, self.ENDC) | |
sys.stderr.write(message) | |
@contextmanager | |
def cd(dirname): | |
""" | |
Context manager to temporarily change current directory. | |
""" | |
_curdir = getcwd() | |
chdir(dirname) | |
yield | |
chdir(_curdir) | |
class RequestHandler(SimpleHTTPRequestHandler): | |
def _directory_dance(self): | |
# We're testing if the current directory still exists. | |
try: | |
getcwd() | |
except FileNotFoundError: | |
chdir('.') | |
root = getattr(self.server, '__root') | |
chdir(root) | |
def do_HEAD(self): | |
""" | |
Root-removal-tolerant HEAD method handling. | |
See do_GET for a little explanation. | |
""" | |
try: | |
self._directory_dance() | |
except FileNotFoundError: | |
self.send_error(404, "Root not found, come back later") | |
else: | |
return super().do_HEAD() | |
def do_GET(self): | |
""" | |
Root-removal-tolerant GET method. | |
TL;DR: trying to load a removed directory content doesn't please | |
getcwd(). Even if the directory is recreated. | |
* if the root directory exists, proceed. | |
* if it doesn't exist, return a 404 with a significant message. | |
* if it *has* existed and it was removed, and then recreated, will make | |
a little dance to try getting back to it and make it the current | |
directory again. | |
""" | |
try: | |
self._directory_dance() | |
except FileNotFoundError: | |
self.send_error(404, "Root not found, come back later") | |
else: | |
return super().do_GET() | |
def port(number): | |
""" | |
A port value is a number between 0 and 65535. | |
""" | |
try: | |
number = int(number) | |
except ValueError: | |
raise argparse.ArgumentTypeError( | |
"invalid int value: '{}'".format(number)) | |
if 0 <= number <= 65535: | |
return number | |
raise argparse.ArgumentTypeError("port must be 0-65535") | |
def certfile(path): | |
""" | |
A certfile is a string that points at an existing file. | |
""" | |
path = '{}'.format(path) | |
if path and not isfile(path): | |
raise argparse.ArgumentTypeError( | |
"`{}` is not a known file".format(path) | |
) | |
return path | |
def serve(root, port, certfile_path=None): | |
""" | |
Serve files | |
""" | |
scheme = 'http' | |
if certfile_path: | |
scheme = 'https' | |
root = abspath(root) | |
if not isdir(root): | |
print("Error: `{}` is not a directory".format(root)) | |
return | |
print( | |
"Serving `{root}`..." | |
"\nGo to: {scheme}://127.0.0.1:{port}".format( | |
scheme=scheme, root=root, port=port) | |
) | |
with cd(root): | |
server_address = ('', port) | |
httpd = HTTPServer(server_address, RequestHandler) | |
httpd.__root = root | |
if certfile_path: | |
httpd.socket = ssl.wrap_socket( | |
httpd.socket, | |
server_side=True, | |
certfile=certfile_path, | |
ssl_version=ssl.PROTOCOL_TLSv1 | |
) | |
try: | |
httpd.serve_forever() | |
except KeyboardInterrupt: | |
pass | |
print("Bye...") | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser() | |
parser.add_argument( | |
'root', nargs='?', default='.', | |
help="Path to serve statically") | |
parser.add_argument( | |
'--port', '-p', type=port, default=8080, | |
help="Port number (0-65535)") | |
parser.add_argument( | |
'--ssl', type=certfile, default=None, | |
help='Path to certfile (.pem format). In that case, you will need to' | |
' access the pages via HTTPS.' | |
) | |
args = parser.parse_args() | |
serve(args.root, args.port, args.ssl) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment