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
|
#!/usr/bin/env python3
# This is a simple HTTP server based on the HTTPServer and
# SimpleHTTPRequestHandler. It has been extended with PUT
# and DELETE functionality to store or delete results.
#
# See: https://github.com/python/cpython/blob/main/Lib/http/server.py
import argparse
import base64
import os
import signal
import socket
import sys
from functools import partial
from http import HTTPStatus
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
class AuthenticationError(Exception):
pass
class PUTEnabledHTTPRequestHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, basic_auth=None, **kwargs):
self.basic_auth = None
if basic_auth:
self.basic_auth = base64.b64encode(basic_auth.encode("ascii")).decode(
"ascii"
)
super().__init__(*args, **kwargs)
def do_GET(self): # noqa: N802
try:
self._handle_auth()
super().do_GET()
except AuthenticationError:
self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
def do_HEAD(self): # noqa: N802
try:
self._handle_auth()
super().do_HEAD()
except AuthenticationError:
self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
def do_PUT(self): # noqa: N802
path = Path(self.translate_path(self.path))
path.parent.mkdir(parents=True, exist_ok=True)
try:
self._handle_auth()
file_length = int(self.headers["Content-Length"])
path.write_bytes(self.rfile.read(file_length))
self.send_response(HTTPStatus.CREATED)
self.send_header("Content-Length", "0")
self.end_headers()
except AuthenticationError:
self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
except OSError:
self.send_error(
HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot open file for writing"
)
def do_DELETE(self): # noqa: N802
path = Path(self.translate_path(self.path))
try:
self._handle_auth()
path.unlink()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Length", "0")
self.end_headers()
except AuthenticationError:
self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
except OSError:
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot delete file")
def _handle_auth(self):
if not self.basic_auth:
return
authorization = self.headers.get("authorization")
if authorization:
authorization = authorization.split()
if authorization == ["Basic", self.basic_auth]:
return
raise AuthenticationError("Authentication required")
def _get_best_family(*address):
infos = socket.getaddrinfo(
*address,
type=socket.SOCK_STREAM,
flags=socket.AI_PASSIVE,
)
family, _type, _proto, _canonname, sockaddr = next(iter(infos))
return family, sockaddr
def run(handler_class, server_class, port, bind):
handler_class.protocol_version = "HTTP/1.1"
server_class.address_family, addr = _get_best_family(bind, port)
with server_class(addr, handler_class) as httpd:
host, port = httpd.socket.getsockname()[:2]
url_host = f"[{host}]" if ":" in host else host
print(f"Serving HTTP on {host} port {port} (http://{url_host}:{port}/) ...")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nKeyboard interrupt received, exiting.")
sys.exit(0)
def on_terminate(signum, _frame):
sys.stdout.flush()
sys.stderr.flush()
sys.exit(128 + signum)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--basic-auth", "-B", help="Basic auth tuple like user:pass")
parser.add_argument(
"--bind",
"-b",
metavar="ADDRESS",
help="Specify alternate bind address [default: all interfaces]",
)
parser.add_argument(
"--directory",
"-d",
default=Path.cwd(),
help="Specify alternative directory [default:current directory]",
)
parser.add_argument(
"port",
action="store",
default=8080,
type=int,
nargs="?",
help="Specify alternate port [default: 8080]",
)
args = parser.parse_args()
handler_class = partial(PUTEnabledHTTPRequestHandler, basic_auth=args.basic_auth)
os.chdir(args.directory)
signal.signal(signal.SIGINT, on_terminate)
signal.signal(signal.SIGTERM, on_terminate)
run(
handler_class=handler_class,
server_class=HTTPServer,
port=args.port,
bind=args.bind,
)
|