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 } : {};
|
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
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 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,
|
||||||
|
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 { 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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user