import path from "node:path"; import fs from "node:fs/promises"; import type { Stats } from "node:fs"; import type { BunFile, Serve } from "bun"; import * as Sentry from "@sentry/node"; import prom from "bun-prometheus-client"; import log from "loglevel"; import { keepAwake } from "./sleep.ts"; import config from "./config"; log.setLevel((import.meta.env["LOG_LEVEL"] || "info") as log.LogLevelDesc); Sentry.init({ release: `homestead@${import.meta.env["FLY_MACHINE_VERSION"]}`, tracesSampleRate: 1.0, }); const expectedHostURL = new URL( import.meta.env.NODE_ENV === "production" ? config.base_url : "http://localhost:3000", ); const defaultHeaders = { ...config.extra.headers, vary: "Accept-Encoding", }; const autoSleep = import.meta.env.NODE_ENV === "production" && import.meta.env["FLY_REGION"] !== import.meta.env["PRIMARY_REGION"]; type File = { filename: string; handle: BunFile; relPath: string; headers?: Record; type: string; size: number; mtime: Date; etag: string; }; const metrics = { requests: new prom.Counter({ name: "homestead_requests", help: "Number of requests by path, status code, and method", labelNames: ["status_code", "content_encoding", "cache_basis"] as const, }), requestDuration: new prom.Histogram({ name: "homestead_request_duration_seconds", help: "Request duration in seconds", labelNames: ["path"] as const, }), }; let files = new Map(); async function hashFile(file: BunFile): Promise { return new Bun.CryptoHasher("sha256") .update(await file.arrayBuffer()) .digest("base64"); } async function registerFile( path: string, pathname: string, filename: string, stat: Stats, ): Promise { pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname); if (files.get(pathname) !== undefined) { log.warn("File already registered:", pathname); } const handle = Bun.file(filename); files.set(pathname, { filename, relPath: "/" + path, handle: handle, type: pathname.startsWith("/feed-styles.xsl") ? "text/xsl" : handle.type, headers: pathname === "/404.html" ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" }) : undefined, size: stat.size, mtime: stat.mtime, etag: `W/"${await hashFile(handle)}"`, }); } async function walkDirectory(root: string) { for (let relPath of await fs.readdir(root, { recursive: true })) { const absPath = path.join(root, relPath); const stat = await fs.stat(absPath); if (stat.isFile()) { if (relPath.includes("index.html")) { const dir = relPath.replace("index.html", ""); await registerFile(relPath, dir, absPath, stat); } else { await registerFile(relPath, relPath, absPath, stat); } } } } await walkDirectory("public/"); async function serveFile( file: File, statusCode: number = 200, extraHeaders: Record = {}, ): Promise { return new Response(await file.handle.arrayBuffer(), { headers: { "last-modified": file.mtime.toUTCString(), ...extraHeaders, ...(file.headers || defaultHeaders), }, status: statusCode, }); } function parseIfModifiedSinceHeader(header: string | null): number { return header ? new Date(header).getTime() + 999 : 0; } export const metricsServer = { port: 9091, fetch: async function (request) { const pathname = new URL(request.url).pathname; switch (pathname) { case "/metrics": return new Response(await prom.register.metrics()); default: return new Response("", { status: 404 }); } }, } satisfies Serve; export const server = { fetch: async function (request) { const url = new URL(request.url); const pathname = url.pathname.replace(/\/\/+/g, "/"); const hostname = request.headers.get("host")?.toLowerCase() || "unknown"; const endTimer = metrics.requestDuration.startTimer({ path: pathname }); let status; let newpath; try { if (pathname === "/health") { return new Response("OK", { status: (status = 200) }); } else if ( config.redirect_other_hostnames && hostname !== expectedHostURL.host ) { metrics.requests.inc({ content_encoding: "identity", status_code: (status = 301), }); return new Response("", { status, headers: { location: new URL(pathname, expectedHostURL).toString(), }, }); } const file = files.get(pathname); let contentEncoding = "identity"; let suffix = ""; if (file && (await file.handle.exists())) { let etagMatch = request.headers.get("if-none-match") === file.etag; let mtimeMatch = parseIfModifiedSinceHeader( request.headers.get("if-modified-since"), ) >= file?.mtime.getTime(); if (etagMatch || mtimeMatch) { metrics.requests.inc({ content_encoding: contentEncoding, status_code: (status = 304), cache_basis: etagMatch ? "etag" : "mtime", }); return new Response("", { status: status, headers: defaultHeaders }); } const encodings = (request.headers.get("accept-encoding") || "") .split(",") .map((x) => x.trim().toLowerCase()); if (encodings.includes("br") && files.has(pathname + ".br")) { contentEncoding = "br"; suffix = ".br"; } else if (encodings.includes("zstd") && files.has(pathname + ".zst")) { contentEncoding = "zstd"; suffix = ".zst"; } else if (encodings.includes("gzip") && files.has(pathname + ".gz")) { contentEncoding = "gzip"; suffix = ".gz"; } status = 200; metrics.requests.inc({ status_code: status, content_encoding: contentEncoding, }); const endFile = files.get(pathname + suffix); if (!endFile) { throw new Error(`File ${pathname} not found`); } return serveFile(endFile, status, { "content-encoding": contentEncoding, "content-type": file.type, // weak etags can be used for multiple equivalent representations etag: file.etag, }); } else { if (files.has(pathname + "/")) { newpath = pathname + "/"; metrics.requests.inc({ content_encoding: contentEncoding, status_code: (status = 302), }); return new Response("", { status: status, headers: { location: newpath }, }); } else if (files.has(pathname.replace(/index.html$/, ""))) { newpath = pathname.replace(/index.html$/, ""); metrics.requests.inc({ content_encoding: contentEncoding, status_code: (status = 302), }); return new Response("", { status: status, headers: { location: newpath }, }); } const notfound = files.get("/404.html"); if (notfound) { return serveFile(notfound, status, { "content-type": "text/html; charset=utf-8", }); } else { log.warn("404.html not found"); return new Response("404 Not Found", { status: status, headers: { "content-type": "text/plain", ...defaultHeaders }, }); } } } catch (error) { metrics.requests.inc({ status_code: status, content_encoding: "identity", }); Sentry.captureException(error); log.error("Error", error); return new Response("Something went wrong", { status: status }); } finally { if (status === 200) { const seconds = endTimer(); metrics.requestDuration.observe(seconds); } if (autoSleep && pathname !== "/health") { keepAwake(); } } }, } satisfies Serve; if (autoSleep) { keepAwake(); } export default server;