mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
feat(media-proxy): sign attachment URLs and gate origins (#2830)
This commit is contained in:
1 parent
025c01ab13
commit
522cf08e61
108 files changed
+10148
-441
No files matched your search
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {signAttachmentUrl} from '@app/api/attachment/AttachmentUrls';
|
||||
import {Config} from '@app/api/Config';
|
||||
import {
|
||||
IMediaService,
|
||||
@@ -9,11 +10,13 @@ import {
|
||||
type MediaProxyMetadataRequest,
|
||||
type MediaProxyMetadataResponse,
|
||||
type MediaProxyNsfwMode,
|
||||
type MediaProxySniffResponse,
|
||||
} from '@app/api/infrastructure/IMediaService';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import * as FetchUtils from '@app/api/utils/FetchUtils';
|
||||
import {isJsonRecord, parseJsonWithGuard} from '@app/api/utils/JsonBoundaryUtils';
|
||||
import {ExplicitContentCannotBeSentError} from '@fluxer/errors/src/domains/moderation/ExplicitContentCannotBeSentError';
|
||||
import {attachmentStorageKeyFromUrl} from '@pkgs/media_proxy_utils/src/AttachmentUrlSignature';
|
||||
import * as MediaProxyUtils from '@pkgs/media_proxy_utils/src/MediaProxyUtils';
|
||||
import {ms} from 'itty-time';
|
||||
|
||||
@@ -30,7 +33,13 @@ const MEDIA_PROXY_METADATA_WITH_BASE64_MAX_BYTES = 64 * 1024 * 1024;
|
||||
const MEDIA_PROXY_ERROR_MAX_BYTES = 16 * 1024;
|
||||
const MEDIA_PROXY_THUMBNAIL_MAX_BYTES = 8 * 1024 * 1024;
|
||||
const MEDIA_PROXY_FRAMES_MAX_BYTES = 512 * 1024;
|
||||
const MEDIA_PROXY_SNIFF_MAX_BYTES = 1024;
|
||||
const MEDIA_PROXY_REQUEST_TIMEOUT_MS = ms('30 seconds');
|
||||
const MEDIA_PROXY_SNIFF_TIMEOUT_MS = ms('2 seconds');
|
||||
|
||||
function isMediaProxySniffResponse(value: unknown): value is MediaProxySniffResponse {
|
||||
return isJsonRecord(value) && (value.content_type === null || typeof value.content_type === 'string');
|
||||
}
|
||||
|
||||
function isMediaProxyMetadataResponse(value: unknown): value is MediaProxyMetadataResponse {
|
||||
if (!isJsonRecord(value)) return false;
|
||||
@@ -135,6 +144,9 @@ export class MediaService extends IMediaService {
|
||||
}
|
||||
|
||||
getExternalMediaProxyURL(url: string): string {
|
||||
if (attachmentStorageKeyFromUrl(url, Config.endpoints.media) !== null) {
|
||||
return signAttachmentUrl(url);
|
||||
}
|
||||
let urlObj: URL;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
@@ -166,6 +178,34 @@ export class MediaService extends IMediaService {
|
||||
}
|
||||
}
|
||||
|
||||
async sniffUpload(uploadFilename: string): Promise<MediaProxySniffResponse | null> {
|
||||
const response = await this.makeRequest(
|
||||
'/_sniff',
|
||||
{
|
||||
type: 'upload',
|
||||
upload_filename: uploadFilename,
|
||||
},
|
||||
MEDIA_PROXY_SNIFF_TIMEOUT_MS,
|
||||
);
|
||||
if (!response) return null;
|
||||
try {
|
||||
const responseText = await FetchUtils.streamToStringWithLimit(response.body, {
|
||||
maxBytes: MEDIA_PROXY_SNIFF_MAX_BYTES,
|
||||
headers: response.headers,
|
||||
description: 'Media proxy sniff response',
|
||||
});
|
||||
const sniff = parseJsonWithGuard(responseText, isMediaProxySniffResponse);
|
||||
if (!sniff) {
|
||||
Logger.error({uploadFilename}, 'Media proxy returned invalid sniff response');
|
||||
return null;
|
||||
}
|
||||
return {content_type: sniff.content_type};
|
||||
} catch (error) {
|
||||
Logger.error({error, uploadFilename}, 'Failed to read media proxy sniff response');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async extractFrames(request: MediaProxyFrameRequest): Promise<MediaProxyFrameResponse> {
|
||||
const response = await this.makeRequest('/_frames', request);
|
||||
if (!response) {
|
||||
@@ -183,7 +223,11 @@ export class MediaService extends IMediaService {
|
||||
return data;
|
||||
}
|
||||
|
||||
private async makeRequest(endpoint: string, body: MediaProxyRequestBody): Promise<Response | null> {
|
||||
private async makeRequest(
|
||||
endpoint: string,
|
||||
body: MediaProxyRequestBody,
|
||||
timeoutMs: number = MEDIA_PROXY_REQUEST_TIMEOUT_MS,
|
||||
): Promise<Response | null> {
|
||||
try {
|
||||
const url = `http://${Config.mediaProxy.host}:${Config.mediaProxy.port}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
@@ -193,7 +237,7 @@ export class MediaService extends IMediaService {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${Config.mediaProxy.secretKey}`,
|
||||
},
|
||||
signal: AbortSignal.timeout(MEDIA_PROXY_REQUEST_TIMEOUT_MS),
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await FetchUtils.streamToStringWithLimit(response.body, {
|
||||
|
||||
Reference in new issue
Block a user