gurted
Version:
A lightweight Node.js implementation of the gurt:// protocol
172 lines (171 loc) • 6.56 kB
JavaScript
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);
}