spec(media-proxy): urlをクエリではなくパラメータで指定するように (MisskeyIO#922)

This commit is contained in:
あわわわとーにゅ 2025-02-01 22:57:44 +09:00 committed by GitHub
parent ff85d650bf
commit 6462968b9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 116 additions and 67 deletions

View file

@ -77,9 +77,8 @@ export class DriveFileEntityService {
@bindThis @bindThis
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string { private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string {
return appendQuery( return appendQuery(
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`, `${this.config.mediaProxy}/${mode ?? 'image'}/${encodeURIComponent(url)}`,
query({ query({
url,
...(mode ? { [mode]: '1' } : {}), ...(mode ? { [mode]: '1' } : {}),
}), }),
); );

View file

@ -26,6 +26,7 @@ import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
import { appendQuery, query } from '@/misc/prelude/url.js';
import { correctFilename } from '@/misc/correct-filename.js'; import { correctFilename } from '@/misc/correct-filename.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
@ -35,6 +36,16 @@ const _dirname = dirname(_filename);
const assets = `${_dirname}/../../server/file/assets/`; const assets = `${_dirname}/../../server/file/assets/`;
interface TransformQuery {
origin?: string;
fallback?: string;
emoji?: string;
avatar?: string;
static?: string;
preview?: string;
badge?: string;
}
@Injectable() @Injectable()
export class FileServerService { export class FileServerService {
private logger: Logger; private logger: Logger;
@ -87,10 +98,18 @@ export class FileServerService {
done(); done();
}); });
fastify.get<{
Params: { type: string; url: string; };
Querystring: { url?: string; } & TransformQuery;
}>('/proxy/:type/:url', async (request, reply) => {
return await this.proxyHandler(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{ fastify.get<{
Params: { url: string; }; Params: { url: string; };
Querystring: { url?: string; }; Querystring: { url?: string; } & TransformQuery;
}>('/proxy/:url*', async (request, reply) => { }>('/proxy/:url', async (request, reply) => {
return await this.proxyHandler(request, reply) return await this.proxyHandler(request, reply)
.catch(err => this.errorHandler(request, reply, err)); .catch(err => this.errorHandler(request, reply, err));
}); });
@ -142,12 +161,15 @@ export class FileServerService {
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) { if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/static.webp`); const url = appendQuery(
url.searchParams.set('url', file.url); `${this.config.mediaProxy}/static/${encodeURIComponent(file.url)}`,
url.searchParams.set('static', '1'); query({
static: '1',
}),
);
file.cleanup(); file.cleanup();
return await reply.redirect(url.toString(), 301); return await reply.redirect(url, 301);
} else if (file.mime.startsWith('video/')) { } else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url); const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) { if (externalThumbnail) {
@ -163,11 +185,10 @@ export class FileServerService {
if (['image/svg+xml'].includes(file.mime)) { if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/svg.webp`); const url = `${this.config.mediaProxy}/svg/${encodeURIComponent(file.url)}`;
url.searchParams.set('url', file.url);
file.cleanup(); file.cleanup();
return await reply.redirect(url.toString(), 301); return await reply.redirect(url, 301);
} }
} }
@ -291,30 +312,43 @@ export class FileServerService {
} }
@bindThis @bindThis
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) { private async proxyHandler(request: FastifyRequest<{ Params: { type?: string; url: string; }; Querystring: { url?: string; } & TransformQuery; }>, reply: FastifyReply) {
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url; let url: string;
if ('url' in request.query && request.query.url) {
url = request.query.url;
} else {
url = request.params.url;
}
if (typeof url !== 'string') { // noinspection HttpUrlsUsage
if (url
&& !url.startsWith('http://')
&& !url.startsWith('https://')
) {
url = 'https://' + url;
}
if (!url) {
reply.code(400); reply.code(400);
return; return;
} }
// アバタークロップなど、どうしてもオリジンである必要がある場合 // アバタークロップなど、どうしてもオリジンである必要がある場合
const mustOrigin = 'origin' in request.query; const mustOrigin = 'origin' in request.query;
const transformQuery = request.query as TransformQuery;
if (this.config.externalMediaProxyEnabled && !mustOrigin) { if (this.config.externalMediaProxyEnabled && !mustOrigin) {
// 外部のメディアプロキシが有効なら、そちらにリダイレクト // 外部のメディアプロキシが有効なら、そちらにリダイレクト
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`); const url = appendQuery(
`${this.config.mediaProxy}/redirect/${encodeURIComponent(request.params.url)}`,
for (const [key, value] of Object.entries(request.query)) { query(transformQuery as Record<string, string>),
url.searchParams.append(key, value); );
}
return reply.redirect( return reply.redirect(
url.toString(), url,
301, 301,
); );
} }
@ -344,11 +378,11 @@ export class FileServerService {
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp'); const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
if ( if (
'emoji' in request.query || 'emoji' in transformQuery ||
'avatar' in request.query || 'avatar' in transformQuery ||
'static' in request.query || 'static' in transformQuery ||
'preview' in request.query || 'preview' in transformQuery ||
'badge' in request.query 'badge' in transformQuery
) { ) {
if (!isConvertibleImage) { if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す // 画像でないなら404でお茶を濁す
@ -357,17 +391,17 @@ export class FileServerService {
} }
let image: IImageStreamable | null = null; let image: IImageStreamable | null = null;
if ('emoji' in request.query || 'avatar' in request.query) { if ('emoji' in transformQuery || 'avatar' in transformQuery) {
if (!isAnimationConvertibleImage && !('static' in request.query)) { if (!isAnimationConvertibleImage && !('static' in transformQuery)) {
image = { image = {
data: fs.createReadStream(file.path), data: fs.createReadStream(file.path),
ext: file.ext, ext: file.ext,
type: file.mime, type: file.mime,
}; };
} else { } else {
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) })) const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in transformQuery) }))
.resize({ .resize({
height: 'emoji' in request.query ? 128 : 320, height: 'emoji' in transformQuery ? 128 : 320,
withoutEnlargement: true, withoutEnlargement: true,
}) })
.webp(webpDefault); .webp(webpDefault);
@ -378,11 +412,11 @@ export class FileServerService {
type: 'image/webp', type: 'image/webp',
}; };
} }
} else if ('static' in request.query) { } else if ('static' in transformQuery) {
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422); image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
} else if ('preview' in request.query) { } else if ('preview' in transformQuery) {
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200); image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
} else if ('badge' in request.query) { } else if ('badge' in transformQuery) {
const mask = (await sharpBmp(file.path, file.mime)) const mask = (await sharpBmp(file.path, file.mime))
.resize(96, 96, { .resize(96, 96, {
fit: 'contain', fit: 'contain',

View file

@ -19,6 +19,7 @@ import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import { genIdenticon } from '@/misc/gen-identicon.js'; import { genIdenticon } from '@/misc/gen-identicon.js';
import { appendQuery, query } from '@/misc/prelude/url.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -162,22 +163,28 @@ export class ServerService implements OnApplicationShutdown {
} }
} }
let url: URL; let url: string;
if ('badge' in request.query) { if ('badge' in request.query) {
url = new URL(`${this.config.mediaProxy}/emoji.png`); url = appendQuery(
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); `${this.config.mediaProxy}/emoji/${encodeURIComponent(emoji.publicUrl || emoji.originalUrl)}`,
url.searchParams.set('badge', '1'); query({
badge: '1',
}),
);
} else { } else {
url = new URL(`${this.config.mediaProxy}/emoji.webp`); url = appendQuery(
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); `${this.config.mediaProxy}/emoji/${encodeURIComponent(emoji.publicUrl || emoji.originalUrl)}`,
url.searchParams.set('emoji', '1'); query({
if ('static' in request.query) url.searchParams.set('static', '1'); emoji: '1',
...('static' in request.query ? { static: '1' } : {}),
}),
);
} }
return reply.redirect( return reply.redirect(
url.toString(), url,
301, 301,
); );
}); });

