feat: add support for HTTPS with --pem option in both Python and Node.js servers

This commit is contained in:
2026-05-22 00:08:50 +02:00
parent c0237886ee
commit fca0b84216
3 changed files with 60 additions and 11 deletions
+5
View File
@@ -20,16 +20,21 @@ To run the server from the command line:
python3 ok-server.py python3 ok-server.py
python3 ok-server.py --look basic python3 ok-server.py --look basic
python3 ok-server.py --bind 127.0.0.1 --port 8080 --look tailwind 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`. `--look` accepts `basic`, `nice`, `bootstrap`, or `tailwind`.
You can override the look per request with the `look` query parameter, for example: You can override the look per request with the `look` query parameter, for example:
`http://localhost:8080/?look=tailwind` `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 ```bash
node ok-server.mjs node ok-server.mjs
node ok-server.mjs --look basic node ok-server.mjs --look basic
node ok-server.mjs --bind 127.0.0.1 --port 8080 --look tailwind 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`: Connect to the server using a web browser or a tool like `curl`:
+38 -8
View File
@@ -8,6 +8,8 @@
* - Displays incoming X-* headers when present * - Displays incoming X-* headers when present
* - If User-Agent contains "curl" or "wget" (case-insensitive), the server * - If User-Agent contains "curl" or "wget" (case-insensitive), the server
* responds with Content-Type: text/plain; otherwise Content-Type: text/html. * 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: * - --look controls which HTML variant is returned for non-CLI agents:
* * basic - plain HTML with no external references * * basic - plain HTML with no external references
* * nice - includes Google Font "Noto Sans" (default) * * nice - includes Google Font "Noto Sans" (default)
@@ -18,6 +20,8 @@
*/ */
import http from "node:http"; import http from "node:http";
import https from "node:https";
import fs from "node:fs";
import { URL } from "node:url"; import { URL } from "node:url";
const VALID_LOOKS = new Set(["basic", "nice", "bootstrap", "tailwind"]); const VALID_LOOKS = new Set(["basic", "nice", "bootstrap", "tailwind"]);
@@ -258,6 +262,7 @@ function parseArgs(argv) {
let bind = "0.0.0.0"; let bind = "0.0.0.0";
let port = 8080; let port = 8080;
let look = "nice"; let look = "nice";
let pem = null;
for (let i = 0; i < argv.length; i += 1) { for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i]; const arg = argv[i];
@@ -284,6 +289,15 @@ function parseArgs(argv) {
continue; continue;
} }
if (arg === "--pem") {
i += 1;
if (i >= argv.length) {
throw new Error(`${arg} requires a value`);
}
pem = argv[i];
continue;
}
if (arg === "--look") { if (arg === "--look") {
i += 1; i += 1;
if (i >= argv.length) { if (i >= argv.length) {
@@ -300,11 +314,12 @@ function parseArgs(argv) {
if (arg === "-h" || arg === "--help") { if (arg === "-h" || arg === "--help") {
const help = [ const help = [
"Usage:", "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:", "Defaults:",
" --bind 0.0.0.0", " --bind 0.0.0.0",
" --port 8080", " --port 8080",
" --pem (none, plain HTTP)",
" --look nice", " --look nice",
].join("\n"); ].join("\n");
console.log(help); console.log(help);
@@ -314,12 +329,13 @@ function parseArgs(argv) {
throw new Error(`Unknown argument: ${arg}`); 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; let isShuttingDown = false;
const server = http.createServer((req, res) => {
function createRequestHandler(req, res) {
const method = req.method || "GET"; const method = req.method || "GET";
if (method !== "GET" && method !== "HEAD") { if (method !== "GET" && method !== "HEAD") {
@@ -342,7 +358,20 @@ function run(bind, port, look) {
} }
res.end(body); 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) => { server.on("request", (req) => {
const clientIp = getClientIp(req); const clientIp = getClientIp(req);
@@ -358,7 +387,8 @@ function run(bind, port, look) {
}); });
server.listen(port, bind, () => { 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() { function handleShutdownSignal() {
@@ -375,8 +405,8 @@ function run(bind, port, look) {
} }
try { try {
const { bind, port, look } = parseArgs(process.argv.slice(2)); const { bind, port, look, pem } = parseArgs(process.argv.slice(2));
run(bind, port, look); run(bind, port, look, pem);
} catch (err) { } catch (err) {
process.stderr.write(`${err.message}\n`); process.stderr.write(`${err.message}\n`);
process.exit(2); process.exit(2);
+17 -3
View File
@@ -7,6 +7,8 @@ Behavior:
- Displays whether any incoming X-* headers were detected and lists them - Displays whether any incoming X-* headers were detected and lists them
- If User-Agent contains "curl" or "wget" (case-insensitive) the server - If User-Agent contains "curl" or "wget" (case-insensitive) the server
responds with Content-Type: text/plain; otherwise Content-Type: text/html. 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: - --look controls which HTML variant is returned for non-CLI agents:
* basic - plain HTML with no external references * basic - plain HTML with no external references
* nice - includes Google Font "Noto Sans" (default) * nice - includes Google Font "Noto Sans" (default)
@@ -30,6 +32,7 @@ Test:
import argparse import argparse
import signal import signal
import ssl
import sys import sys
from html import escape from html import escape
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -303,7 +306,7 @@ class OkHandler(BaseHTTPRequestHandler):
(self.client_address[0], self.log_date_time_string(), format % args)) (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) addr = (bind, port)
try: try:
with ThreadingHTTPServer(addr, OkHandler) as httpd: 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 # attach chosen look to server so handlers can use it as default
httpd.look = look 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() httpd.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nShutting down server") print("\nShutting down server")
@@ -333,5 +345,7 @@ if __name__ == "__main__":
help="Port to listen on (default: 8080)") help="Port to listen on (default: 8080)")
parser.add_argument("--look", choices=VALID_LOOKS, default=VALID_LOOKS[1], 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") 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() args = parser.parse_args()
run(args.bind, args.port, args.look) run(args.bind, args.port, args.look, args.pem)