diff --git a/.changeset/ready-schools-laugh.md b/.changeset/ready-schools-laugh.md new file mode 100644 index 0000000..903a980 --- /dev/null +++ b/.changeset/ready-schools-laugh.md @@ -0,0 +1,5 @@ +--- +"nsite-gateway": minor +--- + +Add support for resolving NIP-05 names on set domains diff --git a/.env.example b/.env.example index b63bb27..e17837c 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,12 @@ NSITE_HOMEPAGE="" # a local directory to download the homepage to NSITE_HOMEPAGE_DIR="public" +# The public domain of the gateway (optional) (used to detect when to show the nsite homepage) +PUBLIC_DOMAIN="nsite.gateway.com" + +# The nip-05 domain to use for name resolution +# NIP05_NAME_DOMAINS="example.com,nostr.other.site" + # If this is set, nsite will return the 'Onion-Location' header in responses # ONION_HOST=https://.onion diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..ab4fce5 --- /dev/null +++ b/public/404.html @@ -0,0 +1,51 @@ + + + + + + 404 - Page Not Found + + + +
+

404 - Page Not Found

+
+

We couldn't find an nsite for this domain.

+

This could mean either:

+
    +
  • The domain is not configured to point to an nsite
  • +
+
+

+ For more information about setting up an nsite, please refer to the + documentation +

