#!/usr/bin/env node /** * Simple HTTP server that responds 200 with a test message including client IP. * * Behavior: * - GET returns HTTP/1.1 200 with a message including the requester IP * - HEAD returns the same headers as GET but no body * - 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. * - --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) * * bootstrap - Bootstrap 5 layout * - The URL query parameter "look" (e.g. /?look=nice) overrides --look for * that request only. Values are case-insensitive and must be basic,nice,bootstrap. */ import http from "node:http"; import { URL } from "node:url"; const VALID_LOOKS = new Set(["basic", "nice", "bootstrap"]); const PLAIN_TEMPLATE = "Hello, This is a test HTTP server.\n\n" + "Your request came from {ip}.\n\n" + "{proxy_headers_block}" + "Have a nice day!\n"; const HTML_BASIC = ` Test HTTP server

Hello,

This is a test HTTP server.

Your request came from {ip}.

{proxy_headers_html}

Have a nice day!

`; const HTML_NICE = ` Test HTTP server

Hello,

This is a test HTTP server.

Your request came from {ip}.

{proxy_headers_html}

Have a nice day!

`; const HTML_BOOTSTRAP = ` Test HTTP server

Hello,

This is a test HTTP server.

Your request came from {ip}.

{proxy_headers_html}

Have a nice day!

`; function escapeHtml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); } function isCliAgent(userAgent) { if (!userAgent) { return false; } const ua = userAgent.toLowerCase(); return ua.includes("curl") || ua.includes("wget"); } function getRequestLook(req) { const url = new URL(req.url || "/", "http://localhost"); const look = (url.searchParams.get("look") || "").trim().toLowerCase(); if (VALID_LOOKS.has(look)) { return look; } return null; } function collectXHeaders(req) { const xHeaders = []; for (const [name, value] of Object.entries(req.headers)) { if (!name.toLowerCase().startsWith("x-")) { continue; } if (Array.isArray(value)) { xHeaders.push([name, value.join(", ")]); } else if (value !== undefined) { xHeaders.push([name, value]); } } xHeaders.sort((a, b) => a[0].localeCompare(b[0], "en", { sensitivity: "base" })); return xHeaders; } function proxyMarkup(xHeaders) { if (xHeaders.length === 0) { return { proxyHeadersBlock: "", proxyHeadersHtml: "" }; } const plainLines = xHeaders.map(([name, value]) => ` - ${name}: ${value}`); const proxyHeadersBlock = "Reverse proxy condition: detected via X-* headers.\n" + "X-* headers:\n" + plainLines.join("\n") + "\n\n"; const htmlItems = xHeaders .map(([name, value]) => `
  • ${escapeHtml(name)}: ${escapeHtml(value)}
  • `) .join(""); const proxyHeadersHtml = "

    Reverse proxy condition: detected via X-* headers.

    " + `

    X-* headers:

    `; return { proxyHeadersBlock, proxyHeadersHtml }; } function htmlForLook(look, ip, proxyHeadersHtml) { const template = look === "basic" ? HTML_BASIC : look === "nice" ? HTML_NICE : HTML_BOOTSTRAP; return template .replaceAll("{ip}", ip) .replace("{proxy_headers_html}", proxyHeadersHtml); } function makeBodyAndType(req, clientIp, defaultLook) { const userAgent = req.headers["user-agent"] || ""; const xHeaders = collectXHeaders(req); const { proxyHeadersBlock, proxyHeadersHtml } = proxyMarkup(xHeaders); if (isCliAgent(userAgent)) { const body = PLAIN_TEMPLATE .replace("{ip}", clientIp) .replace("{proxy_headers_block}", proxyHeadersBlock); return { body: Buffer.from(body, "utf8"), contentType: "text/plain; charset=utf-8" }; } const requestLook = getRequestLook(req); const look = requestLook || defaultLook; const html = htmlForLook(look, clientIp, proxyHeadersHtml); return { body: Buffer.from(html, "utf8"), contentType: "text/html; charset=utf-8" }; } function getClientIp(req) { return req.socket?.remoteAddress || "unknown"; } function parseArgs(argv) { let bind = "0.0.0.0"; let port = 8000; let look = "nice"; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === "-b" || arg === "--bind") { i += 1; if (i >= argv.length) { throw new Error(`${arg} requires a value`); } bind = argv[i]; continue; } if (arg === "-p" || arg === "--port") { i += 1; if (i >= argv.length) { throw new Error(`${arg} requires a value`); } const parsedPort = Number.parseInt(argv[i], 10); if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) { throw new Error(`Invalid port: ${argv[i]}`); } port = parsedPort; continue; } if (arg === "--look") { i += 1; if (i >= argv.length) { throw new Error(`${arg} requires a value`); } const value = String(argv[i]).trim().toLowerCase(); if (!VALID_LOOKS.has(value)) { throw new Error(`Invalid look: ${argv[i]} (expected one of: basic, nice, bootstrap)`); } look = value; continue; } if (arg === "-h" || arg === "--help") { const help = [ "Usage:", " node ok-server.mjs [-b|--bind ADDRESS] [-p|--port PORT] [--look basic|nice|bootstrap]", "", "Defaults:", " --bind 0.0.0.0", " --port 8000", " --look nice", ].join("\n"); console.log(help); process.exit(0); } throw new Error(`Unknown argument: ${arg}`); } return { bind, port, look }; } function run(bind, port, look) { const server = http.createServer((req, res) => { const method = req.method || "GET"; if (method !== "GET" && method !== "HEAD") { res.writeHead(405, { "Content-Length": "0" }); res.end(); return; } const clientIp = getClientIp(req); const { body, contentType } = makeBodyAndType(req, clientIp, look); res.writeHead(200, { "Content-Type": contentType, "Content-Length": String(body.length), }); if (method === "HEAD") { res.end(); return; } res.end(body); }); server.on("request", (req) => { const clientIp = getClientIp(req); const now = new Date().toUTCString(); const method = req.method || "GET"; const path = req.url || "/"; process.stderr.write(`${clientIp} - - [${now}] "${method} ${path}"\n`); }); server.on("error", (err) => { process.stderr.write(`Server error: ${err.message}\n`); process.exitCode = 1; }); server.listen(port, bind, () => { console.log(`Serving on ${bind}:${port} (default look=${look}) (Ctrl-C to stop)`); }); process.on("SIGINT", () => { console.log("\nShutting down server"); server.close(() => process.exit(0)); }); } try { const { bind, port, look } = parseArgs(process.argv.slice(2)); run(bind, port, look); } catch (err) { process.stderr.write(`${err.message}\n`); process.exit(2); }