summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.ts9
-rw-r--r--src/index.ts211
2 files changed, 220 insertions, 0 deletions
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..064b038
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,9 @@
+import path from "node:path";
+import fs from "node:fs";
+import toml from "toml";
+
+export default function readConfig(base: string) {
+  const filename = path.join(base, "config.toml");
+
+  return toml.parse(fs.readFileSync(filename, "utf-8"));
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..ee092f2
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,211 @@
+import path from "node:path";
+import fs, { Stats } from "node:fs";
+import type { BunFile, Serve } from "bun";
+import * as Sentry from "@sentry/node";
+import prom from "bun-prometheus-client";
+
+import readConfig from "./config";
+
+Sentry.init({});
+
+const base = "./website/";
+const publicDir = path.resolve(base, "public") + path.sep;
+
+const config = readConfig(base);
+const defaultHeaders = {
+  ...config.extra.headers,
+  vary: "Accept-Encoding",
+};
+
+type File = {
+  filename: string;
+  handle: BunFile;
+  relPath: string;
+  headers?: Record<string, string>;
+  size: number;
+  mtime: Date;
+};
+
+const collectDefaultMetrics = prom.collectDefaultMetrics;
+collectDefaultMetrics({
+  labels: {
+    FLY_APP_NAME: Bun.env.FLY_APP_NAME,
+    FLY_ALLOC_ID: Bun.env.FLY_ALLOC_ID,
+    FLY_REGION: Bun.env.FLY_REGION,
+  },
+});
+const metrics = {
+  requestsByStatus: new prom.Counter({
+    name: "homestead_requests_by_status",
+    help: "Number of requests by status code",
+    labelNames: ["status_code"],
+  }),
+  requestsByPath: new prom.Counter({
+    name: "homestead_requests_by_path",
+    help: "Number of requests by path",
+    labelNames: ["path"],
+  }),
+  requestDuration: new prom.Histogram({
+    name: "homestead_request_duration_seconds",
+    help: "Request duration in seconds",
+    labelNames: ["path"],
+  }),
+};
+
+let files = new Map<string, File>();
+
+function registerFile(
+  path: string,
+  pathname: string,
+  filename: string,
+  stat: Stats,
+): void {
+  pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname);
+
+  if (files.get(pathname) !== undefined) {
+    console.warn("File already registered:", pathname);
+  }
+  files.set(pathname, {
+    filename,
+    relPath: "/" + path,
+    handle: Bun.file(filename),
+    headers:
+      pathname === "/404.html"
+        ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" })
+        : undefined,
+    size: stat.size,
+    mtime: stat.mtime,
+  });
+}
+
+function walkDirectory(root: string, dir: string) {
+  const absDir = path.join(root, dir);
+  for (let pathname of fs.readdirSync(absDir)) {
+    const relPath = path.join(dir, pathname);
+    const absPath = path.join(absDir, pathname);
+    const stat = fs.statSync(absPath);
+    if (stat.isDirectory()) {
+      walkDirectory(root, relPath + path.sep);
+    } else if (stat.isFile()) {
+      if (pathname.startsWith("index.html")) {
+        const dir = relPath.replace("index.html", "");
+        registerFile(relPath, dir, absPath, stat);
+        if (dir !== ".") {
+          registerFile(relPath, dir + path.sep, absPath, stat);
+        }
+      }
+      registerFile(relPath, relPath, absPath, stat);
+    }
+  }
+}
+
+walkDirectory(publicDir, "");
+
+async function serveFile(
+  file: File | undefined,
+  statusCode: number = 200,
+  extraHeaders: Record<string, string> = {},
+): Promise<Response> {
+  if (file && (await file.handle.exists())) {
+    metrics.requestsByStatus.inc({ status_code: statusCode });
+    return new Response(file.handle, {
+      headers: {
+        "last-modified": file.mtime.toUTCString(),
+        ...extraHeaders,
+        ...(file.headers || defaultHeaders),
+      },
+      status: statusCode,
+    });
+  } else {
+    metrics.requestsByStatus.inc({ status_code: 404 });
+    // TODO return encoded
+    return serveFile(files.get("/404.html"), 404);
+  }
+}
+
+async function serveEncodedFile(
+  file: File | undefined,
+  statusCode: number = 200,
+  extraHeaders: Record<string, string> = {},
+): Promise<Response> {
+  const res = await serveFile(file, statusCode, extraHeaders);
+  res.headers.delete("content-disposition");
+  return res;
+}
+
+function parseIfModifiedSinceHeader(header: string | null): number {
+  return header ? new Date(header).getTime() + 999 : 0;
+}
+
+const metricsServer = Bun.serve({
+  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 });
+    }
+  },
+});
+
+console.info(
+  `Serving metrics on http://${metricsServer.hostname}:${metricsServer.port}/metrics`,
+);
+
+const server = Bun.serve({
+  fetch: async function (request) {
+    const pathname = new URL(request.url).pathname;
+    const endTimer = metrics.requestDuration.startTimer({ path: pathname });
+    try {
+      const file = files.get(pathname);
+      metrics.requestsByPath.inc({ path: pathname });
+      if (file) {
+        if (
+          parseIfModifiedSinceHeader(
+            request.headers.get("if-modified-since"),
+          ) >= file?.mtime.getTime()
+        ) {
+          metrics.requestsByStatus.inc({ status_code: 304 });
+          return new Response("", { status: 304, headers: defaultHeaders });
+        }
+        const encodings = (request.headers.get("accept-encoding") || "")
+          .split(",")
+          .map((x) => x.trim().toLowerCase());
+        if (encodings.includes("br") && files.has(file.relPath + ".br")) {
+          return serveEncodedFile(files.get(file.relPath + ".br"), 200, {
+            "content-encoding": "br",
+            "content-type": file.handle.type,
+          });
+        } else if (
+          encodings.includes("zstd") &&
+          files.has(file.relPath + ".zst")
+        ) {
+          return serveEncodedFile(files.get(file.relPath + ".zst"), 200, {
+            "content-encoding": "zstd",
+            "content-type": file.handle.type,
+          });
+        } else if (
+          encodings.includes("gzip") &&
+          files.has(file.relPath + ".gz")
+        ) {
+          return serveEncodedFile(files.get(file.relPath + ".gz"), 200, {
+            "content-encoding": "gzip",
+            "content-type": file.handle.type,
+          });
+        }
+      }
+      return serveFile(file);
+    } catch (error) {
+      metrics.requestsByStatus.inc({ status_code: 503 });
+      Sentry.captureException(error);
+      return new Response("Something went wrong", { status: 503 });
+    } finally {
+      const seconds = endTimer();
+      metrics.requestDuration.observe(seconds);
+    }
+  },
+});
+
+console.info(`Serving website on http://${server.hostname}:${server.port}/`);