feat(api): 🔒 Make all media be proxied through an internal proxy

This commit is contained in:
Jesse Wierzbinski 2024-05-04 19:13:23 -10:00
parent 9547cd097a
commit ead34b818f
No known key found for this signature in database
8 changed files with 54 additions and 18 deletions

View file

@ -1,3 +1,4 @@
import { proxyUrl } from "@response";
import type { Config } from "config-manager"; import type { Config } from "config-manager";
import type { InferSelectModel } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
@ -25,9 +26,9 @@ export const attachmentToAPI = (
return { return {
id: attachment.id, id: attachment.id,
type: type as "image" | "video" | "audio" | "unknown", type: type as "image" | "video" | "audio" | "unknown",
url: attachment.url, url: proxyUrl(attachment.url) ?? "",
remote_url: attachment.remoteUrl, remote_url: proxyUrl(attachment.remoteUrl),
preview_url: attachment.thumbnailUrl || attachment.url, preview_url: proxyUrl(attachment.thumbnailUrl || attachment.url),
text_url: null, text_url: null,
meta: { meta: {
width: attachment.width || undefined, width: attachment.width || undefined,

View file

@ -1,3 +1,4 @@
import { proxyUrl } from "@response";
import { type InferSelectModel, and, eq } from "drizzle-orm"; import { type InferSelectModel, and, eq } from "drizzle-orm";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
@ -93,8 +94,8 @@ export const fetchEmoji = async (
export const emojiToAPI = (emoji: EmojiWithInstance): APIEmoji => { export const emojiToAPI = (emoji: EmojiWithInstance): APIEmoji => {
return { return {
shortcode: emoji.shortcode, shortcode: emoji.shortcode,
static_url: emoji.url, // TODO: Add static version static_url: proxyUrl(emoji.url) ?? "", // TODO: Add static version
url: emoji.url, url: proxyUrl(emoji.url) ?? "",
visible_in_picker: emoji.visibleInPicker, visible_in_picker: emoji.visibleInPicker,
category: undefined, category: undefined,
}; };

View file

@ -1,3 +1,4 @@
import { sanitizedHtmlStrip } from "@sanitization";
import { import {
type InferInsertModel, type InferInsertModel,
type SQL, type SQL,
@ -45,7 +46,6 @@ import { config } from "~packages/config-manager";
import type { Attachment as APIAttachment } from "~types/mastodon/attachment"; import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
import type { Status as APIStatus } from "~types/mastodon/status"; import type { Status as APIStatus } from "~types/mastodon/status";
import { User } from "./user"; import { User } from "./user";
import { sanitizedHtmlStrip } from "@sanitization";
/** /**
* Gives helpers to fetch notes from database in a nice format * Gives helpers to fetch notes from database in a nice format
@ -494,12 +494,7 @@ export class Note {
sensitive: data.sensitive, sensitive: data.sensitive,
spoiler_text: data.spoilerText, spoiler_text: data.spoilerText,
tags: [], tags: [],
uri: uri: data.uri || this.getMastoURI(),
data.uri ||
new URL(
`/@${data.author.username}/${data.id}`,
config.http.base_url,
).toString(),
visibility: data.visibility as APIStatus["visibility"], visibility: data.visibility as APIStatus["visibility"],
url: data.uri || this.getMastoURI(), url: data.uri || this.getMastoURI(),
bookmarked: false, bookmarked: false,

View file

@ -1,6 +1,7 @@
import { idValidator } from "@api"; import { idValidator } from "@api";
import { getBestContentType, urlToContentFormat } from "@content_types"; import { getBestContentType, urlToContentFormat } from "@content_types";
import { addUserToMeilisearch } from "@meilisearch"; import { addUserToMeilisearch } from "@meilisearch";
import { proxyUrl } from "@response";
import { type SQL, and, desc, eq, inArray } from "drizzle-orm"; import { type SQL, and, desc, eq, inArray } from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import type * as Lysand from "lysand-types"; import type * as Lysand from "lysand-types";
@ -367,8 +368,8 @@ export class User {
url: url:
user.uri || user.uri ||
new URL(`/@${user.username}`, config.http.base_url).toString(), new URL(`/@${user.username}`, config.http.base_url).toString(),
avatar: this.getAvatarUrl(config), avatar: proxyUrl(this.getAvatarUrl(config)) ?? "",
header: this.getHeaderUrl(config), header: proxyUrl(this.getHeaderUrl(config)) ?? "",
locked: user.isLocked, locked: user.isLocked,
created_at: new Date(user.createdAt).toISOString(), created_at: new Date(user.createdAt).toISOString(),
followers_count: user.followerCount, followers_count: user.followerCount,
@ -382,8 +383,8 @@ export class User {
bot: user.isBot, bot: user.isBot,
source: isOwnAccount ? user.source : undefined, source: isOwnAccount ? user.source : undefined,
// TODO: Add static avatar and header // TODO: Add static avatar and header
avatar_static: this.getAvatarUrl(config), avatar_static: proxyUrl(this.getAvatarUrl(config)) ?? "",
header_static: this.getHeaderUrl(config), header_static: proxyUrl(this.getHeaderUrl(config)) ?? "",
acct: this.getAcct(), acct: this.getAcct(),
// TODO: Add these fields // TODO: Add these fields
limited: false, limited: false,

View file

@ -3,7 +3,7 @@ import { errorResponse, response } from "@response";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
route: "/api/v1/media/:id", route: "/api/media/:id",
ratelimits: { ratelimits: {
max: 100, max: 100,
duration: 60, duration: 60,

View file

@ -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<typeof meta, typeof schema>(
async (req, matchedRoute, extraData) => {
const { url } = extraData.parsedRequest;
return fetch(url);
},
);

View file

@ -1,3 +1,5 @@
import { config } from "~packages/config-manager";
export const response = ( export const response = (
data: BodyInit | null = null, data: BodyInit | null = null,
status = 200, status = 200,
@ -69,3 +71,12 @@ export const redirect = (url: string | URL, status = 302) => {
Location: url.toString(), Location: url.toString(),
}); });
}; };
export const proxyUrl = (url: string | null) => {
return url
? new URL(
`/media/proxy?url=${encodeURIComponent(url)}`,
config.http.base_url,
).toString()
: url;
};

View file

@ -1,5 +1,5 @@
import xss, { type IFilterXSSOptions } from "xss";
import { stringifyEntitiesLight } from "stringify-entities"; import { stringifyEntitiesLight } from "stringify-entities";
import xss, { type IFilterXSSOptions } from "xss";
export const sanitizedHtmlStrip = (html: string) => { export const sanitizedHtmlStrip = (html: string) => {
return sanitizeHtml(html, { return sanitizeHtml(html, {