mirror of
https://github.com/misskey-dev/media-proxy.git
synced 2025-08-08 17:23:53 +09:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
a359a64288 | |||
16a1900c3c | |||
6b42019d5f | |||
d87bc5f1d5 |
17
README.md
17
README.md
@ -10,6 +10,8 @@ Misskeyの/proxyが単体で動作します(Misskeyのコードがほぼその
|
|||||||
一応AWS Lambdaで動かす実装を用意しましたが、全くおすすめしません。
|
一応AWS Lambdaで動かす実装を用意しましたが、全くおすすめしません。
|
||||||
https://github.com/tamaina/media-proxy-lambda
|
https://github.com/tamaina/media-proxy-lambda
|
||||||
|
|
||||||
|
Sharp.jsを使っているため、メモリアロケータにjemallocを指定することをお勧めします。
|
||||||
|
|
||||||
## Fastifyプラグインとして動作させる
|
## Fastifyプラグインとして動作させる
|
||||||
### npm install
|
### npm install
|
||||||
|
|
||||||
@ -36,6 +38,13 @@ git clone https://github.com/misskey-dev/media-proxy.git
|
|||||||
cd media-proxy
|
cd media-proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### jemallocをインストール
|
||||||
|
Debian/Ubuntuのaptの場合
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install libjemalloc2
|
||||||
|
```
|
||||||
|
|
||||||
### pnpm install
|
### pnpm install
|
||||||
```
|
```
|
||||||
NODE_ENV=production pnpm install
|
NODE_ENV=production pnpm install
|
||||||
@ -76,14 +85,17 @@ export default {
|
|||||||
適当にサーバーを公開してください。
|
適当にサーバーを公開してください。
|
||||||
(ここではmediaproxy.example.comで公開するものとします。)
|
(ここではmediaproxy.example.comで公開するものとします。)
|
||||||
|
|
||||||
メモ書き程度にsystemdでの開始方法を残しますが、もしかしたらAWS Lambdaとかで動かしたほうが楽かもしれません。
|
メモ書き程度にsystemdでの開始方法を残します。
|
||||||
(サーバーレスだとsharp.jsが動かない可能性が高いため、そこはなんとかしてください)
|
(サーバーレスだとsharp.jsが動かない可能性が高いため、そこはなんとかしてください)
|
||||||
|
|
||||||
systemdサービスのファイルを作成…
|
systemdサービスのファイルを作成…
|
||||||
|
|
||||||
/etc/systemd/system/misskey-proxy.service
|
/etc/systemd/system/misskey-proxy.service
|
||||||
|
|
||||||
エディタで開き、以下のコードを貼り付けて保存(ユーザーやポートは適宜変更すること):
|
エディタで開き、以下のコードを貼り付けて保存
|
||||||
|
|
||||||
|
ユーザーやポートは適宜変更すること。
|
||||||
|
また、arm64の場合`Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"`のx86_64をaarch64に変更する必要がある。jemallocのパスはディストリビューションによって変わる可能性がある。
|
||||||
|
|
||||||
```systemd
|
```systemd
|
||||||
[Unit]
|
[Unit]
|
||||||
@ -94,6 +106,7 @@ Type=simple
|
|||||||
User=misskey
|
User=misskey
|
||||||
ExecStart=/usr/bin/npm start
|
ExecStart=/usr/bin/npm start
|
||||||
WorkingDirectory=/home/misskey/media-proxy
|
WorkingDirectory=/home/misskey/media-proxy
|
||||||
|
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
|
||||||
Environment="NODE_ENV=production"
|
Environment="NODE_ENV=production"
|
||||||
Environment="PORT=3000"
|
Environment="PORT=3000"
|
||||||
TimeoutSec=60
|
TimeoutSec=60
|
||||||
|
4
built/download.d.ts
vendored
4
built/download.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
/// <reference types="node" />
|
/// <reference types="node" resolution-mode="require"/>
|
||||||
/// <reference types="node" />
|
/// <reference types="node" resolution-mode="require"/>
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
export type DownloadConfig = {
|
export type DownloadConfig = {
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as stream from 'node:stream';
|
import * as stream from 'node:stream';
|
||||||
import * as util from 'node:util';
|
import * as util from 'node:util';
|
||||||
|
import ipaddr from 'ipaddr.js';
|
||||||
import got, * as Got from 'got';
|
import got, * as Got from 'got';
|
||||||
import IPCIDR from 'ip-cidr';
|
|
||||||
import PrivateIp from 'private-ip';
|
|
||||||
import { StatusError } from './status-error.js';
|
import { StatusError } from './status-error.js';
|
||||||
import { getAgents } from './http.js';
|
import { getAgents } from './http.js';
|
||||||
import { parse } from 'content-disposition';
|
import { parse } from 'content-disposition';
|
||||||
@ -30,7 +29,7 @@ export async function downloadUrl(url, path, settings = defaultDownloadConfig) {
|
|||||||
lookup: timeout,
|
lookup: timeout,
|
||||||
connect: timeout,
|
connect: timeout,
|
||||||
secureConnect: timeout,
|
secureConnect: timeout,
|
||||||
socket: timeout,
|
socket: timeout, // read timeout
|
||||||
response: timeout,
|
response: timeout,
|
||||||
send: timeout,
|
send: timeout,
|
||||||
request: operationTimeout, // whole operation timeout
|
request: operationTimeout, // whole operation timeout
|
||||||
@ -95,11 +94,11 @@ export async function downloadUrl(url, path, settings = defaultDownloadConfig) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
function isPrivateIp(ip, allowedPrivateNetworks) {
|
function isPrivateIp(ip, allowedPrivateNetworks) {
|
||||||
|
const parsedIp = ipaddr.parse(ip);
|
||||||
for (const net of allowedPrivateNetworks ?? []) {
|
for (const net of allowedPrivateNetworks ?? []) {
|
||||||
const cidr = new IPCIDR(net);
|
if (parsedIp.match(ipaddr.parseCIDR(net))) {
|
||||||
if (cidr.contains(ip)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return PrivateIp(ip) ?? false;
|
return parsedIp.range() !== 'unicast';
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ async function checkSvg(path) {
|
|||||||
const size = await getFileSize(path);
|
const size = await getFileSize(path);
|
||||||
if (size > 1 * 1024 * 1024)
|
if (size > 1 * 1024 * 1024)
|
||||||
return false;
|
return false;
|
||||||
return isSvg(fs.readFileSync(path));
|
return isSvg(fs.readFileSync(path, { encoding: 'utf-8' }));
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
return false;
|
return false;
|
||||||
|
4
built/http.d.ts
vendored
4
built/http.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
/// <reference types="node" />
|
/// <reference types="node" resolution-mode="require"/>
|
||||||
/// <reference types="node" />
|
/// <reference types="node" resolution-mode="require"/>
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
export declare function getAgents(proxy?: string): {
|
export declare function getAgents(proxy?: string): {
|
||||||
|
@ -3,8 +3,8 @@ import * as https from 'node:https';
|
|||||||
import CacheableLookup from 'cacheable-lookup';
|
import CacheableLookup from 'cacheable-lookup';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
const cache = new CacheableLookup({
|
const cache = new CacheableLookup({
|
||||||
maxTtl: 3600,
|
maxTtl: 3600, // 1hours
|
||||||
errorTtl: 30,
|
errorTtl: 30, // 30secs
|
||||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||||
});
|
});
|
||||||
const _http = new http.Agent({
|
const _http = new http.Agent({
|
||||||
|
4
built/image-processor.d.ts
vendored
4
built/image-processor.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
/// <reference types="node" />
|
/// <reference types="node" resolution-mode="require"/>
|
||||||
/// <reference types="node" />
|
/// <reference types="node" resolution-mode="require"/>
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
export type IImage = {
|
export type IImage = {
|
||||||
|
4
built/index.d.ts
vendored
4
built/index.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
/// <reference types="node" />
|
/// <reference types="node" resolution-mode="require"/>
|
||||||
/// <reference types="node" />
|
/// <reference types="node" resolution-mode="require"/>
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
@ -7,7 +7,7 @@ import { FILE_TYPE_BROWSERSAFE } from './const.js';
|
|||||||
import { convertToWebpStream, webpDefault, convertSharpToWebpStream } from './image-processor.js';
|
import { convertToWebpStream, webpDefault, convertSharpToWebpStream } from './image-processor.js';
|
||||||
import { detectType, isMimeImage } from './file-info.js';
|
import { detectType, isMimeImage } from './file-info.js';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { sharpBmp } from 'sharp-read-bmp';
|
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||||
import { StatusError } from './status-error.js';
|
import { StatusError } from './status-error.js';
|
||||||
import { defaultDownloadConfig, downloadUrl } from './download.js';
|
import { defaultDownloadConfig, downloadUrl } from './download.js';
|
||||||
import { getAgents } from './http.js';
|
import { getAgents } from './http.js';
|
||||||
@ -158,7 +158,7 @@ async function proxyHandler(request, reply) {
|
|||||||
else if (file.mime === 'image/svg+xml') {
|
else if (file.mime === 'image/svg+xml') {
|
||||||
image = convertToWebpStream(file.path, 2048, 2048);
|
image = convertToWebpStream(file.path, 2048, 2048);
|
||||||
}
|
}
|
||||||
else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
|
else if (!(file.mime.startsWith('image/') || FILE_TYPE_BROWSERSAFE.includes(file.mime))) {
|
||||||
throw new StatusError('Rejected type', 403, 'Rejected type');
|
throw new StatusError('Rejected type', 403, 'Rejected type');
|
||||||
}
|
}
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
36
package.json
36
package.json
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey-media-proxy",
|
"name": "misskey-media-proxy",
|
||||||
"version": "0.0.22",
|
"version": "0.0.24",
|
||||||
"description": "The Media Proxy for Misskey",
|
"description": "The Media Proxy for Misskey",
|
||||||
"main": "built/index.js",
|
"main": "built/index.js",
|
||||||
"packageManager": "pnpm@7.28.0",
|
"packageManager": "pnpm@8.7.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"built",
|
"built",
|
||||||
@ -26,28 +26,28 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/misskey-dev/media-proxy#readme",
|
"homepage": "https://github.com/misskey-dev/media-proxy#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/cli": "^0.1.62",
|
"@swc/cli": "^0.1.63",
|
||||||
"@swc/core": "^1.3.57",
|
"@swc/core": "^1.3.104",
|
||||||
"@types/content-disposition": "^0.5.5",
|
"@types/content-disposition": "^0.5.8",
|
||||||
"@types/node": "^18.16.7",
|
"@types/node": "^20.11.5",
|
||||||
"@types/sharp": "^0.31.1",
|
"@types/tmp": "^0.2.6",
|
||||||
"@types/tmp": "^0.2.3",
|
"typescript": "^5.3.3"
|
||||||
"typescript": "^4.9.5"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/static": "^6.10.1",
|
"@fastify/static": "^6.12.0",
|
||||||
|
"@misskey-dev/sharp-read-bmp": "^1.1.1",
|
||||||
"cacheable-lookup": "^7.0.0",
|
"cacheable-lookup": "^7.0.0",
|
||||||
"content-disposition": "^0.5.4",
|
"content-disposition": "^0.5.4",
|
||||||
"fastify": "^4.17.0",
|
"fastify": "^4.25.2",
|
||||||
"fastify-cli": "^5.7.1",
|
"fastify-cli": "^6.0.1",
|
||||||
"file-type": "^18.4.0",
|
"file-type": "^19.0.0",
|
||||||
"got": "^12.6.0",
|
"got": "^13.0.0",
|
||||||
"hpagent": "^1.2.0",
|
"hpagent": "^1.2.0",
|
||||||
"ip-cidr": "^3.1.0",
|
"ip-cidr": "^3.1.0",
|
||||||
"is-svg": "^4.4.0",
|
"ipaddr.js": "^2.1.0",
|
||||||
"private-ip": "^3.0.0",
|
"is-svg": "^5.0.0",
|
||||||
"sharp": "^0.31.3",
|
"private-ip": "^3.0.1",
|
||||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
"sharp": "^0.32.6",
|
||||||
"tmp": "^0.2.1"
|
"tmp": "^0.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1150
pnpm-lock.yaml
generated
1150
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,9 +3,8 @@ import * as stream from 'node:stream';
|
|||||||
import * as util from 'node:util';
|
import * as util from 'node:util';
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
|
import ipaddr from 'ipaddr.js';
|
||||||
import got, * as Got from 'got';
|
import got, * as Got from 'got';
|
||||||
import IPCIDR from 'ip-cidr';
|
|
||||||
import PrivateIp from 'private-ip';
|
|
||||||
import { StatusError } from './status-error.js';
|
import { StatusError } from './status-error.js';
|
||||||
import { getAgents } from './http.js';
|
import { getAgents } from './http.js';
|
||||||
import { parse } from 'content-disposition';
|
import { parse } from 'content-disposition';
|
||||||
@ -116,12 +115,13 @@ export async function downloadUrl(url: string, path: string, settings:DownloadCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPrivateIp(ip: string, allowedPrivateNetworks: string[]): boolean {
|
function isPrivateIp(ip: string, allowedPrivateNetworks: string[]): boolean {
|
||||||
|
const parsedIp = ipaddr.parse(ip);
|
||||||
|
|
||||||
for (const net of allowedPrivateNetworks ?? []) {
|
for (const net of allowedPrivateNetworks ?? []) {
|
||||||
const cidr = new IPCIDR(net);
|
if (parsedIp.match(ipaddr.parseCIDR(net))) {
|
||||||
if (cidr.contains(ip)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return PrivateIp(ip) ?? false;
|
return parsedIp.range() !== 'unicast';
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { fileTypeFromFile } from 'file-type';
|
import { fileTypeFromFile } from 'file-type';
|
||||||
import type fileType from 'file-type';
|
import type { MimeType } from 'file-type';
|
||||||
import isSvg from 'is-svg';
|
import isSvg from 'is-svg';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ async function checkSvg(path: string) {
|
|||||||
try {
|
try {
|
||||||
const size = await getFileSize(path);
|
const size = await getFileSize(path);
|
||||||
if (size > 1 * 1024 * 1024) return false;
|
if (size > 1 * 1024 * 1024) return false;
|
||||||
return isSvg(fs.readFileSync(path));
|
return isSvg(fs.readFileSync(path, { encoding: 'utf-8' }));
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -73,7 +73,7 @@ const dictionary = {
|
|||||||
|
|
||||||
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
|
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
|
||||||
|
|
||||||
function fixMime(mime: string | fileType.MimeType): string {
|
function fixMime(mime: string | MimeType): string {
|
||||||
// see https://github.com/misskey-dev/misskey/pull/10686
|
// see https://github.com/misskey-dev/misskey/pull/10686
|
||||||
if (mime === "audio/x-flac") {
|
if (mime === "audio/x-flac") {
|
||||||
return "audio/flac";
|
return "audio/flac";
|
||||||
|
@ -2,6 +2,7 @@ import * as http from 'node:http';
|
|||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
import CacheableLookup from 'cacheable-lookup';
|
import CacheableLookup from 'cacheable-lookup';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
import { LookupFunction } from 'node:net';
|
||||||
|
|
||||||
const cache = new CacheableLookup({
|
const cache = new CacheableLookup({
|
||||||
maxTtl: 3600, // 1hours
|
maxTtl: 3600, // 1hours
|
||||||
@ -12,13 +13,13 @@ const cache = new CacheableLookup({
|
|||||||
const _http = new http.Agent({
|
const _http = new http.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: cache.lookup as unknown as LookupFunction,
|
||||||
} as http.AgentOptions);
|
} as http.AgentOptions);
|
||||||
|
|
||||||
const _https = new https.Agent({
|
const _https = new https.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: cache.lookup as unknown as LookupFunction,
|
||||||
} as https.AgentOptions);
|
} as https.AgentOptions);
|
||||||
|
|
||||||
export function getAgents(proxy?: string) {
|
export function getAgents(proxy?: string) {
|
||||||
|
@ -10,7 +10,7 @@ import { IImageStreamable, convertToWebpStream, webpDefault, convertSharpToWebpS
|
|||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
import { detectType, isMimeImage } from './file-info.js';
|
import { detectType, isMimeImage } from './file-info.js';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { sharpBmp } from 'sharp-read-bmp';
|
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||||
import { StatusError } from './status-error.js';
|
import { StatusError } from './status-error.js';
|
||||||
import { DownloadConfig, defaultDownloadConfig, downloadUrl } from './download.js';
|
import { DownloadConfig, defaultDownloadConfig, downloadUrl } from './download.js';
|
||||||
import { getAgents } from './http.js';
|
import { getAgents } from './http.js';
|
||||||
@ -204,7 +204,7 @@ async function proxyHandler(request: FastifyRequest<{ Params: { url: string; };
|
|||||||
};
|
};
|
||||||
} else if (file.mime === 'image/svg+xml') {
|
} else if (file.mime === 'image/svg+xml') {
|
||||||
image = convertToWebpStream(file.path, 2048, 2048);
|
image = convertToWebpStream(file.path, 2048, 2048);
|
||||||
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
|
} else if (!(file.mime.startsWith('image/') || FILE_TYPE_BROWSERSAFE.includes(file.mime))) {
|
||||||
throw new StatusError('Rejected type', 403, 'Rejected type');
|
throw new StatusError('Rejected type', 403, 'Rejected type');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"target": "es2021",
|
"target": "es2021",
|
||||||
"module": "esnext",
|
"module": "nodenext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "nodenext",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"removeComments": false,
|
"removeComments": false,
|
||||||
"noLib": false,
|
"noLib": false,
|
||||||
@ -36,6 +36,7 @@
|
|||||||
],
|
],
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"./node_modules/@types",
|
"./node_modules/@types",
|
||||||
|
"./node_modules",
|
||||||
"./src/@types"
|
"./src/@types"
|
||||||
],
|
],
|
||||||
"lib": [
|
"lib": [
|
||||||
|
Reference in New Issue
Block a user