diff --git a/README.md b/README.md index ed590be..7b01ba2 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,21 @@ To run the server from the command line: python3 ok-server.py python3 ok-server.py --look basic python3 ok-server.py --bind 127.0.0.1 --port 8080 --look tailwind +python3 ok-server.py --pem /path/to/bundle.pem --port 8443 ``` `--look` accepts `basic`, `nice`, `bootstrap`, or `tailwind`. You can override the look per request with the `look` query parameter, for example: `http://localhost:8080/?look=tailwind` +`--pem` accepts a path to a PEM file containing the certificate chain and private key. +When provided, the server listens on HTTPS instead of plain HTTP. + ```bash node ok-server.mjs node ok-server.mjs --look basic node ok-server.mjs --bind 127.0.0.1 --port 8080 --look tailwind +node ok-server.mjs --pem /path/to/bundle.pem --port 8443 ``` Connect to the server using a web browser or a tool like `curl`: diff --git a/ok-server.mjs b/ok-server.mjs index 776a3f2..cd2b8a5 100755 --- a/ok-server.mjs +++ b/ok-server.mjs @@ -8,6 +8,8 @@ * - Displays incoming X-* headers when present * - If User-Agent contains "curl" or "wget" (case-insensitive), the server * responds with Content-Type: text/plain; otherwise Content-Type: text/html. + * - --pem specifies a PEM bundle file (cert chain + private key) to enable HTTPS. + * Without --pem the server listens on plain HTTP. * - --look controls which HTML variant is returned for non-CLI agents: * * basic - plain HTML with no external references * * nice - includes Google Font "Noto Sans" (default) @@ -18,6 +20,8 @@ */ import http from "node:http"; +import https from "node:https"; +import fs from "node:fs"; import { URL } from "node:url"; const VALID_LOOKS = new Set(["basic", "nice", "bootstrap", "tailwind"]); @@ -258,6 +262,7 @@ function parseArgs(argv) { let bind = "0.0.0.0"; let port = 8080; let look = "nice"; + let pem = null; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; @@ -284,6 +289,15 @@ function parseArgs(argv) { continue; } + if (arg === "--pem") { + i += 1; + if (i >= argv.length) { + throw new Error(`${arg} requires a value`); + } + pem = argv[i]; + continue; + } + if (arg === "--look") { i += 1; if (i >= argv.length) { @@ -300,11 +314,12 @@ function parseArgs(argv) { if (arg === "-h" || arg === "--help") { const help = [ "Usage:", - " node ok-server.mjs [-b|--bind ADDRESS] [-p|--port PORT] [--look basic|nice|bootstrap|tailwind]", + " node ok-server.mjs [-b|--bind ADDRESS] [-p|--port PORT] [--pem PEMFILE] [--look basic|nice|bootstrap|tailwind]", "", "Defaults:", " --bind 0.0.0.0", " --port 8080", + " --pem (none, plain HTTP)", " --look nice", ].join("\n"); console.log(help); @@ -314,12 +329,13 @@ function parseArgs(argv) { throw new Error(`Unknown argument: ${arg}`); } - return { bind, port, look }; + return { bind, port, look, pem }; } -function run(bind, port, look) { +function run(bind, port, look, pem) { let isShuttingDown = false; - const server = http.createServer((req, res) => { + + function createRequestHandler(req, res) { const method = req.method || "GET"; if (method !== "GET" && method !== "HEAD") { @@ -342,7 +358,20 @@ function run(bind, port, look) { } res.end(body); - }); + } + + let server; + if (pem) { + let pemContent; + try { + pemContent = fs.readFileSync(pem); + } catch (err) { + throw new Error(`Cannot read PEM file '${pem}': ${err.message}`); + } + server = https.createServer({ cert: pemContent, key: pemContent }, createRequestHandler); + } else { + server = http.createServer(createRequestHandler); + } server.on("request", (req) => { const clientIp = getClientIp(req); @@ -358,7 +387,8 @@ function run(bind, port, look) { }); server.listen(port, bind, () => { - console.log(`Serving on ${bind}:${port} (default look=${look}) (Ctrl-C to stop)`); + const scheme = pem ? "https" : "http"; + console.log(`Serving on ${scheme}://${bind}:${port} (default look=${look}) (Ctrl-C to stop)`); }); function handleShutdownSignal() { @@ -375,8 +405,8 @@ function run(bind, port, look) { } try { - const { bind, port, look } = parseArgs(process.argv.slice(2)); - run(bind, port, look); + const { bind, port, look, pem } = parseArgs(process.argv.slice(2)); + run(bind, port, look, pem); } catch (err) { process.stderr.write(`${err.message}\n`); process.exit(2); diff --git a/ok-server.py b/ok-server.py index ea81718..e1ba6e0 100755 --- a/ok-server.py +++ b/ok-server.py @@ -7,6 +7,8 @@ Behavior: - Displays whether any incoming X-* headers were detected and lists them - If User-Agent contains "curl" or "wget" (case-insensitive) the server responds with Content-Type: text/plain; otherwise Content-Type: text/html. + - --pem path to a PEM bundle (cert chain + private key) to enable HTTPS; + without --pem the server uses plain HTTP. - --look controls which HTML variant is returned for non-CLI agents: * basic - plain HTML with no external references * nice - includes Google Font "Noto Sans" (default) @@ -30,6 +32,7 @@ Test: import argparse import signal +import ssl import sys from html import escape from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer @@ -303,7 +306,7 @@ class OkHandler(BaseHTTPRequestHandler): (self.client_address[0], self.log_date_time_string(), format % args)) -def run(bind: str, port: int, look: str): +def run(bind: str, port: int, look: str, pem: str | None = None): addr = (bind, port) try: with ThreadingHTTPServer(addr, OkHandler) as httpd: @@ -316,7 +319,16 @@ def run(bind: str, port: int, look: str): # attach chosen look to server so handlers can use it as default httpd.look = look - print(f"Serving on {bind}:{port} (default look={look}) (Ctrl-C to stop)") + if pem: + try: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.load_cert_chain(pem) + except (ssl.SSLError, OSError) as e: + print(f"Cannot load PEM file '{pem}': {e}", file=sys.stderr) + sys.exit(1) + httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) + scheme = "https" if pem else "http" + print(f"Serving on {scheme}://{bind}:{port} (default look={look}) (Ctrl-C to stop)") httpd.serve_forever() except KeyboardInterrupt: print("\nShutting down server") @@ -333,5 +345,7 @@ if __name__ == "__main__": help="Port to listen on (default: 8080)") parser.add_argument("--look", choices=VALID_LOOKS, default=VALID_LOOKS[1], help="Default HTML look for non-cli agents (basic, nice, bootstrap, tailwind). Default: nice") + parser.add_argument("--pem", default=None, metavar="PEMFILE", + help="PEM bundle file (cert chain + private key) to enable HTTPS; plain HTTP if omitted") args = parser.parse_args() - run(args.bind, args.port, args.look) + run(args.bind, args.port, args.look, args.pem)