spec(media-proxy): urlをクエリではなくパラメータで指定するように (MisskeyIO#922)
This commit is contained in:
parent
ff85d650bf
commit
6462968b9d
5 changed files with 116 additions and 67 deletions
|
@ -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' } : {}),
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
// noinspection HttpUrlsUsage
|
||||
if (url
|
||||
&& !url.startsWith('http://')
|
||||
&& !url.startsWith('https://')
|
||||
) {
|
||||
url = 'https://' + url;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
@ -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でお茶を濁す
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue