mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
feat(api): 🔒 Make all media be proxied through an internal proxy
This commit is contained in:
parent
9547cd097a
commit
ead34b818f
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
27
server/api/media/proxy/index.ts
Normal file
27
server/api/media/proxy/index.ts
Normal 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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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, {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue