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 } : {};
/** 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,
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<string[] | undefined>({
export const pubkeyServers = new Keyv<string[] | undefined>({
...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<string[] | undefined>({
export const pubkeyRelays = new Keyv<string[] | undefined>({
...opts,
namespace: "relays",
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 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<string | undefined>(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 find blob";
ctx.body = `Failed to stream blob ${event.path}\n${error}`;
return;
}
});
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,

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 { 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;