+
+ + diff --git a/src/cache.ts b/src/cache.ts index 9ba6f2e..509e8c0 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,6 +1,7 @@ -import Keyv from "keyv"; +import Keyv, { KeyvOptions } from "keyv"; import { CACHE_PATH, CACHE_TIME } from "./env.js"; import logger from "./logger.js"; +import { ParsedEvent } from "./events.js"; const log = logger.extend("cache"); @@ -24,11 +25,13 @@ store?.on("error", (err) => { process.exit(1); }); -const opts = store ? { store } : {}; +const json: KeyvOptions = { serialize: JSON.stringify, deserialize: JSON.parse }; +const opts: KeyvOptions = store ? { store } : {}; /** A cache that maps a domain to a pubkey ( domain -> pubkey ) */ export const pubkeyDomains = new Keyv({ ...opts, + ...json, namespace: "domains", ttl: CACHE_TIME * 1000, }); @@ -36,6 +39,7 @@ export const pubkeyDomains = new Keyv({ /** A cache that maps a pubkey to a set of blossom servers ( pubkey -> servers ) */ export const pubkeyServers = new Keyv({ ...opts, + ...json, namespace: "servers", ttl: CACHE_TIME * 1000, }); @@ -43,13 +47,15 @@ export const pubkeyServers = new Keyv({ /** A cache that maps a pubkey to a set of relays ( pubkey -> relays ) */ export const pubkeyRelays = new Keyv({ ...opts, + ...json, namespace: "relays", ttl: CACHE_TIME * 1000, }); /** A cache that maps a pubkey + path to sha256 hash of the blob ( pubkey/path -> sha256 ) */ -export const pathBlobs = new Keyv({ +export const pathBlobs = new Keyv({ ...opts, + ...json, namespace: "paths", ttl: CACHE_TIME * 1000, }); @@ -57,6 +63,7 @@ export const pathBlobs = new Keyv({ /** A cache that maps a sha256 hash to a set of URLs that had the blob ( sha256 -> URLs ) */ export const blobURLs = new Keyv({ ...opts, + ...json, namespace: "blobs", ttl: CACHE_TIME * 1000, }); diff --git a/src/dns.ts b/src/dns.ts index 6ab9495..f5a9a2d 100644 --- a/src/dns.ts +++ b/src/dns.ts @@ -1,7 +1,8 @@ import dns from "node:dns"; -import { nip19 } from "nostr-tools"; +import { nip05, nip19 } from "nostr-tools"; import { pubkeyDomains as pubkeyDomains } from "./cache.js"; import logger from "./logger.js"; +import { NIP05_NAME_DOMAINS } from "./env.js"; export function getCnameRecords(hostname: string): Promise { return new Promise((res, rej) => { @@ -42,8 +43,8 @@ export async function resolvePubkeyFromHostname(hostname: string): Promise d.trim()); +const PUBLIC_DOMAIN = process.env.PUBLIC_DOMAIN; + const PAC_PROXY = process.env.PAC_PROXY; const TOR_PROXY = process.env.TOR_PROXY; const I2P_PROXY = process.env.I2P_PROXY; @@ -45,4 +48,6 @@ export { HOST, ONION_HOST, CACHE_TIME, + NIP05_NAME_DOMAINS, + PUBLIC_DOMAIN, }; diff --git a/src/events.ts b/src/events.ts index 8ee0fe2..17f5441 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,6 +1,14 @@ import { extname, join } from "path"; import { NSITE_KIND } from "./const.js"; import { requestEvents } from "./nostr.js"; +import { pathBlobs } from "./cache.js"; + +export type ParsedEvent = { + pubkey: string; + path: string; + sha256: string; + created_at: number; +}; /** Returns all the `d` tags that should be searched for a given path */ export function getSearchPaths(path: string) { @@ -26,20 +34,24 @@ export function parseNsiteEvent(event: { pubkey: string; tags: string[][]; creat } /** Returns the first blob found for a given path */ -export async function getNsiteBlob( - pubkey: string, - path: string, - relays: string[], -): Promise<{ sha256: string; path: string; created_at: number } | undefined> { +export async function getNsiteBlob(pubkey: string, path: string, relays: string[]): Promise { + const key = pubkey + path; + + const cached = await pathBlobs.get(key); + if (cached) return cached; + // NOTE: hack, remove "/" paths since it breaks some relays const paths = getSearchPaths(path).filter((p) => p !== "/"); const events = await requestEvents(relays, { kinds: [NSITE_KIND], "#d": paths, authors: [pubkey] }); // Sort the found blobs by the order of the paths array - const blobs = Array.from(events) + const options = Array.from(events) .map(parseNsiteEvent) .filter((e) => !!e) .sort((a, b) => paths.indexOf(a.path) - paths.indexOf(b.path)); - return blobs[0]; + // Remember the blob for this path + if (options.length > 0) await pathBlobs.set(key, options[0]); + + return options[0]; } diff --git a/src/index.ts b/src/index.ts index d9def96..9ad4b22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import { NSITE_HOST, NSITE_PORT, ONION_HOST, + PUBLIC_DOMAIN, SUBSCRIPTION_RELAYS, } from "./env.js"; import pool, { getUserBlossomServers, getUserOutboxes } from "./nostr.js"; @@ -59,18 +60,21 @@ app.use(async (ctx, next) => { }); // handle nsite requests -app.use(async (ctx, next) => { +app.use(async (ctx) => { let pubkey = await resolvePubkeyFromHostname(ctx.hostname); - if (!pubkey) { - if (NSITE_HOMEPAGE) { - const parsed = nip19.decode(NSITE_HOMEPAGE); - // TODO: use the relays in the nprofile + if (!pubkey && NSITE_HOMEPAGE && (!PUBLIC_DOMAIN || ctx.hostname === PUBLIC_DOMAIN)) { + const parsed = nip19.decode(NSITE_HOMEPAGE); + // TODO: use the relays in the nprofile - 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 (parsed.type === "nprofile") pubkey = parsed.data.pubkey; + else if (parsed.type === "npub") pubkey = parsed.data; + } + + if (!pubkey) { + ctx.status = 404; + ctx.body = fs.readFileSync(path.resolve(__dirname, "../public/404.html"), "utf-8"); + return; } // fetch relays diff --git a/src/invalidation.ts b/src/invalidation.ts index 21a4801..895df6d 100644 --- a/src/invalidation.ts +++ b/src/invalidation.ts @@ -1,27 +1,31 @@ -import { nip19 } from "nostr-tools"; +import { npubEncode } from "nostr-tools/nip19"; import { SUBSCRIPTION_RELAYS } from "./env.js"; import { parseNsiteEvent } from "./events.js"; import pool from "./nostr.js"; import { NSITE_KIND } from "./const.js"; import logger from "./logger.js"; +import { pathBlobs } from "./cache.js"; + +const log = logger.extend("invalidation"); export function watchInvalidation() { - // invalidate nginx cache on new events - if (SUBSCRIPTION_RELAYS.length > 0) { - logger(`Listening for new nsite events on: ${SUBSCRIPTION_RELAYS.join(", ")}`); + if (SUBSCRIPTION_RELAYS.length === 0) return; - pool.subscribeMany(SUBSCRIPTION_RELAYS, [{ kinds: [NSITE_KIND], since: Math.round(Date.now() / 1000) - 60 * 60 }], { - onevent: async (event) => { - try { - const nsite = parseNsiteEvent(event); - if (nsite) { - const log = logger.extend(nip19.npubEncode(nsite.pubkey)); - } - } catch (error) { - console.log(`Failed to invalidate ${event.id}`); + logger(`Listening for new nsite events on: ${SUBSCRIPTION_RELAYS.join(", ")}`); + + pool.subscribeMany(SUBSCRIPTION_RELAYS, [{ kinds: [NSITE_KIND], since: Math.round(Date.now() / 1000) - 60 * 60 }], { + onevent: async (event) => { + try { + const parsed = parseNsiteEvent(event); + if (parsed) { + pathBlobs.delete(parsed.pubkey + parsed.path); + + log(`Invalidated ${npubEncode(parsed.pubkey) + parsed.path}`); } - }, - }); - } + } catch (error) { + console.log(`Failed to invalidate ${event.id}`); + } + }, + }); }