Cleanup DNS pubkey resolution

This commit is contained in:
hzrd149 2025-04-05 15:31:28 +01:00
parent ef5262f73c
commit b37664bc5b
6 changed files with 111 additions and 138 deletions

View File

@ -0,0 +1,5 @@
---
"nsite-gateway": minor
---
Cleanup DNS pubkey resolution

View File

@ -27,21 +27,21 @@ store?.on("error", (err) => {
const opts = store ? { store } : {}; const opts = store ? { store } : {};
/** A cache that maps a domain to a pubkey ( domain -> pubkey ) */ /** A cache that maps a domain to a pubkey ( domain -> pubkey ) */
export const userDomains = new Keyv<string | undefined>({ export const pubkeyDomains = new Keyv<string | undefined>({
...opts, ...opts,
namespace: "domains", namespace: "domains",
ttl: CACHE_TIME * 1000, ttl: CACHE_TIME * 1000,
}); });
/** A cache that maps a pubkey to a set of blossom servers ( pubkey -> servers ) */ /** A cache that maps a pubkey to a set of blossom servers ( pubkey -> servers ) */
export const userServers = new Keyv<string[] | undefined>({ export const pubkeyServers = new Keyv<string[] | undefined>({
...opts, ...opts,
namespace: "servers", namespace: "servers",
ttl: CACHE_TIME * 1000, ttl: CACHE_TIME * 1000,
}); });
/** A cache that maps a pubkey to a set of relays ( pubkey -> relays ) */ /** A cache that maps a pubkey to a set of relays ( pubkey -> relays ) */
export const userRelays = new Keyv<string[] | undefined>({ export const pubkeyRelays = new Keyv<string[] | undefined>({
...opts, ...opts,
namespace: "relays", namespace: "relays",
ttl: CACHE_TIME * 1000, ttl: CACHE_TIME * 1000,

79
src/dns.ts Normal file
View File

@ -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<string[]> {
return new Promise<string[]>((res, rej) => {
dns.resolveCname(hostname, (err, records) => {
if (err) rej(err);
else res(records);
});
});
}
export function getTxtRecords(hostname: string): Promise<string[][]> {
return new Promise<string[][]>((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<string | undefined> {
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;
}

View File

@ -1,59 +0,0 @@
import dns from "node:dns";
import { nip19 } from "nostr-tools";
export function getCnameRecords(hostname: string) {
return new Promise<string[]>((res, rej) => {
dns.resolveCname(hostname, (err, records) => {
if (err) rej(err);
else res(records);
});
});
}
export function getTxtRecords(hostname: string) {
return new Promise<string[][]>((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) {}
}

View File

@ -9,10 +9,9 @@ import { fileURLToPath } from "node:url";
import mime from "mime"; import mime from "mime";
import morgan from "koa-morgan"; import morgan from "koa-morgan";
import { npubEncode } from "nostr-tools/nip19"; import { npubEncode } from "nostr-tools/nip19";
import { spawn } from "node:child_process";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { resolveNpubFromHostname } from "./helpers/dns.js"; import { resolvePubkeyFromHostname } from "./dns.js";
import { getNsiteBlob } from "./events.js"; import { getNsiteBlob } from "./events.js";
import { streamBlob } from "./blossom.js"; import { streamBlob } from "./blossom.js";
import { import {
@ -25,7 +24,6 @@ import {
ONION_HOST, ONION_HOST,
SUBSCRIPTION_RELAYS, SUBSCRIPTION_RELAYS,
} from "./env.js"; } from "./env.js";
import { userDomains, userRelays, userServers } from "./cache.js";
import pool, { getUserBlossomServers, getUserOutboxes } from "./nostr.js"; import pool, { getUserBlossomServers, getUserOutboxes } from "./nostr.js";
import logger from "./logger.js"; import logger from "./logger.js";
import { watchInvalidation } from "./invalidation.js"; import { watchInvalidation } from "./invalidation.js";
@ -62,28 +60,20 @@ app.use(async (ctx, next) => {
// handle nsite requests // handle nsite requests
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
let pubkey = await userDomains.get<string | undefined>(ctx.hostname); let pubkey = await resolvePubkeyFromHostname(ctx.hostname);
// resolve pubkey if not in cache if (!pubkey) {
if (pubkey === undefined) { if (NSITE_HOMEPAGE) {
logger(`${ctx.hostname}: Resolving`); const parsed = nip19.decode(NSITE_HOMEPAGE);
pubkey = await resolveNpubFromHostname(ctx.hostname); // TODO: use the relays in the nprofile
if (pubkey) { if (parsed.type === "nprofile") pubkey = parsed.data.pubkey;
await userDomains.set(ctx.hostname, pubkey); else if (parsed.type === "npub") pubkey = parsed.data;
logger(`${ctx.hostname}: Found ${pubkey}`); else return await next();
} else { } else return await next();
await userDomains.set(ctx.hostname, "");
}
} }
if (!pubkey) return await next(); // fetch relays
const npub = npubEncode(pubkey);
const log = logger.extend(npub);
ctx.state.pubkey = pubkey;
// fetch relays if not in cache
const relays = (await getUserOutboxes(pubkey)) || []; const relays = (await getUserOutboxes(pubkey)) || [];
// always check subscription relays // always check subscription relays
@ -94,21 +84,13 @@ app.use(async (ctx, next) => {
// fetch servers and events in parallel // fetch servers and events in parallel
let [servers, event] = await Promise.all([ let [servers, event] = await Promise.all([
getUserBlossomServers(pubkey, relays).then((s) => s || []), getUserBlossomServers(pubkey, relays).then((s) => s || []),
(async () => { getNsiteBlob(pubkey, ctx.path, relays).then((e) => {
let e = await getNsiteBlob(pubkey, ctx.path, relays); if (!e) return getNsiteBlob(pubkey, "/404.html", relays);
else return e;
// fallback to custom 404 page }),
if (!e) {
log(`Looking for custom 404 page`);
e = await getNsiteBlob(pubkey, "/404.html", relays);
}
return e;
})(),
]); ]);
if (!event) { if (!event) {
log(`Found 0 events for ${ctx.path}`);
ctx.status = 404; ctx.status = 404;
ctx.body = `Not Found: no events found\npath: ${ctx.path}\nkind: ${NSITE_KIND}\npubkey: ${pubkey}\nrelays: ${relays.join(", ")}`; ctx.body = `Not Found: no events found\npath: ${ctx.path}\nkind: ${NSITE_KIND}\npubkey: ${pubkey}\nrelays: ${relays.join(", ")}`;
return; return;
@ -150,11 +132,10 @@ app.use(async (ctx, next) => {
ctx.body = res; ctx.body = res;
return; return;
} catch (error) { } 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) { 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 // serve static files from public
const serveOptions: serve.Options = { const serveOptions: serve.Options = {
hidden: true, hidden: true,

View File

@ -2,7 +2,7 @@ import { Filter, NostrEvent, SimplePool } from "nostr-tools";
import { getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk"; import { getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk";
import { LOOKUP_RELAYS } from "./env.js"; import { LOOKUP_RELAYS } from "./env.js";
import { userRelays, userServers } from "./cache.js"; import { pubkeyRelays, pubkeyServers } from "./cache.js";
import logger from "./logger.js"; import logger from "./logger.js";
import { npubEncode } from "nostr-tools/nip19"; import { npubEncode } from "nostr-tools/nip19";
@ -12,7 +12,7 @@ const log = logger.extend("nostr");
/** Fetches a pubkeys mailboxes from the cache or relays */ /** Fetches a pubkeys mailboxes from the cache or relays */
export async function getUserOutboxes(pubkey: string) { export async function getUserOutboxes(pubkey: string) {
const cached = await userRelays.get(pubkey); const cached = await pubkeyRelays.get(pubkey);
if (cached) return cached; if (cached) return cached;
const mailboxes = await pool.get(LOOKUP_RELAYS, { kinds: [10002], authors: [pubkey] }); 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]); .map((t) => t[1]);
log(`Found ${relays.length} relays for ${npubEncode(pubkey)}`); 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; return relays;
} }
/** Fetches a pubkeys blossom servers from the cache or relays */ /** Fetches a pubkeys blossom servers from the cache or relays */
export async function getUserBlossomServers(pubkey: string, relays: string[]) { export async function getUserBlossomServers(pubkey: string, relays: string[]) {
const cached = await userServers.get(pubkey); const cached = await pubkeyServers.get(pubkey);
if (cached) return cached; if (cached) return cached;
const blossomServersEvent = await pool.get(relays, { kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] }); 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 // Save servers if found
if (servers) { if (servers) {
log(`Found ${servers.length} blossom servers for ${npubEncode(pubkey)}`); log(`Found ${servers.length} blossom servers for ${npubEncode(pubkey)}`);
await userServers.set(pubkey, servers); await pubkeyServers.set(pubkey, servers);
} }
return servers; return servers;