diff --git a/config.ts b/config.ts index eae3219..c4e6e36 100644 --- a/config.ts +++ b/config.ts @@ -45,6 +45,7 @@ const config = { password: Deno.env.get("LDAP_PASSWORD"), searchDN: Deno.env.get("LDAP_SEARCH_DN"), }, + query_timeout: parseInt(Deno.env.get("RELAY_TIMEOUT_MS") || "5000"), njump_url: Deno.env.get("NJUMP_URL") || "https://njump.me", }; diff --git a/handlers/errors.ts b/handlers/errors.ts new file mode 100644 index 0000000..cc9bd11 --- /dev/null +++ b/handlers/errors.ts @@ -0,0 +1,20 @@ +import { Context } from "@oak/oak"; +import { errorPageHtml } from "../html.ts"; + +export const notFoundHandler = function (ctx: Context) { + const html = errorPageHtml(404, "Not found"); + ctx.response.body = html; + ctx.response.status = 404; +}; + +export const badGatewayHandler = function (ctx: Context) { + const html = errorPageHtml(502, "Bad gateway"); + ctx.response.body = html; + ctx.response.status = 502; +}; + +export const internalServerErrorHandler = function (ctx: Context) { + const html = errorPageHtml(500, "Internal server error"); + ctx.response.body = html; + ctx.response.status = 500; +}; diff --git a/handlers/naddr.ts b/handlers/naddr.ts index fb6ad5f..57f395c 100644 --- a/handlers/naddr.ts +++ b/handlers/naddr.ts @@ -1,22 +1,25 @@ import { Context } from "@oak/oak"; import { nip19 } from "@nostr/tools"; import { lookupUsernameByPubkey } from "../directory.ts"; -import notFoundHandler from "../handlers/not-found.ts"; +import { notFoundHandler } from "../handlers/errors.ts"; const naddrHandler = async function (ctx: Context) { const naddr = ctx.state.path; + let data: nip19.AddressPointer; try { - const data = nip19.decode(naddr).data as nip19.AddressPointer; - const username = await lookupUsernameByPubkey(data.pubkey); - - if (username && data.identifier) { - ctx.response.redirect(`/@${username}/${data.identifier}`); - } else { - notFoundHandler(ctx); - } + data = nip19.decode(naddr).data as nip19.AddressPointer; } catch (_e) { notFoundHandler(ctx); + return; + } + + const username = await lookupUsernameByPubkey(data.pubkey); + + if (username && data.identifier) { + ctx.response.redirect(`/@${username}/${data.identifier}`); + } else { + notFoundHandler(ctx); } }; diff --git a/handlers/not-found.ts b/handlers/not-found.ts deleted file mode 100644 index 46524bd..0000000 --- a/handlers/not-found.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Context } from "@oak/oak"; -import { errorPageHtml } from "../html.ts"; - -const notFoundHandler = function (ctx: Context) { - const html = errorPageHtml(404, "Not found"); - ctx.response.body = html; - ctx.response.status = 404; -}; - -export default notFoundHandler; diff --git a/handlers/nprofile.ts b/handlers/nprofile.ts index 04a7c75..6b1adb1 100644 --- a/handlers/nprofile.ts +++ b/handlers/nprofile.ts @@ -1,22 +1,26 @@ import { Context } from "@oak/oak"; import { nip19 } from "@nostr/tools"; import { lookupUsernameByPubkey } from "../directory.ts"; -import notFoundHandler from "../handlers/not-found.ts"; +import { notFoundHandler } from "../handlers/errors.ts"; const nprofileHandler = async function (ctx: Context) { const nprofile = ctx.state.path; + let data: nip19.ProfilePointer; try { - const data = nip19.decode(nprofile).data as nip19.ProfilePointer; - const username = await lookupUsernameByPubkey(data.pubkey); - - if (username) { - ctx.response.redirect(`/@${username}`); - } else { - notFoundHandler(ctx); - } + data = nip19.decode(nprofile).data as nip19.ProfilePointer; + console.log(data); } catch (_e) { notFoundHandler(ctx); + return; + } + + const username = await lookupUsernameByPubkey(data.pubkey); + + if (username) { + ctx.response.redirect(`/@${username}`); + } else { + notFoundHandler(ctx); } }; diff --git a/handlers/npub.ts b/handlers/npub.ts index 315bd3f..a56be83 100644 --- a/handlers/npub.ts +++ b/handlers/npub.ts @@ -1,22 +1,25 @@ import { Context } from "@oak/oak"; import { nip19 } from "@nostr/tools"; import { lookupUsernameByPubkey } from "../directory.ts"; -import notFoundHandler from "../handlers/not-found.ts"; +import { notFoundHandler } from "../handlers/errors.ts"; const npubHandler = async function (ctx: Context) { const npub = ctx.state.path; + let pubkey: string; try { - const pubkey = nip19.decode(npub).data as string; - const username = await lookupUsernameByPubkey(pubkey); - - if (username) { - ctx.response.redirect(`/@${username}`); - } else { - notFoundHandler(ctx); - } + pubkey = nip19.decode(npub).data as string; } catch (_e) { notFoundHandler(ctx); + return; + } + + const username = await lookupUsernameByPubkey(pubkey); + + if (username) { + ctx.response.redirect(`/@${username}`); + } else { + notFoundHandler(ctx); } }; diff --git a/handlers/user-atom-feed.ts b/handlers/user-atom-feed.ts index a464847..3ea7b46 100644 --- a/handlers/user-atom-feed.ts +++ b/handlers/user-atom-feed.ts @@ -4,7 +4,7 @@ import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts"; import { profileAtomFeed } from "../feeds.ts"; import Article from "../models/article.ts"; import Profile from "../models/profile.ts"; -import notFoundHandler from "../handlers/not-found.ts"; +import { notFoundHandler } from "../handlers/errors.ts"; const userAtomFeedHandler = async function (ctx: Context) { const username = ctx.state.username; @@ -15,26 +15,22 @@ const userAtomFeedHandler = async function (ctx: Context) { return; } - try { - const profileEvent = await fetchProfileEvent(pubkey); + const profileEvent = await fetchProfileEvent(pubkey); - if (profileEvent) { - const profile = new Profile(profileEvent, username); + if (profileEvent) { + const profile = new Profile(profileEvent, username); - if (profile.nip05) { - const articleEvents = await fetchArticlesByAuthor(pubkey); - const articles = articleEvents.map((a) => new Article(a)); - const atom = profileAtomFeed(profile, articles); + if (profile.nip05) { + const articleEvents = await fetchArticlesByAuthor(pubkey); + const articles = articleEvents.map((a) => new Article(a)); + const atom = profileAtomFeed(profile, articles); - ctx.response.headers.set("Content-Type", "application/atom+xml"); - ctx.response.body = atom; - return; - } + ctx.response.headers.set("Content-Type", "application/atom+xml"); + ctx.response.body = atom; + return; } - notFoundHandler(ctx); - } catch (_e) { - notFoundHandler(ctx); } + notFoundHandler(ctx); }; export default userAtomFeedHandler; diff --git a/handlers/user-event.ts b/handlers/user-event.ts index 9743249..342bf5e 100644 --- a/handlers/user-event.ts +++ b/handlers/user-event.ts @@ -4,7 +4,7 @@ import { fetchProfileEvent, fetchReplaceableEvent } from "../nostr.ts"; import Article from "../models/article.ts"; import Profile from "../models/profile.ts"; import { articleHtml } from "../html.ts"; -import notFoundHandler from "../handlers/not-found.ts"; +import { notFoundHandler } from "../handlers/errors.ts"; import { generateOgProfileImage } from "../magick.ts"; const userEventHandler = async function (ctx: Context) { @@ -17,24 +17,20 @@ const userEventHandler = async function (ctx: Context) { return; } - try { - const articleEvent = await fetchReplaceableEvent( - pubkey, - identifier, - ); - const profileEvent = await fetchProfileEvent(pubkey); + const articleEvent = await fetchReplaceableEvent( + pubkey, + identifier, + ); + const profileEvent = await fetchProfileEvent(pubkey); - if (articleEvent && profileEvent) { - const article = new Article(articleEvent); - const profile = new Profile(profileEvent, username); - const html = await articleHtml(article, profile); - generateOgProfileImage(profile); + if (articleEvent && profileEvent) { + const article = new Article(articleEvent); + const profile = new Profile(profileEvent, username); + const html = await articleHtml(article, profile); + generateOgProfileImage(profile); - ctx.response.body = html; - } else { - notFoundHandler(ctx); - } - } catch (_e) { + ctx.response.body = html; + } else { notFoundHandler(ctx); } }; diff --git a/handlers/user-profile.ts b/handlers/user-profile.ts index f8062b3..cfc4537 100644 --- a/handlers/user-profile.ts +++ b/handlers/user-profile.ts @@ -4,7 +4,7 @@ import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts"; import Article from "../models/article.ts"; import Profile from "../models/profile.ts"; import { profilePageHtml } from "../html.ts"; -import notFoundHandler from "../handlers/not-found.ts"; +import { notFoundHandler } from "../handlers/errors.ts"; import { generateOgProfileImage } from "../magick.ts"; const userProfileHandler = async function (ctx: Context) { @@ -16,21 +16,17 @@ const userProfileHandler = async function (ctx: Context) { return; } - try { - const profileEvent = await fetchProfileEvent(pubkey); + const profileEvent = await fetchProfileEvent(pubkey); - if (profileEvent) { - const profile = new Profile(profileEvent, username); - const articleEvents = await fetchArticlesByAuthor(pubkey); - const articles = articleEvents.map((a) => new Article(a)); - const html = profilePageHtml(profile, articles); - generateOgProfileImage(profile); + if (profileEvent) { + const profile = new Profile(profileEvent, username); + const articleEvents = await fetchArticlesByAuthor(pubkey); + const articles = articleEvents.map((a) => new Article(a)); + const html = profilePageHtml(profile, articles); + generateOgProfileImage(profile); - ctx.response.body = html; - } else { - notFoundHandler(ctx); - } - } catch (_e) { + ctx.response.body = html; + } else { notFoundHandler(ctx); } }; diff --git a/ldap.ts b/ldap.ts index d021572..ec3d973 100644 --- a/ldap.ts +++ b/ldap.ts @@ -25,10 +25,11 @@ export async function lookupPubkeyByUsername(username: string) { ) { pubkey = searchEntries[0].nostrKey; } - } catch (e) { - console.error(e); - } finally { + await client.unbind(); + } catch (e) { + await client.unbind(); + throw e; } return pubkey; @@ -49,9 +50,8 @@ export async function lookupUsernameByPubkey(pubkey: string) { username = searchEntries[0].cn; } } catch (e) { - console.error(e); - } finally { await client.unbind(); + throw e; } return username; diff --git a/nostr.ts b/nostr.ts index 366ee20..6c7eb5b 100644 --- a/nostr.ts +++ b/nostr.ts @@ -1,4 +1,4 @@ -import { NPool, NRelay1 } from "@nostrify/nostrify"; +import { NostrEvent, NostrFilter, NPool, NRelay1 } from "@nostrify/nostrify"; import { nip19 } from "@nostr/tools"; import { lookupUsernameByPubkey } from "./directory.ts"; import config from "./config.ts"; @@ -14,47 +14,57 @@ const relayPool = new NPool({ eventRouter: async (_event) => [], }); +async function fetchWithTimeout(filters: NostrFilter[]) { + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("relay timeout")), config.query_timeout) + ); + const eventsPromise = relayPool.query(filters); + + const events = await Promise.race([eventsPromise, timeoutPromise]); + return events; +} + export async function fetchReplaceableEvent( pubkey: string, identifier: string, ) { - let events = await relayPool.query([{ + let events = await fetchWithTimeout([{ authors: [pubkey], kinds: [30023], "#d": [identifier], limit: 1, - }]); + }]) as NostrEvent[]; if (events.length > 0) { return events[0]; } else { - events = await relayPool.query([{ + events = await fetchWithTimeout([{ authors: [pubkey], kinds: [30024], "#d": [identifier], limit: 1, - }]); + }]) as NostrEvent[]; return events.length > 0 ? events[0] : null; } } export async function fetchArticlesByAuthor(pubkey: string) { - const events = await relayPool.query([{ + const events = await fetchWithTimeout([{ authors: [pubkey], kinds: [30023], limit: 10, - }]); + }]) as NostrEvent[]; return events; } export async function fetchProfileEvent(pubkey: string) { - const events = await relayPool.query([{ + const events = await fetchWithTimeout([{ authors: [pubkey], kinds: [0], limit: 1, - }]); + }]) as NostrEvent[]; return events.length > 0 ? events[0] : null; } diff --git a/server.ts b/server.ts index cb3a6bf..90d680b 100644 --- a/server.ts +++ b/server.ts @@ -7,23 +7,36 @@ import npubHandler from "./handlers/npub.ts"; import userProfileHandler from "./handlers/user-profile.ts"; import userEventHandler from "./handlers/user-event.ts"; import userAtomFeedHandler from "./handlers/user-atom-feed.ts"; -import notFoundHandler from "./handlers/not-found.ts"; +import { + badGatewayHandler, + internalServerErrorHandler, + notFoundHandler, +} from "./handlers/errors.ts"; const router = new Router(); router.get("/:path", async (ctx) => { const path = ctx.state.path = ctx.params.path; - if (path.startsWith("naddr")) { - await naddrHandler(ctx); - } else if (path.startsWith("nprofile")) { - await nprofileHandler(ctx); - } else if (path.startsWith("npub")) { - await npubHandler(ctx); - } else if (path.startsWith("@") || path.startsWith("~")) { - await userProfileHandler(ctx); - } else { - notFoundHandler(ctx); + try { + if (path.startsWith("naddr")) { + await naddrHandler(ctx); + } else if (path.startsWith("nprofile")) { + await nprofileHandler(ctx); + } else if (path.startsWith("npub")) { + await npubHandler(ctx); + } else if (path.startsWith("@") || path.startsWith("~")) { + await userProfileHandler(ctx); + } else { + notFoundHandler(ctx); + } + } catch (e) { + console.error(e); + if (e instanceof Error && e.message.match(/^connect|relay timeout/)) { + badGatewayHandler(ctx); + } else { + internalServerErrorHandler(ctx); + } } }); @@ -36,7 +49,16 @@ router.get("/:username/:kind.atom", async (ctx) => { (ctx.params.username.startsWith("@") || ctx.params.username.startsWith("~")) ) { - await userAtomFeedHandler(ctx); + try { + await userAtomFeedHandler(ctx); + } catch (e) { + console.error(e); + if (e instanceof Error && e.message.match(/^connect|relay timeout/)) { + badGatewayHandler(ctx); + } else { + internalServerErrorHandler(ctx); + } + } } else { notFoundHandler(ctx); } @@ -47,7 +69,16 @@ router.get("/:username/:identifier", async (ctx) => { ctx.state.identifier = ctx.params.identifier; if (username.startsWith("@") || username.startsWith("~")) { - await userEventHandler(ctx); + try { + await userEventHandler(ctx); + } catch (e) { + console.error(e); + if (e instanceof Error && e.message.match(/^connect|relay timeout/)) { + badGatewayHandler(ctx); + } else { + internalServerErrorHandler(ctx); + } + } } else { notFoundHandler(ctx); } @@ -66,8 +97,13 @@ router.get("/assets/:path*", async (ctx) => { } await send(ctx, filePath, { root }); - } catch (_e) { - notFoundHandler(ctx); + } catch (e) { + if (e instanceof Error && e.name === "NotFoundError") { + notFoundHandler(ctx); + } else { + console.error(e); + badGatewayHandler(ctx); + } } });