From f4107b1c2b0632504b9fefb6c8c5608282313cc2 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 13:31:24 -0400 Subject: check if previews are disabled before anything else --- packages/backend/src/server/web/UrlPreviewService.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'packages/backend/src/server/web/UrlPreviewService.ts') diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 2a300782c6..160cf37c00 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -123,6 +123,16 @@ export class UrlPreviewService { request: FastifyRequest, reply: FastifyReply, ): Promise { + if (!this.meta.urlPreviewEnabled) { + return reply.code(403).send({ + error: { + message: 'URL preview is disabled', + code: 'URL_PREVIEW_DISABLED', + id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8', + }, + }); + } + const url = request.query.url; if (typeof url !== 'string' || !URL.canParse(url)) { reply.code(400); @@ -135,16 +145,6 @@ export class UrlPreviewService { return; } - if (!this.meta.urlPreviewEnabled) { - return reply.code(403).send({ - error: { - message: 'URL preview is disabled', - code: 'URL_PREVIEW_DISABLED', - id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8', - }, - }); - } - // Check rate limit const auth = await this.authenticate(request); if (!await this.checkRateLimit(auth, reply)) { -- cgit v1.2.3-freya From f601cff5c5222d6f3a7c06ecbafb3d07ad63997f Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 13:31:40 -0400 Subject: check input URL scheme before continuing --- packages/backend/src/server/web/UrlPreviewService.ts | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'packages/backend/src/server/web/UrlPreviewService.ts') diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 160cf37c00..da2660ab0f 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -139,6 +139,13 @@ export class UrlPreviewService { return; } + // Enforce HTTP(S) for input URLs + const urlScheme = this.utilityService.getUrlScheme(url); + if (urlScheme !== 'http:' && urlScheme !== 'https:') { + reply.code(400); + return; + } + const lang = request.query.lang; if (Array.isArray(lang)) { reply.code(400); -- cgit v1.2.3-freya From 865b198ab31688de957d82ea447e11be78f718dc Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 13:32:04 -0400 Subject: redirect to exclude hash from preview URL --- packages/backend/src/server/web/UrlPreviewService.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/web/UrlPreviewService.ts') diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index da2660ab0f..3e0133d50e 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -152,13 +152,25 @@ export class UrlPreviewService { return; } + // Strip out hash (anchor) + const urlObj = new URL(url); + if (urlObj.hash) { + urlObj.hash = ''; + const params = new URLSearchParams({ url: urlObj.href }); + if (lang) params.set('lang', lang); + const newUrl = `/url?${params.toString()}`; + + reply.redirect(newUrl, 301); + return; + } + // Check rate limit const auth = await this.authenticate(request); if (!await this.checkRateLimit(auth, reply)) { return; } - if (this.utilityService.isBlockedHost(this.meta.blockedHosts, new URL(url).host)) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, urlObj.host)) { return reply.code(403).send({ error: { message: 'URL is blocked', -- cgit v1.2.3-freya From a91c0de9b5b337fdb65fbd922969132d610bd8c4 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 28 May 2025 13:32:21 -0400 Subject: cache alternate URLs in UrlPreviewService --- packages/backend/src/server/web/UrlPreviewService.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/web/UrlPreviewService.ts') diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 3e0133d50e..78b2204fbb 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -185,7 +185,7 @@ export class UrlPreviewService { return; } - const cacheKey = `${url}@${lang}@${cacheFormatVersion}`; + const cacheKey = getCacheKey(url, lang); if (await this.sendCachedPreview(cacheKey, reply, fetch)) { return; } @@ -236,6 +236,18 @@ export class UrlPreviewService { // Await this to avoid hammering redis when a bunch of URLs are fetched at once await this.previewCache.set(cacheKey, summary); + // Also cache the response URL in case of redirects + if (summary.url !== url) { + const responseCacheKey = getCacheKey(summary.url, lang); + await this.previewCache.set(responseCacheKey, summary); + } + + // Also cache the ActivityPub URL, if different from the others + if (summary.activityPub && summary.activityPub !== summary.url) { + const apCacheKey = getCacheKey(summary.activityPub, lang); + await this.previewCache.set(apCacheKey, summary); + } + // Cache 1 day (matching redis), but only once we finalize the result if (!summary.activityPub || summary.haveNoteLocally) { reply.header('Cache-Control', 'public, max-age=86400'); @@ -552,3 +564,7 @@ export class UrlPreviewService { return true; } } + +function getCacheKey(url: string, lang = 'none') { + return `${url}@${lang}@${cacheFormatVersion}`; +} -- cgit v1.2.3-freya