View file

@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { query } from '@/misc/prelude/url.js'; import { appendQuery, query } from '@/misc/prelude/url.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
@ -36,14 +36,15 @@ export class UrlPreviewService {
@bindThis @bindThis
private wrap(url?: string | null): string | null { private wrap(url?: string | null): string | null {
return url != null if (!url) return null;
? url.match(/^https?:\/\//) if (!RegExp(/^https?:\/\//).exec(url)) return url;
? `${this.config.mediaProxy}/preview.webp?${query({
url, return appendQuery(
`${this.config.mediaProxy}/preview/${encodeURIComponent(url)}`,
query({
preview: '1', preview: '1',
})}` }),
: url );
: null;
} }
@bindThis @bindThis

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { query } from '@/scripts/url.js'; import { appendQuery, query } from '@/scripts/url.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
@ -12,18 +12,26 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji'
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
// もう既にproxyっぽそうだったらurlを取り出す // もう既にproxyっぽそうだったらurlを取り出す
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; const url = (new URL(imageUrl)).searchParams.get('url');
if (url) {
imageUrl = url;
} else if (imageUrl.startsWith(instance.mediaProxy + '/')) {
imageUrl = imageUrl.slice(instance.mediaProxy.length + 1);
} else if (imageUrl.startsWith('/proxy/')) {
imageUrl = imageUrl.slice('/proxy/'.length);
} else if (imageUrl.startsWith(localProxy + '/')) {
imageUrl = imageUrl.slice(localProxy.length + 1);
}
} }
return `${mustOrigin ? localProxy : instance.mediaProxy}/${ return appendQuery(
type === 'preview' ? 'preview.webp' `${mustOrigin ? localProxy : instance.mediaProxy}/${type === 'preview' ? 'preview' : 'image'}/${encodeURIComponent(imageUrl)}`,
: 'image.webp' query({
}?${query({
url: imageUrl,
...(!noFallback ? { 'fallback': '1' } : {}), ...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}), ...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}), ...(mustOrigin ? { origin: '1' } : {}),
})}`; }),
);
} }
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
@ -46,8 +54,8 @@ export function getStaticImageUrl(baseUrl: string): string {
return u.href; return u.href;
} }
return `${instance.mediaProxy}/static.webp?${query({ return appendQuery(
url: u.href, `${instance.mediaProxy}/static/${encodeURIComponent(u.href)}`,
static: '1', query({ static: '1' }),
})}`; );
} }