mirror of
https://github.com/hzrd149/nsite-gateway.git
synced 2025-06-23 12:05:01 +00:00
Cleanup DNS pubkey resolution
This commit is contained in:
parent
ef5262f73c
commit
b37664bc5b
5
.changeset/pink-regions-exist.md
Normal file
5
.changeset/pink-regions-exist.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nsite-gateway": minor
|
||||
---
|
||||
|
||||
Cleanup DNS pubkey resolution
|
@ -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
79
src/dns.ts
Normal 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;
|
||||
}
|
@ -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) {}
|
||||
}
|
88
src/index.ts
88
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<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 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,
|
||||
|
12
src/nostr.ts
12
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user