Last active
January 24, 2026 02:39
-
-
Save lamw/94214b58fa89d111219023344e34dcf5 to your computer and use it in GitHub Desktop.
Python script to serve directory via HTTP or HTTPS w/basic authentication
This file contains hidden or 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 | |
| # | |
| # Extended python -m http.server with optional Basic Auth and HTTPS support | |
| # basic auth, based on https://gist.github.com/fxsjy/5465353 | |
| # Further extended https://gist.github.com/mauler/593caee043f5fe4623732b4db5145a82 (with help from ChatGPT) to add support for HTTPS | |
| # | |
| # Example: | |
| # With Auth: | |
| # python3 http_server_auth.py --bind 192.168.30.4 --user vcf --password vcf123! \ | |
| # --port 443 --directory /path/to/dir --certfile server.crt --keyfile server.key | |
| # | |
| # Without Auth: | |
| # python3 http_server_auth.py --bind 192.168.30.4 --port 443 \ | |
| # --directory /path/to/dir --certfile server.crt --keyfile server.key | |
| # | |
| from functools import partial | |
| from http.server import HTTPServer, SimpleHTTPRequestHandler | |
| import base64 | |
| import os | |
| import ssl | |
| import argparse | |
| class AuthHTTPRequestHandler(SimpleHTTPRequestHandler): | |
| """HTTP server with optional Basic Auth and Range Requests disabled.""" | |
| def __init__(self, *args, **kwargs): | |
| username = kwargs.pop("username", None) | |
| password = kwargs.pop("password", None) | |
| # If both username and password provided, enable auth | |
| if username and password: | |
| self._auth = base64.b64encode(f"{username}:{password}".encode()).decode() | |
| else: | |
| self._auth = None # no auth required | |
| super().__init__(*args, **kwargs) | |
| def send_head(self): | |
| """ | |
| Disable HTTP Range requests by stripping the Range header. | |
| This forces full file delivery with 200 OK responses. | |
| """ | |
| if "Range" in self.headers: | |
| del self.headers["Range"] | |
| return super().send_head() | |
| def do_AUTHHEAD(self): | |
| self.send_response(401) | |
| self.send_header("WWW-Authenticate", 'Basic realm="Protected"') | |
| self.send_header("Content-type", "text/html") | |
| self.end_headers() | |
| def do_GET(self): | |
| """Serve GET requests, requiring auth only if configured.""" | |
| if not self._auth: | |
| return super().do_GET() | |
| auth_header = self.headers.get("Authorization") | |
| if auth_header is None: | |
| self.do_AUTHHEAD() | |
| self.wfile.write(b"No auth header received.") | |
| elif auth_header == "Basic " + self._auth: | |
| super().do_GET() | |
| else: | |
| self.do_AUTHHEAD() | |
| self.wfile.write(b"Invalid authentication credentials.") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser( | |
| description="Simple HTTPS file server with optional Basic Auth" | |
| ) | |
| parser.add_argument("--cgi", action="store_true", help="Run as CGI Server") | |
| parser.add_argument( | |
| "--bind", "-b", | |
| metavar="ADDRESS", | |
| default="127.0.0.1", | |
| help="Specify bind address [default: 127.0.0.1]", | |
| ) | |
| parser.add_argument( | |
| "--directory", "-d", | |
| default=os.getcwd(), | |
| help="Specify alternative directory [default: current directory]", | |
| ) | |
| parser.add_argument( | |
| "--port", "-p", | |
| type=int, | |
| default=8000, | |
| help="Specify alternate port [default: 8000]", | |
| ) | |
| parser.add_argument("--username", "-u", metavar="USERNAME", help="Optional username for basic auth") | |
| parser.add_argument("--password", "-P", metavar="PASSWORD", help="Optional password for basic auth") | |
| parser.add_argument("--certfile", metavar="CERTFILE", help="Path to TLS certificate file") | |
| parser.add_argument("--keyfile", metavar="KEYFILE", help="Path to TLS key file") | |
| args = parser.parse_args() | |
| handler_class = partial( | |
| AuthHTTPRequestHandler, | |
| username=args.username, | |
| password=args.password, | |
| directory=args.directory, | |
| ) | |
| httpd = HTTPServer((args.bind, args.port), handler_class) | |
| # Enable TLS if certificate and key files are provided | |
| if args.certfile and args.keyfile: | |
| context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) | |
| context.load_cert_chain(certfile=args.certfile, keyfile=args.keyfile) | |
| httpd.socket = context.wrap_socket(httpd.socket, server_side=True) | |
| mode = "๐ HTTPS" | |
| else: | |
| mode = "๐ HTTP" | |
| if args.username and args.password: | |
| print(f"{mode} server with Basic Auth running on {args.bind}:{args.port}") | |
| else: | |
| print(f"{mode} server (no authentication) running on {args.bind}:{args.port}") | |
| try: | |
| httpd.serve_forever() | |
| except KeyboardInterrupt: | |
| print("\nServer stopped by user.") | |
| httpd.server_close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
httpd.socket = ssl.wrap_socket has been deprecated in Python 3.7, and does not work with Python 3.13, make sure you use Python 3.12