import http from "node:http" import { ServerResponse } from "http" import { createReadStream } from "node:fs" export interface Handler { mustMatch: string | RegExp mimetype: string addHeaders?: Record /** * IF set, do not write headers and result; only close. * Use this if you want to send binary data */ unmanaged?: boolean handle: ( path: string, queryParams: URLSearchParams, req: http.IncomingMessage, body: string | undefined, res: http.ServerResponse ) => Promise } class ServerUtils { public static getBody(req: http.IncomingMessage): Promise { return new Promise((resolve) => { let body = "" req.on("data", (chunk) => { body += chunk }) req.on("end", () => { resolve(body) }) }) } } export class Server { private readonly options: Readonly<{ ignorePathPrefix?: ReadonlyArray }> private readonly handlers: ReadonlyArray constructor( port: number, options: { ignorePathPrefix?: string[] }, handle: Handler[] ) { this.options = options handle.push({ mustMatch: "", mimetype: "text/html", handle: async () => { return `Supported endpoints are
    ${handle .filter((h) => h.mustMatch !== "") .map((h) => { let l = h.mustMatch if (typeof h.mustMatch === "string") { l = `${l}` } return "
  • " + l + "
  • " }) .join("")}
` }, }) this.handlers = handle http.createServer((req: http.IncomingMessage, res) => this.answerRequest(req, res)).listen( port ) console.log( "Server is running on http://127.0.0.1:" + port, ". Supported endpoints are: " + handle.map((h) => h.mustMatch).join(", ") ) } private async answerRequest(req: http.IncomingMessage, res: ServerResponse) { try { const url = new URL(`http://127.0.0.1/` + req.url) let path = url.pathname while (path.startsWith("/")) { path = path.substring(1) } console.log( req.method + " " + req.url, "from:", req.headers.origin, new Date().toISOString(), path ) if (this.options?.ignorePathPrefix) { for (const toIgnore of this.options.ignorePathPrefix) { if (path.startsWith(toIgnore)) { path = path.substring(toIgnore.length + 1) break } } } const handler = this.handlers.find((h) => { if (typeof h.mustMatch === "string") { return h.mustMatch === path } if (path.match(h.mustMatch)) { return true } }) if (handler === undefined || handler === null) { res.writeHead(404, { "Content-Type": "text/html" }) res.write("

Not found...

") res.end() return } res.setHeader( "Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept" ) res.setHeader("Access-Control-Allow-Origin", req.headers.origin ?? "*") if (req.method === "OPTIONS") { res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, UPDATE") res.writeHead(204, { "Content-Type": handler.mimetype }) res.end() return } let body: string | undefined = undefined if (req.method === "POST" || req.method === "UPDATE") { body = await ServerUtils.getBody(req) } if (req.method === "DELETE") { return } try { const task = handler.handle(path, url.searchParams, req, body, res) if (handler.unmanaged) { return } const result = await task if (result === undefined) { res.writeHead(500) res.write("Could not fetch this website, probably blocked by them") res.end() return } if (typeof result !== "string") { console.error( "Internal server error: handling", url, "resulted in a ", typeof result, " instead of a string:", result ) } const extraHeaders = handler.addHeaders ?? {} res.writeHead(200, { "Content-Type": handler.mimetype, ...extraHeaders }) res.write("" + result) res.end() } catch (e) { console.error("Could not handle request:", e) res.writeHead(500) res.write("Internal server error - something went wrong:") res.write(e.toString()) res.end() } } catch (e) { console.error("FATAL:", e) res.end() } } /** * Sends a file straight from disk. * Assumes that headers have been set on res * @param path * @param res */ public static sendFile(path: string, res: ServerResponse) { createReadStream(path).pipe(res) } }