Skip to content

Commit

Permalink
spec(media-proxy): urlをクエリではなくパラメータで指定するように (MisskeyIO#922)
Browse files Browse the repository at this point in the history
  • Loading branch information
u1-liquid authored Feb 1, 2025
1 parent ff85d65 commit 6462968
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 67 deletions.
3 changes: 1 addition & 2 deletions packages/backend/src/core/entities/DriveFileEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,8 @@ export class DriveFileEntityService {
@bindThis
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string {
return appendQuery(
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
`${this.config.mediaProxy}/${mode ?? 'image'}/${encodeURIComponent(url)}`,
query({
url,
...(mode ? { [mode]: '1' } : {}),
}),
);
Expand Down
94 changes: 64 additions & 30 deletions packages/backend/src/server/FileServerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.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 { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
Expand All @@ -35,6 +36,16 @@ const _dirname = dirname(_filename);

const assets = `${_dirname}/../../server/file/assets/`;

interface TransformQuery {
origin?: string;
fallback?: string;
emoji?: string;
avatar?: string;
static?: string;
preview?: string;
badge?: string;
}

@Injectable()
export class FileServerService {
private logger: Logger;
Expand Down Expand Up @@ -87,10 +98,18 @@ export class FileServerService {
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<{
Params: { url: string; };
Querystring: { url?: string; };
}>('/proxy/:url*', async (request, reply) => {
Querystring: { url?: string; } & TransformQuery;
}>('/proxy/:url', async (request, reply) => {
return await this.proxyHandler(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
Expand Down Expand Up @@ -142,12 +161,15 @@ export class FileServerService {
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
reply.header('Cache-Control', 'max-age=31536000, immutable');

const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url);
url.searchParams.set('static', '1');
const url = appendQuery(
`${this.config.mediaProxy}/static/${encodeURIComponent(file.url)}`,
query({
static: '1',
}),
);

file.cleanup();
return await reply.redirect(url.toString(), 301);
return await reply.redirect(url, 301);
} else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) {
Expand All @@ -163,11 +185,10 @@ export class FileServerService {
if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable');

const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url);
const url = `${this.config.mediaProxy}/svg/${encodeURIComponent(file.url)}`;

file.cleanup();
return await reply.redirect(url.toString(), 301);
return await reply.redirect(url, 301);
}
}

Expand Down Expand Up @@ -291,30 +312,43 @@ export class FileServerService {
}

@bindThis
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
private async proxyHandler(request: FastifyRequest<{ Params: { type?: string; url: string; }; Querystring: { url?: string; } & TransformQuery; }>, reply: FastifyReply) {
let url: string;
if ('url' in request.query && request.query.url) {
url = request.query.url;
} else {
url = request.params.url;
}

// noinspection HttpUrlsUsage
if (url
&& !url.startsWith('http://')
&& !url.startsWith('https://')
) {
url = 'https://' + url;
}

if (typeof url !== 'string') {
if (!url) {
reply.code(400);
return;
}

// アバタークロップなど、どうしてもオリジンである必要がある場合
const mustOrigin = 'origin' in request.query;
const transformQuery = request.query as TransformQuery;

if (this.config.externalMediaProxyEnabled && !mustOrigin) {
// 外部のメディアプロキシが有効なら、そちらにリダイレクト

reply.header('Cache-Control', 'public, max-age=259200'); // 3 days

const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);

for (const [key, value] of Object.entries(request.query)) {
url.searchParams.append(key, value);
}
const url = appendQuery(
`${this.config.mediaProxy}/redirect/${encodeURIComponent(request.params.url)}`,
query(transformQuery as Record<string, string>),
);

return reply.redirect(
url.toString(),
url,
301,
);
}
Expand Down Expand Up @@ -344,11 +378,11 @@ export class FileServerService {
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');

if (
'emoji' in request.query ||
'avatar' in request.query ||
'static' in request.query ||
'preview' in request.query ||
'badge' in request.query
'emoji' in transformQuery ||
'avatar' in transformQuery ||
'static' in transformQuery ||
'preview' in transformQuery ||
'badge' in transformQuery
) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
Expand All @@ -357,17 +391,17 @@ export class FileServerService {
}

let image: IImageStreamable | null = null;
if ('emoji' in request.query || 'avatar' in request.query) {
if (!isAnimationConvertibleImage && !('static' in request.query)) {
if ('emoji' in transformQuery || 'avatar' in transformQuery) {
if (!isAnimationConvertibleImage && !('static' in transformQuery)) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
} 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({
height: 'emoji' in request.query ? 128 : 320,
height: 'emoji' in transformQuery ? 128 : 320,
withoutEnlargement: true,
})
.webp(webpDefault);
Expand All @@ -378,11 +412,11 @@ export class FileServerService {
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);
} else if ('preview' in request.query) {
} else if ('preview' in transformQuery) {
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))
.resize(96, 96, {
fit: 'contain',
Expand Down
29 changes: 18 additions & 11 deletions packages/backend/src/server/ServerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js';
import { genIdenticon } from '@/misc/gen-identicon.js';
import { appendQuery, query } from '@/misc/prelude/url.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
Expand Down Expand Up @@ -162,22 +163,28 @@ export class ServerService implements OnApplicationShutdown {
}
}

let url: URL;
let url: string;
if ('badge' in request.query) {
url = new URL(`${this.config.mediaProxy}/emoji.png`);
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('badge', '1');
url = appendQuery(
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
`${this.config.mediaProxy}/emoji/${encodeURIComponent(emoji.publicUrl || emoji.originalUrl)}`,
query({
badge: '1',
}),
);
} else {
url = new URL(`${this.config.mediaProxy}/emoji.webp`);
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('emoji', '1');
if ('static' in request.query) url.searchParams.set('static', '1');
url = appendQuery(
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
`${this.config.mediaProxy}/emoji/${encodeURIComponent(emoji.publicUrl || emoji.originalUrl)}`,
query({
emoji: '1',
...('static' in request.query ? { static: '1' } : {}),
}),
);
}

return reply.redirect(
url.toString(),
url,
301,
);
});
Expand Down
19 changes: 10 additions & 9 deletions packages/backend/src/server/web/UrlPreviewService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.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 { bindThis } from '@/decorators.js';
import { ApiError } from '@/server/api/error.js';
Expand All @@ -36,14 +36,15 @@ export class UrlPreviewService {

@bindThis
private wrap(url?: string | null): string | null {
return url != null
? url.match(/^https?:\/\//)
? `${this.config.mediaProxy}/preview.webp?${query({
url,
preview: '1',
})}`
: url
: null;
if (!url) return null;
if (!RegExp(/^https?:\/\//).exec(url)) return url;

return appendQuery(
`${this.config.mediaProxy}/preview/${encodeURIComponent(url)}`,
query({
preview: '1',
}),
);
}

@bindThis
Expand Down
38 changes: 23 additions & 15 deletions packages/frontend/src/scripts/media-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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 { instance } from '@/instance.js';

Expand All @@ -12,18 +12,26 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji'

if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
// もう既に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}/${
type === 'preview' ? 'preview.webp'
: 'image.webp'
}?${query({
url: imageUrl,
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
})}`;
return appendQuery(
`${mustOrigin ? localProxy : instance.mediaProxy}/${type === 'preview' ? 'preview' : 'image'}/${encodeURIComponent(imageUrl)}`,
query({
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
}),
);
}

export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
Expand All @@ -46,8 +54,8 @@ export function getStaticImageUrl(baseUrl: string): string {
return u.href;
}

return `${instance.mediaProxy}/static.webp?${query({
url: u.href,
static: '1',
})}`;
return appendQuery(
`${instance.mediaProxy}/static/${encodeURIComponent(u.href)}`,
query({ static: '1' }),
);
}

0 comments on commit 6462968

Please sign in to comment.