From ead34b818f539cbae9a6feeb49cfb0cf0b01800f Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 4 May 2024 19:13:23 -1000 Subject: [PATCH] feat(api): :lock: Make all media be proxied through an internal proxy --- database/entities/Attachment.ts | 7 ++++--- database/entities/Emoji.ts | 5 +++-- packages/database-interface/note.ts | 9 ++------- packages/database-interface/user.ts | 9 +++++---- server/api/media/[id]/index.ts | 2 +- server/api/media/proxy/index.ts | 27 +++++++++++++++++++++++++++ utils/response.ts | 11 +++++++++++ utils/sanitization.ts | 2 +- 8 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 server/api/media/proxy/index.ts diff --git a/database/entities/Attachment.ts b/database/entities/Attachment.ts index 591162a8..6ce6cdaa 100644 --- a/database/entities/Attachment.ts +++ b/database/entities/Attachment.ts @@ -1,3 +1,4 @@ +import { proxyUrl } from "@response"; import type { Config } from "config-manager"; import type { InferSelectModel } from "drizzle-orm"; import type * as Lysand from "lysand-types"; @@ -25,9 +26,9 @@ export const attachmentToAPI = ( return { id: attachment.id, type: type as "image" | "video" | "audio" | "unknown", - url: attachment.url, - remote_url: attachment.remoteUrl, - preview_url: attachment.thumbnailUrl || attachment.url, + url: proxyUrl(attachment.url) ?? "", + remote_url: proxyUrl(attachment.remoteUrl), + preview_url: proxyUrl(attachment.thumbnailUrl || attachment.url), text_url: null, meta: { width: attachment.width || undefined, diff --git a/database/entities/Emoji.ts b/database/entities/Emoji.ts index 42c8d86a..796ba527 100644 --- a/database/entities/Emoji.ts +++ b/database/entities/Emoji.ts @@ -1,3 +1,4 @@ +import { proxyUrl } from "@response"; import { type InferSelectModel, and, eq } from "drizzle-orm"; import type * as Lysand from "lysand-types"; import { db } from "~drizzle/db"; @@ -93,8 +94,8 @@ export const fetchEmoji = async ( export const emojiToAPI = (emoji: EmojiWithInstance): APIEmoji => { return { shortcode: emoji.shortcode, - static_url: emoji.url, // TODO: Add static version - url: emoji.url, + static_url: proxyUrl(emoji.url) ?? "", // TODO: Add static version + url: proxyUrl(emoji.url) ?? "", visible_in_picker: emoji.visibleInPicker, category: undefined, }; diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index adcabfaa..2ca98bca 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -1,3 +1,4 @@ +import { sanitizedHtmlStrip } from "@sanitization"; import { type InferInsertModel, type SQL, @@ -45,7 +46,6 @@ import { config } from "~packages/config-manager"; import type { Attachment as APIAttachment } from "~types/mastodon/attachment"; import type { Status as APIStatus } from "~types/mastodon/status"; import { User } from "./user"; -import { sanitizedHtmlStrip } from "@sanitization"; /** * Gives helpers to fetch notes from database in a nice format @@ -494,12 +494,7 @@ export class Note { sensitive: data.sensitive, spoiler_text: data.spoilerText, tags: [], - uri: - data.uri || - new URL( - `/@${data.author.username}/${data.id}`, - config.http.base_url, - ).toString(), + uri: data.uri || this.getMastoURI(), visibility: data.visibility as APIStatus["visibility"], url: data.uri || this.getMastoURI(), bookmarked: false, diff --git a/packages/database-interface/user.ts b/packages/database-interface/user.ts index 813dc6c1..2322fe10 100644 --- a/packages/database-interface/user.ts +++ b/packages/database-interface/user.ts @@ -1,6 +1,7 @@ import { idValidator } from "@api"; import { getBestContentType, urlToContentFormat } from "@content_types"; import { addUserToMeilisearch } from "@meilisearch"; +import { proxyUrl } from "@response"; import { type SQL, and, desc, eq, inArray } from "drizzle-orm"; import { htmlToText } from "html-to-text"; import type * as Lysand from "lysand-types"; @@ -367,8 +368,8 @@ export class User { url: user.uri || new URL(`/@${user.username}`, config.http.base_url).toString(), - avatar: this.getAvatarUrl(config), - header: this.getHeaderUrl(config), + avatar: proxyUrl(this.getAvatarUrl(config)) ?? "", + header: proxyUrl(this.getHeaderUrl(config)) ?? "", locked: user.isLocked, created_at: new Date(user.createdAt).toISOString(), followers_count: user.followerCount, @@ -382,8 +383,8 @@ export class User { bot: user.isBot, source: isOwnAccount ? user.source : undefined, // TODO: Add static avatar and header - avatar_static: this.getAvatarUrl(config), - header_static: this.getHeaderUrl(config), + avatar_static: proxyUrl(this.getAvatarUrl(config)) ?? "", + header_static: proxyUrl(this.getHeaderUrl(config)) ?? "", acct: this.getAcct(), // TODO: Add these fields limited: false, diff --git a/server/api/media/[id]/index.ts b/server/api/media/[id]/index.ts index efe78af8..8c42790b 100644 --- a/server/api/media/[id]/index.ts +++ b/server/api/media/[id]/index.ts @@ -3,7 +3,7 @@ import { errorResponse, response } from "@response"; export const meta = applyConfig({ allowedMethods: ["GET"], - route: "/api/v1/media/:id", + route: "/api/media/:id", ratelimits: { max: 100, duration: 60, diff --git a/server/api/media/proxy/index.ts b/server/api/media/proxy/index.ts new file mode 100644 index 00000000..75ff170b --- /dev/null +++ b/server/api/media/proxy/index.ts @@ -0,0 +1,27 @@ +import { apiRoute, applyConfig } from "@api"; +import { errorResponse, response } from "@response"; +import { z } from "zod"; + +export const meta = applyConfig({ + allowedMethods: ["GET"], + route: "/api/media/proxy", + ratelimits: { + max: 100, + duration: 60, + }, + auth: { + required: false, + }, +}); + +export const schema = z.object({ + url: z.string(), +}); + +export default apiRoute( + async (req, matchedRoute, extraData) => { + const { url } = extraData.parsedRequest; + + return fetch(url); + }, +); diff --git a/utils/response.ts b/utils/response.ts index c9cf9e66..d74502a6 100644 --- a/utils/response.ts +++ b/utils/response.ts @@ -1,3 +1,5 @@ +import { config } from "~packages/config-manager"; + export const response = ( data: BodyInit | null = null, status = 200, @@ -69,3 +71,12 @@ export const redirect = (url: string | URL, status = 302) => { Location: url.toString(), }); }; + +export const proxyUrl = (url: string | null) => { + return url + ? new URL( + `/media/proxy?url=${encodeURIComponent(url)}`, + config.http.base_url, + ).toString() + : url; +}; diff --git a/utils/sanitization.ts b/utils/sanitization.ts index 9e8486b3..09938f35 100644 --- a/utils/sanitization.ts +++ b/utils/sanitization.ts @@ -1,5 +1,5 @@ -import xss, { type IFilterXSSOptions } from "xss"; import { stringifyEntitiesLight } from "stringify-entities"; +import xss, { type IFilterXSSOptions } from "xss"; export const sanitizedHtmlStrip = (html: string) => { return sanitizeHtml(html, {