This commit is contained in:
tamaina 2023-02-28 08:03:15 +00:00
parent 0f65312eef
commit 808dacda41
6 changed files with 94 additions and 20 deletions

View File

@ -1,5 +1,7 @@
# Media Proxy for Misskey
[→ メディアプロキシの仕様](./SPECIFICATION.md)
Misskeyの/proxyが単体で動作しますMisskeyのコードがほぼそのまま移植されています
/proxyは画像ではないと403を返しますが、Media Proxyではそのまま内容を送信します。
@ -61,10 +63,6 @@ export default {
maxSize: 262144000,
// CORS
// WARN:
// 'Access-Control-Allow-Origin'を'*'に設定した場合、要求のOriginヘッダーを応答します。
// Misskeyのアバタークロップに必要なため
// Varyヘッダーが付加されるため、同じURLでもOriginごとに画像が生成されてしまうはずです。
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',

81
SPECIFICATION.md Normal file
View File

@ -0,0 +1,81 @@
# Misskeyメディアプロキシ仕様書
## メディアプロキシの種類と目的
Misskeyメディアプロキシは、リモートのファイルをインスタンス管理者が管理するドメインでプロキシ配信し、また、縮小・加工された画像を提供するためのアプリケーションである。
Misskeyサーバー本体の/proxy/で提供されている「本体メディアプロキシ (local media proxy)」と、[github.com/misskey-dev/media-proxy](https://github.com/misskey-dev/media-proxy)で配布されている「外部メディアプロキシ (external media proxy)」がある。
外部メディアプロキシを設定・使用することで、本体のサーバー負荷を軽減できる。また、複数のインスタンスで外部プロキシを共用すると、さらなる負荷軽減が期待できる。
## 外部メディアプロキシの設定と使用
外部メディアプロキシを設定するには、[README.md](./README.md)に記載されている通りインストールする。
外部メディアプロキシが設定されている場合、本体メディアプロキシは外部メディアプロキシへ301リダイレクトを返答するoriginクエリが指定されている場合を除く
Misskeyサーバーのapi/metaの応答に、使用するべきメディアプロキシのURLを示す`mediaProxy`プロパティが存在する。
外部メディアプロキシが指定されているならそのURLが、指定されていなければ本体メディアプロキシ(/proxy/)のURLが入っている。
本体メディアプロキシはリダイレクトを行うものの、Misskeyクライアントは`mediaProxy`の値に応じて適切なメディアプロキシへ直接要求を行うべきである。
メディアプロキシへは、クエリ文字列によって命令を行う。
拡張子によってキャッシュの挙動を変えるCDNがあるため、image.webp、avatar.webp、static.webpなどの適当なファイル名を付加するべきである。
例:
`https://example.com/proxy/image.webp?url=https%3A%2F%2F......`
Acceptヘッダーは無視される。
Cache-Controlは、正常なレスポンスの場合`max-age=31536000, immutable`、エラーレスポンスの場合`max-age=300`である。
Content-Typeは、ファイルの内容について適切なものが挿入される。
Content-Security-Policyは、`default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`となっている。
### クエリの一覧
#### url (必須)
変換ないしはプロキシを行う対象の、元画像のURLを指定する。
指定がなかった場合はHTTPコード400が返される。
https://www.google.com/images/errors/robot.png をプロキシする場合:
`https://example.com/proxy/image.webp?url=https%3A%2F%2Fwww.google.com%2Fimages%2Ferrors%2Frobot.png`
#### origin (本体のみ)
存在すると、外部メディアプロキシへのリダイレクトを行わない。
`https://example.com/proxy/image.webp?url=https%3A%2F%2F...&origin=1`
「存在すると」というのは、Fastifyで`'origin' in request.query`がtureになる場合という意味である。以下同様。
#### fallback
存在すると、元画像に到達できなかったり画像の変換中にエラーが起きたりした場合、正常なレスポンスCache-Controlは`max-age=300`)としてフォールバック画像(カラーバー)が表示される。
#### 変換クエリが存在しない場合の挙動
次の項目からは変換形式を指定するクエリとなっている。
変換形式が指定されていなかった場合は、画像ファイルもしくは許可されたファイルFILE_TYPE_BROWSERSAFEである場合のみプロキシファイルの再配信が行われる。
ただし、svgは、webpに変換される最大サイズ2048x2048
#### emoji
存在すると、高さ128px以下のwebpが応答される。
ただし、sharp.jsの都合により、元画像がapngの場合は無変換で応答される。
`https://example.com/proxy/emoji.webp?url=https%3A%2F%2F...&emoji=1`
「以下」というのは、元画像がこれ未満だった場合は拡大を行わないという意味である。以下同様。
#### avatar
存在すると、高さ320px以下のwebpが応答される。
ただし、sharp.jsの都合により、元画像がapngの場合は無変換で応答される。
`https://example.com/proxy/avatar.webp?url=https%3A%2F%2F...&avatar=1`
#### static
存在すると、アニメーション画像では最初のフレームのみの静止画のwebpが応答される。
emojiまたはavatarとstaticが同時に指定された場合は、それぞれに応じた高さが、指定されていない場合は幅498px・高さ280pxに収まるサイズ以下に縮小される。
#### preview
存在すると、幅200px・高さ200pxに収まるサイズ以下のwebpが応答される。
#### badge
Webプッシュ通知のバッジに適したpngが応答される。
https://developer.mozilla.org/ja/docs/Web/API/Notification/badge
サイズは96x96で、元画像がアルファチャンネルのみで表現される。

BIN
assets/dummy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import fastifyStatic from '@fastify/static';
import { createTemp } from './create-temp.js';
import { FILE_TYPE_BROWSERSAFE } from './const.js';
import { convertToWebpStream, webpDefault } from './image-processor.js';
import { detectType, isMimeImage } from './file-info.js';
import sharp from 'sharp';
@ -11,7 +12,7 @@ import { defaultDownloadConfig, downloadUrl } from './download.js';
import { getAgents } from './http.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const assets = `${_dirname}/../../server/file/assets/`;
const assets = `${_dirname}/../assets/`;
let config = defaultDownloadConfig;
export function setMediaProxyConfig(setting) {
const proxy = process.env.HTTP_PROXY ?? process.env.http_proxy;
@ -45,13 +46,7 @@ export default function (fastify, options, done) {
const corsHeader = options['Access-Control-Allow-Headers'] ?? '*';
const csp = options['Content-Security-Policy'] ?? `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`;
fastify.addHook('onRequest', (request, reply, done) => {
if (corsOrigin === '*') {
reply.header('Access-Control-Allow-Origin', request.headers.origin ?? '*');
reply.header('Vary', 'Origin');
}
else {
reply.header('Access-Control-Allow-Origin', corsOrigin);
}
reply.header('Access-Control-Allow-Origin', corsOrigin);
reply.header('Access-Control-Allow-Headers', corsHeader);
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
reply.header('Content-Security-Policy', csp);
@ -160,6 +155,9 @@ async function proxyHandler(request, reply) {
else if (file.mime === 'image/svg+xml') {
image = convertToWebpStream(file.path, 2048, 2048);
}
else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
}
if (!image) {
image = {
data: fs.createReadStream(file.path),

View File

@ -1,6 +1,6 @@
{
"name": "misskey-media-proxy",
"version": "0.0.12",
"version": "0.0.13",
"description": "The Media Proxy for Misskey",
"main": "built/index.js",
"packageManager": "pnpm@7.26.0",

View File

@ -17,7 +17,7 @@ import { getAgents } from './http.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const assets = `${_dirname}/../../server/file/assets/`;
const assets = `${_dirname}/../assets/`;
export type MediaProxyOptions = {
['Access-Control-Allow-Origin']?: string;
@ -73,12 +73,7 @@ export default function (fastify: FastifyInstance, options: MediaProxyOptions |
const csp = options!['Content-Security-Policy'] ?? `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`;
fastify.addHook('onRequest', (request, reply, done) => {
if (corsOrigin === '*') {
reply.header('Access-Control-Allow-Origin', request.headers.origin ?? '*');
reply.header('Vary', 'Origin');
} else {
reply.header('Access-Control-Allow-Origin', corsOrigin);
}
reply.header('Access-Control-Allow-Origin', corsOrigin);
reply.header('Access-Control-Allow-Headers', corsHeader);
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
reply.header('Content-Security-Policy', csp);
@ -206,6 +201,8 @@ async function proxyHandler(request: FastifyRequest<{ Params: { url: string; };
};
} else if (file.mime === 'image/svg+xml') {
image = convertToWebpStream(file.path, 2048, 2048);
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
}
if (!image) {