Add signature for proxied URLs; Proxy Avatar and Banner #8

Open
interru wants to merge 1 commit from improveProxyHandling into develop
4 changed files with 24 additions and 9 deletions

View file

@ -96,6 +96,7 @@ type Source = {
inboxJobMaxAttempts?: number; inboxJobMaxAttempts?: number;
mediaProxy?: string; mediaProxy?: string;
mediaProxySecret?: string;
proxyRemoteFiles?: boolean; proxyRemoteFiles?: boolean;
videoThumbnailGenerator?: string; videoThumbnailGenerator?: string;
@ -193,6 +194,7 @@ export type Config = {
frontendEmbedEntry: string; frontendEmbedEntry: string;
frontendEmbedManifestExists: boolean; frontendEmbedManifestExists: boolean;
mediaProxy: string; mediaProxy: string;
mediaProxySecret: string;
externalMediaProxyEnabled: boolean; externalMediaProxyEnabled: boolean;
videoThumbnailGenerator: string | null; videoThumbnailGenerator: string | null;
redis: RedisOptions & RedisOptionsSource; redis: RedisOptions & RedisOptionsSource;
@ -333,6 +335,7 @@ export function loadConfig(): Config {
attachLdSignatureForRelays: config.attachLdSignatureForRelays ?? true, attachLdSignatureForRelays: config.attachLdSignatureForRelays ?? true,
checkActivityPubGetSignature: config.checkActivityPubGetSignature, checkActivityPubGetSignature: config.checkActivityPubGetSignature,
mediaProxy: externalMediaProxy ?? internalMediaProxy, mediaProxy: externalMediaProxy ?? internalMediaProxy,
mediaProxySecret: config.mediaProxySecret,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
videoThumbnailGenerator: config.videoThumbnailGenerator ? videoThumbnailGenerator: config.videoThumbnailGenerator ?
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator

View file

@ -249,10 +249,13 @@ export class ApRendererService {
} }
@bindThis @bindThis
public renderImage(file: MiDriveFile): IApImage { public renderImage(file: MiDriveFile, mode: 'avatar' | 'banner' = 'avatar'): IApImage {
return { return {
type: 'Image', type: 'Image',
url: this.driveFileEntityService.getPublicUrl(file), url: this.driveFileEntityService.getProxiedUrl(
this.driveFileEntityService.getPublicUrl(file)
mode,
),
sensitive: file.isSensitive, sensitive: file.isSensitive,
name: file.comment, name: file.comment,
}; };
@ -517,8 +520,8 @@ export class ApRendererService {
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null, summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
_misskey_summary: profile.description, _misskey_summary: profile.description,
_misskey_followedMessage: profile.followedMessage, _misskey_followedMessage: profile.followedMessage,
icon: avatar ? this.renderImage(avatar) : null, icon: avatar ? this.renderImage(avatar, 'avatar') : null,
image: banner ? this.renderImage(banner) : null, image: banner ? this.renderImage(banner, 'banner') : null,
backgroundUrl: background ? this.renderImage(background) : null, backgroundUrl: background ? this.renderImage(background) : null,
tag, tag,
manuallyApprovesFollowers: user.isLocked, manuallyApprovesFollowers: user.isLocked,

View file

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import * as crypto from 'node:crypto';
import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -74,12 +75,16 @@ export class DriveFileEntityService {
} }
@bindThis @bindThis
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string { public getProxiedUrl(url: string, mode?: 'static' | 'avatar' | 'banner'): string {
const signature = crypto.createHmac('sha256', this.config.mediaProxySecret)
.update(url)
.digest('hex')[8];
return appendQuery( return appendQuery(
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`, `${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
query({ query({
url, url,
...(mode ? { [mode]: '1' } : {}), ...(mode ? { [mode]: '1' } : {}),
sign: signature
}), }),
); );
} }

View file

@ -4,6 +4,7 @@
*/ */
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as crypto from 'node:crypto';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
@ -317,10 +318,13 @@ export class FileServerService {
); );
} }
if (!request.headers['user-agent']) { const signature = crypto.createHmac('sha256', this.config.mediaProxySecret)
throw new StatusError('User-Agent is required', 400, 'User-Agent is required'); .update(url)
} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) { .digest('hex')[8];
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
if (signature !== request.params.sign) {
reply.code(403);
return;
} }
// Create temp file // Create temp file