feat(media-proxy): sign attachment URLs and gate origins (#2830)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-18 15:57:32 +02:00
1 parent 025c01ab13
commit 522cf08e61
108 files changed
+10148 -441

No files matched your search

@@ -0,0 +1,38 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {signAttachmentUrl} from '@app/api/attachment/AttachmentUrls';
import {LoginRequired} from '@app/api/middleware/AuthMiddleware';
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
import {RateLimitConfigs} from '@app/api/RateLimitConfig';
import type {HonoApp} from '@app/api/types/HonoEnv';
import {Validator} from '@app/api/Validator';
import {
RefreshAttachmentUrlsRequest,
RefreshAttachmentUrlsResponse,
} from '@fluxer/schema/src/domains/message/AttachmentSchemas';
export function AttachmentController(app: HonoApp) {
app.post(
'/attachments/refresh-urls',
RateLimitMiddleware(RateLimitConfigs.ATTACHMENT_URLS_REFRESH),
LoginRequired,
Validator('json', RefreshAttachmentUrlsRequest),
OpenAPI({
operationId: 'refresh_attachment_urls',
summary: 'Refresh attachment URLs',
responseSchema: RefreshAttachmentUrlsResponse,
statusCode: 200,
security: ['botToken', 'sessionToken'],
tags: ['Messages'],
description:
'Reissues the expiring signature on attachment URLs. Returns one entry per requested URL, in the order they were requested, each pairing the URL exactly as it was sent with a freshly signed copy. A URL that is not an attachment URL of this instance is returned unchanged. No membership or existence check is performed.',
}),
async (ctx) => {
const urls = ctx.req.valid('json').attachment_urls;
return ctx.json({
refreshed_urls: urls.map((original) => ({original, refreshed: signAttachmentUrl(original)})),
});
},
);
}
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {AttachmentID, ChannelID} from '@app/api/BrandedTypes';
import {Config} from '@app/api/Config';
import {makeAttachmentCdnUrl} from '@app/api/channel/services/message/MessageHelpers';
import {extractTimestampBigInt} from '@fluxer/snowflake/src/SnowflakeUtils';
import {
attachmentStorageKeyFromUrl,
type SignAttachmentUrlOptions,
signDataPackageAttachmentUrl as signDataPackageWithSecret,
signAttachmentUrl as signWithSecret,
stripAttachmentSignature as stripSignature,
} from '@pkgs/media_proxy_utils/src/AttachmentUrlSignature';
const SNOWFLAKE_SEGMENT_REGEX = /^[0-9]{1,20}$/u;
const STORAGE_KEY_MIN_SEGMENTS = 4;
function signingSecret(): Buffer | null {
const configured = Config.mediaProxy.attachmentUrls.secretsBase64[0];
if (!configured) return null;
const secret = Buffer.from(configured, 'base64');
return secret.length === 0 ? null : secret;
}
function anchorSecsFromStorageKey(storageKey: string): number | null {
const segments = storageKey.split('/');
if (segments.length < STORAGE_KEY_MIN_SEGMENTS || segments[0] !== 'attachments') return null;
const attachmentId = segments[2] as string;
if (!SNOWFLAKE_SEGMENT_REGEX.test(segments[1] as string) || !SNOWFLAKE_SEGMENT_REGEX.test(attachmentId)) return null;
return Math.floor(extractTimestampBigInt(BigInt(attachmentId)) / 1000);
}
function signingOptions(url: string, nowSecs?: number): SignAttachmentUrlOptions | null {
const secret = signingSecret();
if (secret === null) return null;
const mediaEndpoint = Config.endpoints.media;
const storageKey = attachmentStorageKeyFromUrl(url, mediaEndpoint);
if (storageKey === null) return null;
const anchorSecs = anchorSecsFromStorageKey(storageKey);
if (anchorSecs === null) return null;
return {mediaEndpoint, secret, nowSecs: nowSecs ?? Math.floor(Date.now() / 1000), anchorSecs};
}
export function signAttachmentUrl(url: string, nowSecs?: number): string {
const options = signingOptions(url, nowSecs);
return options === null ? url : signWithSecret(url, options);
}
export function signDataPackageAttachmentUrl(url: string, nowSecs?: number): string {
const options = signingOptions(url, nowSecs);
return options === null ? url : signDataPackageWithSecret(url, options);
}
export function stripAttachmentSignature(url: string): string {
return stripSignature(url);
}
export function stripOwnAttachmentSignature(url: string): string {
return attachmentStorageKeyFromUrl(url, Config.endpoints.media) === null ? url : stripSignature(url);
}
export function makeSignedAttachmentCdnUrl(
channelId: ChannelID,
attachmentId: AttachmentID | bigint,
filename: string,
): string {
return signAttachmentUrl(makeAttachmentCdnUrl(channelId, attachmentId, filename));
}
export function makeDataPackageAttachmentCdnUrl(
channelId: ChannelID,
attachmentId: AttachmentID | bigint,
filename: string,
): string {
return signDataPackageAttachmentUrl(makeAttachmentCdnUrl(channelId, attachmentId, filename));
}