3 # This is a simple HTTP server based on the HTTPServer and
4 # SimpleHTTPRequestHandler. It has been extended with PUT
5 # and DELETE functionality to store or delete results.
7 # See: https://github.com/python/cpython/blob/main/Lib/http/server.py
9 from functools import partial
10 from http import HTTPStatus
11 from http.server import HTTPServer, SimpleHTTPRequestHandler
18 class AuthenticationError(Exception):
22 class PUTEnabledHTTPRequestHandler(SimpleHTTPRequestHandler):
23 def __init__(self, *args, basic_auth=None, **kwargs):
24 self.basic_auth = None
28 self.basic_auth = base64.b64encode(
29 basic_auth.encode("ascii")
31 super().__init__(*args, **kwargs)
37 except AuthenticationError:
38 self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
44 except AuthenticationError:
45 self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
48 path = self.translate_path(self.path)
49 os.makedirs(os.path.dirname(path), exist_ok=True)
52 file_length = int(self.headers["Content-Length"])
53 with open(path, "wb") as output_file:
54 output_file.write(self.rfile.read(file_length))
55 self.send_response(HTTPStatus.CREATED)
56 self.send_header("Content-Length", "0")
58 except AuthenticationError:
59 self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
62 HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot open file for writing"
66 path = self.translate_path(self.path)
70 self.send_response(HTTPStatus.OK)
71 self.send_header("Content-Length", "0")
73 except AuthenticationError:
74 self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
77 HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot delete file"
80 def _handle_auth(self):
81 if not self.basic_auth:
83 authorization = self.headers.get("authorization")
85 authorization = authorization.split()
86 if len(authorization) == 2:
88 authorization[0] == "Basic"
89 and authorization[1] == self.basic_auth
92 raise AuthenticationError("Authentication required")
95 def _get_best_family(*address):
96 infos = socket.getaddrinfo(
98 type=socket.SOCK_STREAM,
99 flags=socket.AI_PASSIVE,
101 family, type, proto, canonname, sockaddr = next(iter(infos))
102 return family, sockaddr
105 def run(HandlerClass, ServerClass, port, bind):
106 HandlerClass.protocol_version = "HTTP/1.1"
107 ServerClass.address_family, addr = _get_best_family(bind, port)
109 with ServerClass(addr, HandlerClass) as httpd:
110 host, port = httpd.socket.getsockname()[:2]
111 url_host = f"[{host}]" if ":" in host else host
113 f"Serving HTTP on {host} port {port} "
114 f"(http://{url_host}:{port}/) ..."
117 httpd.serve_forever()
118 except KeyboardInterrupt:
119 print("\nKeyboard interrupt received, exiting.")
123 def on_terminate(signum, frame):
126 sys.exit(128 + signum)
129 if __name__ == "__main__":
132 parser = argparse.ArgumentParser()
134 "--basic-auth", "-B", help="Basic auth tuple like user:pass"
140 help="Specify alternate bind address " "[default: all interfaces]",
146 help="Specify alternative directory " "[default:current directory]",
154 help="Specify alternate port [default: 8080]",
156 args = parser.parse_args()
158 handler_class = partial(
159 PUTEnabledHTTPRequestHandler, basic_auth=args.basic_auth
162 os.chdir(args.directory)
164 signal.signal(signal.SIGINT, on_terminate)
165 signal.signal(signal.SIGTERM, on_terminate)
168 HandlerClass=handler_class,
169 ServerClass=HTTPServer,