UNPKG

gurted

Version:

A lightweight Node.js implementation of the gurt:// protocol

172 lines (171 loc) 6.56 kB
import { createServer } from "tls"; import { readFileSync, existsSync, statSync } from "fs"; import { extname, join } from "path"; import chalk from "chalk"; import { performance } from "perf_hooks"; import { GurtRequest, GurtResponse, GurtMethod } from "./message.js"; import { BODY_SEPARATOR, DEFAULT_PORT } from "./protocol.js"; class ResponseWrapper { constructor(socket) { this.status = 200; this.message = "OK"; this.headers = {}; this.body = ""; this.socket = socket; } statusCode(code, message = "OK") { this.status = code; this.message = message; return this; } header(key, value) { this.headers[key.toLowerCase()] = value; return this; } send(body) { this.body = body; const resp = new GurtResponse(this.status, this.message); for (const [k, v] of Object.entries(this.headers)) { resp.withHeader(k, v); } const length = typeof body === "string" ? Buffer.byteLength(body) : body.length; resp.withHeader("content-length", length.toString()); if (typeof body === "string") { resp.withStringBody(body); } else { resp.withBody(body); } this.socket.write(resp.toBytes()); } json(obj) { const str = JSON.stringify(obj); this.header("content-type", "application/json"); this.send(str); } text(str) { this.header("content-type", "text/plain"); this.send(str); } } export class GurtApp { constructor(certFile, keyFile, logging = true) { this.routes = {}; this.middlewares = []; const cert = readFileSync(certFile); const key = readFileSync(keyFile); this.logging = logging; this.server = createServer({ key, cert }, async (socket) => { try { await this.handleConnection(socket); } catch (err) { console.error("Connection error:", err); socket.destroy(); } }); } listen(port = DEFAULT_PORT, cb) { this.server.listen(port, cb ?? (() => { console.log(`GURT server listening on port ${port}`); })); } use(mw) { this.middlewares.push(mw); } get(path, handler) { this.registerRoute(GurtMethod.GET, path, handler); } post(path, handler) { this.registerRoute(GurtMethod.POST, path, handler); } put(path, handler) { this.registerRoute(GurtMethod.PUT, path, handler); } patch(path, handler) { this.registerRoute(GurtMethod.PATCH, path, handler); } delete(path, handler) { this.registerRoute(GurtMethod.DELETE, path, handler); } options(path, handler) { this.registerRoute(GurtMethod.OPTIONS, path, handler); } head(path, handler) { this.registerRoute(GurtMethod.HEAD, path, handler); } registerRoute(method, path, handler) { if (!this.routes[method]) this.routes[method] = new Map(); this.routes[method].set(path, handler); } async handleConnection(socket) { let buffer = Buffer.alloc(0); socket.on("data", async (chunk) => { buffer = Buffer.concat([buffer, chunk]); const sep = buffer.indexOf(BODY_SEPARATOR); if (sep === -1) return; const raw = buffer.toString(); const [headerPart, bodyPart] = raw.split(BODY_SEPARATOR); // Perform handshake (GURT requires upgrade-like 101) if (headerPart.startsWith("GURT_HANDSHAKE")) { const handshakeResp = "GURT/1.0.0 101 SWITCHING_PROTOCOLS\r\n\r\n"; socket.write(handshakeResp); return; } const req = GurtRequest.parse(headerPart + BODY_SEPARATOR + (bodyPart ?? "")); const res = new ResponseWrapper(socket); const start = performance.now(); try { // middlewares for (const mw of this.middlewares) { await mw(req, res); } const handler = this.routes[req.method]?.get(req.path); if (!handler) { res.statusCode(404, "NOT_FOUND").text("Route not found"); } else { await handler(req, res); } } catch (err) { res.statusCode(500, "INTERNAL_SERVER_ERROR").text("Internal server error"); } const end = performance.now(); const duration = (end - start).toFixed(2); if (this.logging) { this.logRequest(req.method, req.path, res["status"], duration); } }); socket.on("error", (err) => console.error("Socket error:", err)); socket.on("close", () => { }); } logRequest(method, path, status, ms) { let color = chalk.white; if (status >= 200 && status < 300) color = chalk.green; else if (status === 404) color = chalk.yellow; else if (status >= 400) color = chalk.red; console.log(`${method} ${path} ${color(status.toString())} - ${ms} ms`); } } // --- Static file serving --- export function staticServe(root, withFilename = false) { return (req, res) => { if (req.method !== "GET" && req.method !== "HEAD") return; let filePath = join(root, req.path); if (!withFilename && !extname(filePath)) { filePath += ".html"; } if (!existsSync(filePath) || !statSync(filePath).isFile()) { res.statusCode(404, "NOT_FOUND").text("Not found"); return; } const ext = extname(filePath).toLowerCase(); const mime = ext === ".html" ? "text/html" : ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : ext === ".json" ? "application/json" : ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "application/octet-stream"; const fileSize = statSync(filePath).size; res.header("content-type", mime).header("content-length", fileSize.toString()); res.send(readFileSync(filePath)); }; } // --- Helper factory --- export function app(certFile, keyFile, logging = true) { return new GurtApp(certFile, keyFile, logging); }