mirror of
https://github.com/misskey-dev/media-proxy.git
synced 2025-08-03 23:06:36 +09:00
パッケージ公開に最適化
This commit is contained in:
@ -1,16 +1,35 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as stream from 'node:stream';
|
||||
import * as util from 'node:util';
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import got, * as Got from 'got';
|
||||
import IPCIDR from 'ip-cidr';
|
||||
import PrivateIp from 'private-ip';
|
||||
import { StatusError } from './status-error.js';
|
||||
import config from '../config.js';
|
||||
import { httpAgent, httpsAgent } from './http.js';
|
||||
import { getAgents } from './http.js';
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
export type DownloadConfig = {
|
||||
[x: string]: any;
|
||||
userAgent: string;
|
||||
allowedPrivateNetworks: string[];
|
||||
maxSize: number;
|
||||
httpAgent: http.Agent,
|
||||
httpsAgent: https.Agent,
|
||||
proxy?: boolean;
|
||||
}
|
||||
|
||||
export const defaultDownloadConfig = {
|
||||
userAgent: `MisskeyMediaProxy/0.0.0`,
|
||||
allowedPrivateNetworks: [],
|
||||
maxSize: 262144000,
|
||||
proxy: false,
|
||||
...getAgents()
|
||||
}
|
||||
|
||||
export async function downloadUrl(url: string, path: string, settings:DownloadConfig = defaultDownloadConfig): Promise<void> {
|
||||
if (process.env.NODE_ENV !== 'production') console.log(`Downloading ${url} to ${path} ...`);
|
||||
|
||||
const timeout = 30 * 1000;
|
||||
@ -18,7 +37,7 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
|
||||
const req = got.stream(url, {
|
||||
headers: {
|
||||
'User-Agent': config.userAgent,
|
||||
'User-Agent': settings.userAgent,
|
||||
},
|
||||
timeout: {
|
||||
lookup: timeout,
|
||||
@ -30,8 +49,8 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
request: operationTimeout, // whole operation timeout
|
||||
},
|
||||
agent: {
|
||||
http: httpAgent,
|
||||
https: httpsAgent,
|
||||
http: settings.httpAgent,
|
||||
https: settings.httpsAgent,
|
||||
},
|
||||
http2: true,
|
||||
retry: {
|
||||
@ -39,8 +58,8 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
},
|
||||
enableUnixSockets: false,
|
||||
}).on('response', (res: Got.Response) => {
|
||||
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) {
|
||||
if (isPrivateIp(res.ip)) {
|
||||
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !settings.proxy && res.ip) {
|
||||
if (isPrivateIp(res.ip, settings.allowedPrivateNetworks)) {
|
||||
console.log(`Blocked address: ${res.ip}`);
|
||||
req.destroy();
|
||||
}
|
||||
@ -49,14 +68,14 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
const contentLength = res.headers['content-length'];
|
||||
if (contentLength != null) {
|
||||
const size = Number(contentLength);
|
||||
if (size > config.maxSize) {
|
||||
console.log(`maxSize exceeded (${size} > ${config.maxSize}) on response`);
|
||||
if (size > settings.maxSize) {
|
||||
console.log(`maxSize exceeded (${size} > ${settings.maxSize}) on response`);
|
||||
req.destroy();
|
||||
}
|
||||
}
|
||||
}).on('downloadProgress', (progress: Got.Progress) => {
|
||||
if (progress.transferred > config.maxSize) {
|
||||
console.log(`maxSize exceeded (${progress.transferred} > ${config.maxSize}) on downloadProgress`);
|
||||
if (progress.transferred > settings.maxSize) {
|
||||
console.log(`maxSize exceeded (${progress.transferred} > ${settings.maxSize}) on downloadProgress`);
|
||||
req.destroy();
|
||||
}
|
||||
});
|
||||
@ -75,8 +94,8 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
}
|
||||
|
||||
|
||||
function isPrivateIp(ip: string): boolean {
|
||||
for (const net of config.allowedPrivateNetworks ?? []) {
|
||||
function isPrivateIp(ip: string, allowedPrivateNetworks: string[]): boolean {
|
||||
for (const net of allowedPrivateNetworks ?? []) {
|
||||
const cidr = new IPCIDR(net);
|
||||
if (cidr.contains(ip)) {
|
||||
return false;
|
||||
|
48
src/http.ts
48
src/http.ts
@ -2,7 +2,6 @@ import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import CacheableLookup from 'cacheable-lookup';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
import config from "../config.js";
|
||||
|
||||
const cache = new CacheableLookup({
|
||||
maxTtl: 3600, // 1hours
|
||||
@ -22,24 +21,31 @@ const _https = new https.Agent({
|
||||
lookup: cache.lookup,
|
||||
} as https.AgentOptions);
|
||||
|
||||
export const httpAgent = config.proxy
|
||||
? new HttpProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
maxSockets: 256,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: config.proxy,
|
||||
})
|
||||
: _http;
|
||||
export function getAgents(proxy?: string) {
|
||||
const httpAgent = proxy
|
||||
? new HttpProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
maxSockets: 256,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: proxy,
|
||||
})
|
||||
: _http;
|
||||
|
||||
export const httpsAgent = config.proxy
|
||||
? new HttpsProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
maxSockets: 256,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: config.proxy,
|
||||
})
|
||||
: _https;
|
||||
const httpsAgent = proxy
|
||||
? new HttpsProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
maxSockets: 256,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: proxy,
|
||||
})
|
||||
: _https;
|
||||
|
||||
return {
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
};
|
||||
}
|
||||
|
58
src/index.ts
58
src/index.ts
@ -1,4 +1,6 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
@ -9,14 +11,60 @@ import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOption
|
||||
import { detectType, isMimeImage } from './file-info.js';
|
||||
import sharp from 'sharp';
|
||||
import { StatusError } from './status-error.js';
|
||||
import { downloadUrl } from './download.js';
|
||||
import { DownloadConfig, 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/`;
|
||||
|
||||
export default function (fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
export type MediaProxyOptions = {
|
||||
userAgent?: string;
|
||||
allowedPrivateNetworks?: string[];
|
||||
maxSize?: number;
|
||||
} & ({
|
||||
proxy?: string;
|
||||
} | {
|
||||
httpAgent: http.Agent;
|
||||
httpsAgent: https.Agent;
|
||||
});
|
||||
|
||||
let config: DownloadConfig = defaultDownloadConfig;
|
||||
|
||||
export function setMediaProxyConfig(setting?: MediaProxyOptions | null) {
|
||||
const proxy = process.env.HTTP_PROXY ?? process.env.http_proxy;
|
||||
|
||||
if (!setting) {
|
||||
config = {
|
||||
...defaultDownloadConfig,
|
||||
...(proxy ? getAgents(proxy) : {}),
|
||||
proxy: !!proxy,
|
||||
};
|
||||
console.log(config);
|
||||
return;
|
||||
}
|
||||
|
||||
config = {
|
||||
userAgent: setting.userAgent ?? defaultDownloadConfig.userAgent,
|
||||
allowedPrivateNetworks: setting.allowedPrivateNetworks ?? defaultDownloadConfig.allowedPrivateNetworks,
|
||||
maxSize: setting.maxSize ?? defaultDownloadConfig.maxSize,
|
||||
...('proxy' in setting ?
|
||||
{ ...getAgents(setting.proxy), proxy: !!setting.proxy } :
|
||||
'httpAgent' in setting ? {
|
||||
httpAgent: setting.httpAgent,
|
||||
httpsAgent: setting.httpsAgent,
|
||||
proxy: true,
|
||||
} :
|
||||
{ ...getAgents(proxy), proxy: !!proxy }),
|
||||
};
|
||||
|
||||
console.log(config);
|
||||
}
|
||||
|
||||
export default function (fastify: FastifyInstance, options: MediaProxyOptions | null | undefined, done: (err?: Error) => void) {
|
||||
setMediaProxyConfig(options);
|
||||
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`);
|
||||
done();
|
||||
@ -57,9 +105,9 @@ function errorHandler(request: FastifyRequest<{ Params?: { [x: string]: any }; Q
|
||||
}
|
||||
|
||||
async function proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
|
||||
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
|
||||
const url = 'url' in request.query ? request.query.url : (request.params.url && 'https://' + request.params.url);
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
if (!url || typeof url !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
@ -171,7 +219,7 @@ async function downloadAndDetectTypeFromUrl(url: string): Promise<
|
||||
> {
|
||||
const [path, cleanup] = await createTemp();
|
||||
try {
|
||||
await downloadUrl(url, path);
|
||||
await downloadUrl(url, path, config);
|
||||
|
||||
const { mime, ext } = await detectType(path);
|
||||
|
||||
|
Reference in New Issue
Block a user