nsite-ts/src/index.ts

200 lines
5.3 KiB
TypeScript
Raw Normal View History

2024-09-01 13:26:37 -05:00
#!/usr/bin/env node
import "./polyfill.js";
import Koa from "koa";
import serve from "koa-static";
import path from "node:path";
2024-09-01 13:26:37 -05:00
import cors from "@koa/cors";
import fs from "node:fs";
import { fileURLToPath } from "node:url";
2024-09-25 13:37:32 -05:00
import mime from "mime";
import morgan from "koa-morgan";
2024-10-06 10:01:15 -05:00
import { npubEncode } from "nostr-tools/nip19";
import { nip19 } from "nostr-tools";
2024-09-01 13:26:37 -05:00
2025-04-05 15:31:28 +01:00
import { resolvePubkeyFromHostname } from "./dns.js";
import { getNsiteBlob } from "./events.js";
import { streamBlob } from "./blossom.js";
import {
BLOSSOM_SERVERS,
HOST,
NSITE_HOMEPAGE,
NSITE_HOMEPAGE_DIR,
NSITE_HOST,
NSITE_PORT,
2024-10-06 10:01:15 -05:00
ONION_HOST,
PUBLIC_DOMAIN,
SUBSCRIPTION_RELAYS,
} from "./env.js";
2025-03-17 17:35:11 +00:00
import pool, { getUserBlossomServers, getUserOutboxes } from "./nostr.js";
2025-01-22 10:32:12 -06:00
import logger from "./logger.js";
2025-03-17 17:35:11 +00:00
import { watchInvalidation } from "./invalidation.js";
import { NSITE_KIND } from "./const.js";
2024-09-01 13:26:37 -05:00
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = new Koa();
2024-09-25 13:37:32 -05:00
morgan.token("host", (req) => req.headers.host ?? "");
app.use(morgan(":method :host:url :status :response-time ms - :res[content-length]"));
2024-09-01 13:26:37 -05:00
// set CORS headers
app.use(
cors({
origin: "*",
allowMethods: "*",
allowHeaders: "Authorization,*",
exposeHeaders: "*",
}),
);
// handle errors
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
2024-09-25 13:37:32 -05:00
console.log(err);
ctx.status = 500;
2024-10-04 10:32:57 -05:00
if (err instanceof Error) ctx.body = { message: err.message };
2024-09-01 13:26:37 -05:00
}
});
// handle nsite requests
app.use(async (ctx, next) => {
2025-04-05 15:31:28 +01:00
let pubkey = await resolvePubkeyFromHostname(ctx.hostname);
2024-09-01 13:26:37 -05:00
let fallthrough = true;
if (!pubkey && NSITE_HOMEPAGE && (!PUBLIC_DOMAIN || ctx.hostname === PUBLIC_DOMAIN)) {
const parsed = nip19.decode(NSITE_HOMEPAGE);
// TODO: use the relays in the nprofile
2024-09-25 15:28:28 -05:00
if (parsed.type === "nprofile") pubkey = parsed.data.pubkey;
else if (parsed.type === "npub") pubkey = parsed.data;
// Fallback to public dir if path cannot be found on the nsite homepage
if (pubkey) fallthrough = true;
}
if (!pubkey) {
if (fallthrough) return next();
ctx.status = 404;
ctx.body = fs.readFileSync(path.resolve(__dirname, "../public/404.html"), "utf-8");
return;
2025-04-05 15:31:28 +01:00
}
2024-10-04 10:32:57 -05:00
2025-04-05 15:31:28 +01:00
// fetch relays
const relays = (await getUserOutboxes(pubkey)) || [];
2024-10-18 11:29:29 +01:00
2025-03-17 17:35:11 +00:00
// always check subscription relays
relays.push(...SUBSCRIPTION_RELAYS);
2024-09-01 13:26:37 -05:00
if (relays.length === 0) throw new Error("No relays found");
// fetch servers and events in parallel
let [servers, event] = await Promise.all([
getUserBlossomServers(pubkey, relays).then((s) => s || []),
2025-04-05 15:31:28 +01:00
getNsiteBlob(pubkey, ctx.path, relays).then((e) => {
if (!e) return getNsiteBlob(pubkey, "/404.html", relays);
else return e;
}),
]);
if (!event) {
if (fallthrough) return next();
2025-03-17 17:35:11 +00:00
ctx.status = 404;
ctx.body = `Not Found: no events found\npath: ${ctx.path}\nkind: ${NSITE_KIND}\npubkey: ${pubkey}\nrelays: ${relays.join(", ")}`;
2025-03-17 17:35:11 +00:00
return;
}
2024-09-25 13:37:32 -05:00
2025-03-17 17:35:11 +00:00
// always fetch from additional servers
servers.push(...BLOSSOM_SERVERS);
if (servers.length === 0) throw new Error("Failed to find blossom servers");
try {
const res = await streamBlob(event.sha256, servers);
if (!res) {
ctx.status = 502;
ctx.body = `Failed to find blob\npath: ${event.path}\nsha256: ${event.sha256}\nservers: ${servers.join(", ")}`;
return;
}
2025-03-17 17:35:11 +00:00
const type = mime.getType(event.path);
2025-03-17 17:35:11 +00:00
if (type) ctx.set("content-type", type);
else if (res.headers["content-type"]) ctx.set("content-type", res.headers["content-type"]);
// pass headers along
if (res.headers["content-length"]) ctx.set("content-length", res.headers["content-length"]);
// set Onion-Location header
if (ONION_HOST) {
const url = new URL(ONION_HOST);
url.hostname = npubEncode(pubkey) + "." + url.hostname;
ctx.set("Onion-Location", url.toString().replace(/\/$/, ""));
}
// add cache headers
ctx.set("ETag", res.headers["etag"] || `"${event.sha256}"`);
2025-03-17 17:35:11 +00:00
ctx.set("Cache-Control", "public, max-age=3600");
ctx.set("Last-Modified", res.headers["last-modified"] || new Date(event.created_at * 1000).toUTCString());
2025-03-17 17:35:11 +00:00
ctx.status = 200;
ctx.body = res;
return;
} catch (error) {
2025-04-05 15:31:28 +01:00
ctx.status = 500;
ctx.body = `Failed to stream blob ${event.path}\n${error}`;
return;
2025-03-17 17:35:11 +00:00
}
2024-09-01 13:26:37 -05:00
});
2024-10-06 10:01:15 -05:00
if (ONION_HOST) {
app.use((ctx, next) => {
// set Onion-Location header if it was not set before
if (!ctx.get("Onion-Location") && ONION_HOST) {
ctx.set("Onion-Location", ONION_HOST);
}
return next();
});
}
// serve static files from public
const serveOptions: serve.Options = {
hidden: true,
maxAge: 60 * 60 * 1000,
index: "index.html",
};
try {
const www = NSITE_HOMEPAGE_DIR;
fs.statSync(www);
app.use(serve(www, serveOptions));
} catch (error) {
const www = path.resolve(__dirname, "../public");
app.use(serve(www, serveOptions));
}
2025-03-17 17:35:11 +00:00
// start the server
2024-10-04 10:32:57 -05:00
app.listen({ host: NSITE_HOST, port: NSITE_PORT }, () => {
logger("Started on port", HOST);
2024-10-04 10:32:57 -05:00
});
2025-03-17 17:35:11 +00:00
// watch for invalidations
watchInvalidation();
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
2024-09-25 13:37:32 -05:00
});
2024-09-01 13:26:37 -05:00
async function shutdown() {
logger("Shutting down...");
pool.destroy();
2024-09-01 13:26:37 -05:00
process.exit(0);
}
process.addListener("SIGTERM", shutdown);
process.addListener("SIGINT", shutdown);