diff --git a/.changeset/pink-regions-exist.md b/.changeset/pink-regions-exist.md new file mode 100644 index 0000000..02464f0 --- /dev/null +++ b/.changeset/pink-regions-exist.md @@ -0,0 +1,5 @@ +--- +"nsite-gateway": minor +--- + +Cleanup DNS pubkey resolution diff --git a/src/cache.ts b/src/cache.ts index c4d2596..9ba6f2e 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -27,21 +27,21 @@ store?.on("error", (err) => { const opts = store ? { store } : {}; /** A cache that maps a domain to a pubkey ( domain -> pubkey ) */ -export const userDomains = new Keyv({ +export const pubkeyDomains = new Keyv({ ...opts, namespace: "domains", ttl: CACHE_TIME * 1000, }); /** A cache that maps a pubkey to a set of blossom servers ( pubkey -> servers ) */ -export const userServers = new Keyv({ +export const pubkeyServers = new Keyv({ ...opts, namespace: "servers", ttl: CACHE_TIME * 1000, }); /** A cache that maps a pubkey to a set of relays ( pubkey -> relays ) */ -export const userRelays = new Keyv({ +export const pubkeyRelays = new Keyv({ ...opts, namespace: "relays", ttl: CACHE_TIME * 1000, diff --git a/src/dns.ts b/src/dns.ts new file mode 100644 index 0000000..6ab9495 --- /dev/null +++ b/src/dns.ts @@ -0,0 +1,79 @@ +import dns from "node:dns"; +import { nip19 } from "nostr-tools"; +import { pubkeyDomains as pubkeyDomains } from "./cache.js"; +import logger from "./logger.js"; + +export function getCnameRecords(hostname: string): Promise { + return new Promise((res, rej) => { + dns.resolveCname(hostname, (err, records) => { + if (err) rej(err); + else res(records); + }); + }); +} +export function getTxtRecords(hostname: string): Promise { + return new Promise((res, rej) => { + dns.resolveTxt(hostname, (err, records) => { + if (err) rej(err); + else res(records); + }); + }); +} + +function extractPubkeyFromHostname(hostname: string): string | undefined { + const [npub] = hostname.split("."); + + if (npub.startsWith("npub")) { + const parsed = nip19.decode(npub); + if (parsed.type !== "npub") throw new Error("Expected npub"); + + return parsed.data; + } +} + +const log = logger.extend("DNS"); + +export async function resolvePubkeyFromHostname(hostname: string): Promise { + if (hostname === "localhost") return undefined; + + const cached = await pubkeyDomains.get(hostname); + if (cached) return cached; + + // check if domain contains an npub + let pubkey = extractPubkeyFromHostname(hostname); + + // try to get npub from CNAME or TXT records + if (!pubkey) { + try { + const cnameRecords = await getCnameRecords(hostname); + for (const cname of cnameRecords) { + const p = extractPubkeyFromHostname(cname); + if (p) { + pubkey = p; + break; + } + } + } catch (error) {} + } + + if (!pubkey) { + try { + const txtRecords = await getTxtRecords(hostname); + + for (const txt of txtRecords) { + for (const entry of txt) { + const p = extractPubkeyFromHostname(entry); + if (p) { + pubkey = p; + break; + } + } + } + } catch (error) {} + } + + log(`Resolved ${hostname} to ${pubkey}`); + await pubkeyDomains.set(hostname, pubkey); + + return pubkey; +} diff --git a/src/helpers/dns.ts b/src/helpers/dns.ts deleted file mode 100644 index abaae53..0000000 --- a/src/helpers/dns.ts +++ /dev/null @@ -1,59 +0,0 @@ -import dns from "node:dns"; -import { nip19 } from "nostr-tools"; - -export function getCnameRecords(hostname: string) { - return new Promise((res, rej) => { - dns.resolveCname(hostname, (err, records) => { - if (err) rej(err); - else res(records); - }); - }); -} -export function getTxtRecords(hostname: string) { - return new Promise((res, rej) => { - dns.resolveTxt(hostname, (err, records) => { - if (err) rej(err); - else res(records); - }); - }); -} - -function extractNpubFromHostname(hostname: string) { - const [npub] = hostname.split("."); - - if (npub.startsWith("npub")) { - const parsed = nip19.decode(npub); - if (parsed.type !== "npub") throw new Error("Expected npub"); - - return parsed.data; - } -} - -export async function resolveNpubFromHostname(hostname: string) { - // check if domain contains an npub - let pubkey = extractNpubFromHostname(hostname); - - if (pubkey) return pubkey; - - if (hostname === "localhost") return undefined; - - // try to get npub from CNAME or TXT records - try { - const cnameRecords = await getCnameRecords(hostname); - for (const cname of cnameRecords) { - const p = extractNpubFromHostname(cname); - if (p) return p; - } - } catch (error) {} - - try { - const txtRecords = await getTxtRecords(hostname); - - for (const txt of txtRecords) { - for (const entry of txt) { - const p = extractNpubFromHostname(entry); - if (p) return p; - } - } - } catch (error) {} -} diff --git a/src/index.ts b/src/index.ts index 2360f3e..d9def96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,10 +9,9 @@ import { fileURLToPath } from "node:url"; import mime from "mime"; import morgan from "koa-morgan"; import { npubEncode } from "nostr-tools/nip19"; -import { spawn } from "node:child_process"; import { nip19 } from "nostr-tools"; -import { resolveNpubFromHostname } from "./helpers/dns.js"; +import { resolvePubkeyFromHostname } from "./dns.js"; import { getNsiteBlob } from "./events.js"; import { streamBlob } from "./blossom.js"; import { @@ -25,7 +24,6 @@ import { ONION_HOST, SUBSCRIPTION_RELAYS, } from "./env.js"; -import { userDomains, userRelays, userServers } from "./cache.js"; import pool, { getUserBlossomServers, getUserOutboxes } from "./nostr.js"; import logger from "./logger.js"; import { watchInvalidation } from "./invalidation.js"; @@ -62,28 +60,20 @@ app.use(async (ctx, next) => { // handle nsite requests app.use(async (ctx, next) => { - let pubkey = await userDomains.get(ctx.hostname); + let pubkey = await resolvePubkeyFromHostname(ctx.hostname); - // resolve pubkey if not in cache - if (pubkey === undefined) { - logger(`${ctx.hostname}: Resolving`); - pubkey = await resolveNpubFromHostname(ctx.hostname); + if (!pubkey) { + if (NSITE_HOMEPAGE) { + const parsed = nip19.decode(NSITE_HOMEPAGE); + // TODO: use the relays in the nprofile - if (pubkey) { - await userDomains.set(ctx.hostname, pubkey); - logger(`${ctx.hostname}: Found ${pubkey}`); - } else { - await userDomains.set(ctx.hostname, ""); - } + if (parsed.type === "nprofile") pubkey = parsed.data.pubkey; + else if (parsed.type === "npub") pubkey = parsed.data; + else return await next(); + } else return await next(); } - if (!pubkey) return await next(); - - const npub = npubEncode(pubkey); - const log = logger.extend(npub); - ctx.state.pubkey = pubkey; - - // fetch relays if not in cache + // fetch relays const relays = (await getUserOutboxes(pubkey)) || []; // always check subscription relays @@ -94,21 +84,13 @@ app.use(async (ctx, next) => { // fetch servers and events in parallel let [servers, event] = await Promise.all([ getUserBlossomServers(pubkey, relays).then((s) => s || []), - (async () => { - let e = await getNsiteBlob(pubkey, ctx.path, relays); - - // fallback to custom 404 page - if (!e) { - log(`Looking for custom 404 page`); - e = await getNsiteBlob(pubkey, "/404.html", relays); - } - - return e; - })(), + getNsiteBlob(pubkey, ctx.path, relays).then((e) => { + if (!e) return getNsiteBlob(pubkey, "/404.html", relays); + else return e; + }), ]); if (!event) { - log(`Found 0 events for ${ctx.path}`); ctx.status = 404; ctx.body = `Not Found: no events found\npath: ${ctx.path}\nkind: ${NSITE_KIND}\npubkey: ${pubkey}\nrelays: ${relays.join(", ")}`; return; @@ -150,11 +132,10 @@ app.use(async (ctx, next) => { ctx.body = res; return; } catch (error) { - log(`Failed to stream ${event.sha256}\n${error}`); + ctx.status = 500; + ctx.body = `Failed to stream blob ${event.path}\n${error}`; + return; } - - ctx.status = 500; - ctx.body = "Failed to find blob"; }); if (ONION_HOST) { @@ -168,39 +149,6 @@ if (ONION_HOST) { }); } -// download homepage -if (NSITE_HOMEPAGE) { - try { - const log = logger.extend("homepage"); - // create the public dir - try { - fs.mkdirSync(NSITE_HOMEPAGE_DIR); - } catch (error) {} - - const bin = (await import.meta.resolve("nsite-cli")).replace("file://", ""); - - const decode = nip19.decode(NSITE_HOMEPAGE); - if (decode.type !== "nprofile") throw new Error("NSITE_HOMEPAGE must be a valid nprofile"); - - // use nsite-cli to download the homepage - const args = [bin, "download", NSITE_HOMEPAGE_DIR, nip19.npubEncode(decode.data.pubkey)]; - if (decode.data.relays) args.push("--relays", decode.data.relays?.join(",")); - - const child = spawn("node", args, { stdio: "pipe" }); - - child.on("spawn", () => log("Downloading...")); - child.stdout.on("data", (line) => log(line.toString("utf-8"))); - child.on("error", (e) => log("Failed", e)); - child.on("close", (code) => { - if (code === 0) log("Finished"); - else log("Failed"); - }); - } catch (error) { - console.log(`Failed to download homepage`); - console.log(error); - } -} - // serve static files from public const serveOptions: serve.Options = { hidden: true, diff --git a/src/nostr.ts b/src/nostr.ts index cb268e1..6a45982 100644 --- a/src/nostr.ts +++ b/src/nostr.ts @@ -2,7 +2,7 @@ import { Filter, NostrEvent, SimplePool } from "nostr-tools"; import { getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk"; import { LOOKUP_RELAYS } from "./env.js"; -import { userRelays, userServers } from "./cache.js"; +import { pubkeyRelays, pubkeyServers } from "./cache.js"; import logger from "./logger.js"; import { npubEncode } from "nostr-tools/nip19"; @@ -12,7 +12,7 @@ const log = logger.extend("nostr"); /** Fetches a pubkeys mailboxes from the cache or relays */ export async function getUserOutboxes(pubkey: string) { - const cached = await userRelays.get(pubkey); + const cached = await pubkeyRelays.get(pubkey); if (cached) return cached; const mailboxes = await pool.get(LOOKUP_RELAYS, { kinds: [10002], authors: [pubkey] }); @@ -23,15 +23,15 @@ export async function getUserOutboxes(pubkey: string) { .map((t) => t[1]); log(`Found ${relays.length} relays for ${npubEncode(pubkey)}`); - await userRelays.set(pubkey, relays); + await pubkeyRelays.set(pubkey, relays); - await userRelays.set(pubkey, relays); + await pubkeyRelays.set(pubkey, relays); return relays; } /** Fetches a pubkeys blossom servers from the cache or relays */ export async function getUserBlossomServers(pubkey: string, relays: string[]) { - const cached = await userServers.get(pubkey); + const cached = await pubkeyServers.get(pubkey); if (cached) return cached; const blossomServersEvent = await pool.get(relays, { kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] }); @@ -42,7 +42,7 @@ export async function getUserBlossomServers(pubkey: string, relays: string[]) { // Save servers if found if (servers) { log(`Found ${servers.length} blossom servers for ${npubEncode(pubkey)}`); - await userServers.set(pubkey, servers); + await pubkeyServers.set(pubkey, servers); } return servers;