mirror of
https://github.com/misskey-dev/media-proxy.git
synced 2025-04-29 19:07:23 +09:00
パッケージ公開に最適化
This commit is contained in:
parent
c6b14befe9
commit
85a7f47539
23
README.md
23
README.md
@ -5,7 +5,25 @@ Misskeyの/proxyが単体で動作します(Misskeyのコードがほぼその
|
|||||||
**Fastifyプラグインとして動作する気がします。**
|
**Fastifyプラグインとして動作する気がします。**
|
||||||
`pnpm start`は[fastify-cli](https://github.com/fastify/fastify-cli)が動作します。
|
`pnpm start`は[fastify-cli](https://github.com/fastify/fastify-cli)が動作します。
|
||||||
|
|
||||||
## セットアップ方法
|
## Fastifyプラグインとして動作させる
|
||||||
|
### npm install
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install git+https://github.com/misskey-dev/media-proxy.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fastifyプラグインを書く
|
||||||
|
```
|
||||||
|
import MediaProxy from 'misskey-media-proxy';
|
||||||
|
|
||||||
|
// ......
|
||||||
|
|
||||||
|
fastify.register(MediaProxy);
|
||||||
|
```
|
||||||
|
|
||||||
|
オプションを指定できます。オプションの内容はindex.tsのMediaProxyOptionsに指定してあります。
|
||||||
|
|
||||||
|
## サーバーのセットアップ方法
|
||||||
まずはgit cloneしてcdしてください。
|
まずはgit cloneしてcdしてください。
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -25,7 +43,7 @@ NODE_ENV=production pnpm install
|
|||||||
```js
|
```js
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
const repo = JSON.stringify(readFileSync('./package.json', 'utf8'));
|
const repo = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// UA
|
// UA
|
||||||
@ -91,3 +109,4 @@ mediaProxyの指定をdefault.ymlに追記し、Misskeyを再起動してくだ
|
|||||||
```yml
|
```yml
|
||||||
mediaProxy: https://mediaproxy.example.com
|
mediaProxy: https://mediaproxy.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
1
built/const.d.ts
vendored
Normal file
1
built/const.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export declare const FILE_TYPE_BROWSERSAFE: string[];
|
@ -1 +1,40 @@
|
|||||||
export const FILE_TYPE_BROWSERSAFE=["image/png","image/gif","image/jpeg","image/webp","image/avif","image/apng","image/bmp","image/tiff","image/x-icon","audio/opus","video/ogg","audio/ogg","application/ogg","video/quicktime","video/mp4","audio/mp4","video/x-m4v","audio/x-m4a","video/3gpp","video/3gpp2","video/mpeg","audio/mpeg","video/webm","audio/webm","audio/aac","audio/x-flac","audio/vnd.wave"];
|
// ブラウザで直接表示することを許可するファイルの種類のリスト
|
||||||
|
// ここに含まれないものは application/octet-stream としてレスポンスされる
|
||||||
|
// SVGはXSSを生むので許可しない
|
||||||
|
export const FILE_TYPE_BROWSERSAFE = [
|
||||||
|
// Images
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/webp',
|
||||||
|
'image/avif',
|
||||||
|
'image/apng',
|
||||||
|
'image/bmp',
|
||||||
|
'image/tiff',
|
||||||
|
'image/x-icon',
|
||||||
|
// OggS
|
||||||
|
'audio/opus',
|
||||||
|
'video/ogg',
|
||||||
|
'audio/ogg',
|
||||||
|
'application/ogg',
|
||||||
|
// ISO/IEC base media file format
|
||||||
|
'video/quicktime',
|
||||||
|
'video/mp4',
|
||||||
|
'audio/mp4',
|
||||||
|
'video/x-m4v',
|
||||||
|
'audio/x-m4a',
|
||||||
|
'video/3gpp',
|
||||||
|
'video/3gpp2',
|
||||||
|
'video/mpeg',
|
||||||
|
'audio/mpeg',
|
||||||
|
'video/webm',
|
||||||
|
'audio/webm',
|
||||||
|
'audio/aac',
|
||||||
|
'audio/x-flac',
|
||||||
|
'audio/vnd.wave',
|
||||||
|
];
|
||||||
|
/*
|
||||||
|
https://github.com/sindresorhus/file-type/blob/main/supported.js
|
||||||
|
https://github.com/sindresorhus/file-type/blob/main/core.js
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
||||||
|
*/
|
||||||
|
2
built/create-temp.d.ts
vendored
Normal file
2
built/create-temp.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export declare function createTemp(): Promise<[string, () => void]>;
|
||||||
|
export declare function createTempDir(): Promise<[string, () => void]>;
|
@ -1 +1,21 @@
|
|||||||
import*as tmp from"tmp";export function createTemp(){return new Promise((res,rej)=>{tmp.file((e,path,fd,cleanup)=>{if(e)return rej(e);res([path,process.env.NODE_ENV==="production"?cleanup:()=>{}])})})}export function createTempDir(){return new Promise((res,rej)=>{tmp.dir({unsafeCleanup:true},(e,path,cleanup)=>{if(e)return rej(e);res([path,process.env.NODE_ENV==="production"?cleanup:()=>{}])})})}
|
import * as tmp from 'tmp';
|
||||||
|
export function createTemp() {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
tmp.file((e, path, fd, cleanup) => {
|
||||||
|
if (e)
|
||||||
|
return rej(e);
|
||||||
|
res([path, process.env.NODE_ENV === 'production' ? cleanup : () => { }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function createTempDir() {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
tmp.dir({
|
||||||
|
unsafeCleanup: true,
|
||||||
|
}, (e, path, cleanup) => {
|
||||||
|
if (e)
|
||||||
|
return rej(e);
|
||||||
|
res([path, process.env.NODE_ENV === 'production' ? cleanup : () => { }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
22
built/download.d.ts
vendored
Normal file
22
built/download.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
/// <reference types="node" />
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import * as https from 'node:https';
|
||||||
|
export type DownloadConfig = {
|
||||||
|
[x: string]: any;
|
||||||
|
userAgent: string;
|
||||||
|
allowedPrivateNetworks: string[];
|
||||||
|
maxSize: number;
|
||||||
|
httpAgent: http.Agent;
|
||||||
|
httpsAgent: https.Agent;
|
||||||
|
proxy?: boolean;
|
||||||
|
};
|
||||||
|
export declare const defaultDownloadConfig: {
|
||||||
|
httpAgent: http.Agent;
|
||||||
|
httpsAgent: https.Agent;
|
||||||
|
userAgent: string;
|
||||||
|
allowedPrivateNetworks: never[];
|
||||||
|
maxSize: number;
|
||||||
|
proxy: boolean;
|
||||||
|
};
|
||||||
|
export declare function downloadUrl(url: string, path: string, settings?: DownloadConfig): Promise<void>;
|
@ -1 +1,87 @@
|
|||||||
import*as fs from"node:fs";import*as stream from"node:stream";import*as util from"node:util";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";const pipeline=util.promisify(stream.pipeline);export async function downloadUrl(url,path){if(process.env.NODE_ENV!=="production")console.log(`Downloading ${url} to ${path} ...`);const timeout=30*1e3;const operationTimeout=60*1e3;const req=got.stream(url,{headers:{"User-Agent":config.userAgent},timeout:{lookup:timeout,connect:timeout,secureConnect:timeout,socket:timeout,response:timeout,send:timeout,request:operationTimeout},agent:{http:httpAgent,https:httpsAgent},http2:true,retry:{limit:0},enableUnixSockets:false}).on("response",res=>{if((process.env.NODE_ENV==="production"||process.env.NODE_ENV==="test")&&!config.proxy&&res.ip){if(isPrivateIp(res.ip)){console.log(`Blocked address: ${res.ip}`);req.destroy()}}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`);req.destroy()}}}).on("downloadProgress",progress=>{if(progress.transferred>config.maxSize){console.log(`maxSize exceeded (${progress.transferred} > ${config.maxSize}) on downloadProgress`);req.destroy()}});try{await pipeline(req,fs.createWriteStream(path))}catch(e){if(e instanceof Got.HTTPError){throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`,e.response.statusCode,e.response.statusMessage)}else{throw e}}if(process.env.NODE_ENV!=="production")console.log(`Download finished: ${url}`)}function isPrivateIp(ip){for(const net of config.allowedPrivateNetworks??[]){const cidr=new IPCIDR(net);if(cidr.contains(ip)){return false}}return PrivateIp(ip)??false}
|
import * as fs from 'node:fs';
|
||||||
|
import * as stream from 'node:stream';
|
||||||
|
import * as util from 'node:util';
|
||||||
|
import got, * as Got from 'got';
|
||||||
|
import IPCIDR from 'ip-cidr';
|
||||||
|
import PrivateIp from 'private-ip';
|
||||||
|
import { StatusError } from './status-error.js';
|
||||||
|
import { getAgents } from './http.js';
|
||||||
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
export const defaultDownloadConfig = {
|
||||||
|
userAgent: `MisskeyMediaProxy/0.0.0`,
|
||||||
|
allowedPrivateNetworks: [],
|
||||||
|
maxSize: 262144000,
|
||||||
|
proxy: false,
|
||||||
|
...getAgents()
|
||||||
|
};
|
||||||
|
export async function downloadUrl(url, path, settings = defaultDownloadConfig) {
|
||||||
|
if (process.env.NODE_ENV !== 'production')
|
||||||
|
console.log(`Downloading ${url} to ${path} ...`);
|
||||||
|
const timeout = 30 * 1000;
|
||||||
|
const operationTimeout = 60 * 1000;
|
||||||
|
const req = got.stream(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': settings.userAgent,
|
||||||
|
},
|
||||||
|
timeout: {
|
||||||
|
lookup: timeout,
|
||||||
|
connect: timeout,
|
||||||
|
secureConnect: timeout,
|
||||||
|
socket: timeout,
|
||||||
|
response: timeout,
|
||||||
|
send: timeout,
|
||||||
|
request: operationTimeout, // whole operation timeout
|
||||||
|
},
|
||||||
|
agent: {
|
||||||
|
http: settings.httpAgent,
|
||||||
|
https: settings.httpsAgent,
|
||||||
|
},
|
||||||
|
http2: true,
|
||||||
|
retry: {
|
||||||
|
limit: 0,
|
||||||
|
},
|
||||||
|
enableUnixSockets: false,
|
||||||
|
}).on('response', (res) => {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const contentLength = res.headers['content-length'];
|
||||||
|
if (contentLength != null) {
|
||||||
|
const size = Number(contentLength);
|
||||||
|
if (size > settings.maxSize) {
|
||||||
|
console.log(`maxSize exceeded (${size} > ${settings.maxSize}) on response`);
|
||||||
|
req.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).on('downloadProgress', (progress) => {
|
||||||
|
if (progress.transferred > settings.maxSize) {
|
||||||
|
console.log(`maxSize exceeded (${progress.transferred} > ${settings.maxSize}) on downloadProgress`);
|
||||||
|
req.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await pipeline(req, fs.createWriteStream(path));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e instanceof Got.HTTPError) {
|
||||||
|
throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV !== 'production')
|
||||||
|
console.log(`Download finished: ${url}`);
|
||||||
|
}
|
||||||
|
function isPrivateIp(ip, allowedPrivateNetworks) {
|
||||||
|
for (const net of allowedPrivateNetworks ?? []) {
|
||||||
|
const cidr = new IPCIDR(net);
|
||||||
|
if (cidr.contains(ip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PrivateIp(ip) ?? false;
|
||||||
|
}
|
||||||
|
11
built/file-info.d.ts
vendored
Normal file
11
built/file-info.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export declare function detectType(path: string): Promise<{
|
||||||
|
mime: string;
|
||||||
|
ext: string | null;
|
||||||
|
}>;
|
||||||
|
declare const dictionary: {
|
||||||
|
'safe-file': string[];
|
||||||
|
'sharp-convertible-image': string[];
|
||||||
|
'sharp-animation-convertible-image': string[];
|
||||||
|
};
|
||||||
|
export declare const isMimeImage: (mime: string, type: keyof typeof dictionary) => boolean;
|
||||||
|
export {};
|
@ -1 +1,58 @@
|
|||||||
import fs from"node:fs";import{fileTypeFromFile}from"file-type";import isSvg from"is-svg";import{promisify}from"node:util";const TYPE_OCTET_STREAM={mime:"application/octet-stream",ext:null};const TYPE_SVG={mime:"image/svg+xml",ext:"svg"};async function getFileSize(path){const getStat=promisify(fs.stat);return(await getStat(path)).size}export async function detectType(path){const fileSize=await getFileSize(path);if(fileSize===0){return TYPE_OCTET_STREAM}const type=await fileTypeFromFile(path);if(type){if(type.mime==="application/xml"&&await checkSvg(path)){return TYPE_SVG}return{mime:type.mime,ext:type.ext}}if(await checkSvg(path)){return TYPE_SVG}return TYPE_OCTET_STREAM}async function checkSvg(path){try{const size=await getFileSize(path);if(size>1*1024*1024)return false;return isSvg(fs.readFileSync(path))}catch{return false}}import{FILE_TYPE_BROWSERSAFE}from"./const.js";const dictionary={"safe-file":FILE_TYPE_BROWSERSAFE,"sharp-convertible-image":["image/jpeg","image/png","image/gif","image/apng","image/vnd.mozilla.apng","image/webp","image/avif","image/svg+xml"],"sharp-animation-convertible-image":["image/jpeg","image/png","image/gif","image/webp","image/avif","image/svg+xml"]};export const isMimeImage=(mime,type)=>dictionary[type].includes(mime);
|
import fs from 'node:fs';
|
||||||
|
import { fileTypeFromFile } from 'file-type';
|
||||||
|
import isSvg from 'is-svg';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
const TYPE_OCTET_STREAM = {
|
||||||
|
mime: 'application/octet-stream',
|
||||||
|
ext: null,
|
||||||
|
};
|
||||||
|
const TYPE_SVG = {
|
||||||
|
mime: 'image/svg+xml',
|
||||||
|
ext: 'svg',
|
||||||
|
};
|
||||||
|
async function getFileSize(path) {
|
||||||
|
const getStat = promisify(fs.stat);
|
||||||
|
return (await getStat(path)).size;
|
||||||
|
}
|
||||||
|
export async function detectType(path) {
|
||||||
|
// Check 0 byte
|
||||||
|
const fileSize = await getFileSize(path);
|
||||||
|
if (fileSize === 0) {
|
||||||
|
return TYPE_OCTET_STREAM;
|
||||||
|
}
|
||||||
|
const type = await fileTypeFromFile(path);
|
||||||
|
if (type) {
|
||||||
|
// XMLはSVGかもしれない
|
||||||
|
if (type.mime === 'application/xml' && await checkSvg(path)) {
|
||||||
|
return TYPE_SVG;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mime: type.mime,
|
||||||
|
ext: type.ext,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 種類が不明でもSVGかもしれない
|
||||||
|
if (await checkSvg(path)) {
|
||||||
|
return TYPE_SVG;
|
||||||
|
}
|
||||||
|
// それでも種類が不明なら application/octet-stream にする
|
||||||
|
return TYPE_OCTET_STREAM;
|
||||||
|
}
|
||||||
|
async function checkSvg(path) {
|
||||||
|
try {
|
||||||
|
const size = await getFileSize(path);
|
||||||
|
if (size > 1 * 1024 * 1024)
|
||||||
|
return false;
|
||||||
|
return isSvg(fs.readFileSync(path));
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
import { FILE_TYPE_BROWSERSAFE } from './const.js';
|
||||||
|
const dictionary = {
|
||||||
|
'safe-file': FILE_TYPE_BROWSERSAFE,
|
||||||
|
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
|
||||||
|
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
|
||||||
|
};
|
||||||
|
export const isMimeImage = (mime, type) => dictionary[type].includes(mime);
|
||||||
|
8
built/http.d.ts
vendored
Normal file
8
built/http.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
/// <reference types="node" />
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import * as https from 'node:https';
|
||||||
|
export declare function getAgents(proxy?: string): {
|
||||||
|
httpAgent: http.Agent;
|
||||||
|
httpsAgent: https.Agent;
|
||||||
|
};
|
@ -1 +1,45 @@
|
|||||||
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,errorTtl:30,lookup:false});const _http=new http.Agent({keepAlive:true,keepAliveMsecs:30*1e3,lookup:cache.lookup});const _https=new https.Agent({keepAlive:true,keepAliveMsecs:30*1e3,lookup:cache.lookup});export const httpAgent=config.proxy?new HttpProxyAgent({keepAlive:true,keepAliveMsecs:30*1e3,maxSockets:256,maxFreeSockets:256,scheduling:"lifo",proxy:config.proxy}):_http;export const httpsAgent=config.proxy?new HttpsProxyAgent({keepAlive:true,keepAliveMsecs:30*1e3,maxSockets:256,maxFreeSockets:256,scheduling:"lifo",proxy:config.proxy}):_https;
|
import * as http from 'node:http';
|
||||||
|
import * as https from 'node:https';
|
||||||
|
import CacheableLookup from 'cacheable-lookup';
|
||||||
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
const cache = new CacheableLookup({
|
||||||
|
maxTtl: 3600,
|
||||||
|
errorTtl: 30,
|
||||||
|
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||||
|
});
|
||||||
|
const _http = new http.Agent({
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveMsecs: 30 * 1000,
|
||||||
|
lookup: cache.lookup,
|
||||||
|
});
|
||||||
|
const _https = new https.Agent({
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveMsecs: 30 * 1000,
|
||||||
|
lookup: cache.lookup,
|
||||||
|
});
|
||||||
|
export function getAgents(proxy) {
|
||||||
|
const httpAgent = proxy
|
||||||
|
? new HttpProxyAgent({
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveMsecs: 30 * 1000,
|
||||||
|
maxSockets: 256,
|
||||||
|
maxFreeSockets: 256,
|
||||||
|
scheduling: 'lifo',
|
||||||
|
proxy: proxy,
|
||||||
|
})
|
||||||
|
: _http;
|
||||||
|
const httpsAgent = proxy
|
||||||
|
? new HttpsProxyAgent({
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveMsecs: 30 * 1000,
|
||||||
|
maxSockets: 256,
|
||||||
|
maxFreeSockets: 256,
|
||||||
|
scheduling: 'lifo',
|
||||||
|
proxy: proxy,
|
||||||
|
})
|
||||||
|
: _https;
|
||||||
|
return {
|
||||||
|
httpAgent,
|
||||||
|
httpsAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
18
built/image-processor.d.ts
vendored
Normal file
18
built/image-processor.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
/// <reference types="node" />
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
|
export type IImage = {
|
||||||
|
data: Buffer;
|
||||||
|
ext: string | null;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
export type IImageStream = {
|
||||||
|
data: Readable;
|
||||||
|
ext: string | null;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
export type IImageStreamable = IImage | IImageStream;
|
||||||
|
export declare const webpDefault: sharp.WebpOptions;
|
||||||
|
export declare function convertToWebpStream(path: string, width: number, height: number, options?: sharp.WebpOptions): IImageStream;
|
||||||
|
export declare function convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options?: sharp.WebpOptions): IImageStream;
|
@ -1 +1,26 @@
|
|||||||
import sharp from"sharp";export const webpDefault={quality:85,alphaQuality:95,lossless:false,nearLossless:false,smartSubsample:true,mixed:true};export function convertToWebpStream(path,width,height,options=webpDefault){return convertSharpToWebpStream(sharp(path),width,height,options)}export function convertSharpToWebpStream(sharp,width,height,options=webpDefault){const data=sharp.resize(width,height,{fit:"inside",withoutEnlargement:true}).rotate().webp(options);return{data,ext:"webp",type:"image/webp"}}
|
import sharp from 'sharp';
|
||||||
|
export const webpDefault = {
|
||||||
|
quality: 85,
|
||||||
|
alphaQuality: 95,
|
||||||
|
lossless: false,
|
||||||
|
nearLossless: false,
|
||||||
|
smartSubsample: true,
|
||||||
|
mixed: true,
|
||||||
|
};
|
||||||
|
export function convertToWebpStream(path, width, height, options = webpDefault) {
|
||||||
|
return convertSharpToWebpStream(sharp(path), width, height, options);
|
||||||
|
}
|
||||||
|
export function convertSharpToWebpStream(sharp, width, height, options = webpDefault) {
|
||||||
|
const data = sharp
|
||||||
|
.resize(width, height, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true,
|
||||||
|
})
|
||||||
|
.rotate()
|
||||||
|
.webp(options);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
ext: 'webp',
|
||||||
|
type: 'image/webp',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
17
built/index.d.ts
vendored
Normal file
17
built/index.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
/// <reference types="node" />
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import * as https from 'node:https';
|
||||||
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
export type MediaProxyOptions = {
|
||||||
|
userAgent?: string;
|
||||||
|
allowedPrivateNetworks?: string[];
|
||||||
|
maxSize?: number;
|
||||||
|
} & ({
|
||||||
|
proxy?: string;
|
||||||
|
} | {
|
||||||
|
httpAgent: http.Agent;
|
||||||
|
httpsAgent: https.Agent;
|
||||||
|
});
|
||||||
|
export declare function setMediaProxyConfig(setting?: MediaProxyOptions | null): void;
|
||||||
|
export default function (fastify: FastifyInstance, options: MediaProxyOptions | null | undefined, done: (err?: Error) => void): void;
|
193
built/index.js
193
built/index.js
@ -1 +1,192 @@
|
|||||||
import*as fs from"node:fs";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";import{StatusError}from"./status-error.js";import{downloadUrl}from"./download.js";const _filename=fileURLToPath(import.meta.url);const _dirname=dirname(_filename);const assets=`${_dirname}/../../server/file/assets/`;export default function(fastify,options,done){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()});fastify.register(fastifyStatic,{root:_dirname,serve:false});fastify.get("/:url*",async(request,reply)=>{return await proxyHandler(request,reply).catch(err=>errorHandler(request,reply,err))});done()}function errorHandler(request,reply,err){console.log(`${err}`);reply.header("Cache-Control","max-age=300");if(request.query&&"fallback"in request.query){return reply.sendFile("/dummy.png",assets)}if(err instanceof StatusError&&(err.statusCode===302||err.isClientError)){reply.code(err.statusCode);return}reply.code(500);return}async function proxyHandler(request,reply){const url="url"in request.query?request.query.url:"https://"+request.params.url;if(typeof url!=="string"){reply.code(400);return}const file=await downloadAndDetectTypeFromUrl(url);try{const isConvertibleImage=isMimeImage(file.mime,"sharp-convertible-image");const isAnimationConvertibleImage=isMimeImage(file.mime,"sharp-animation-convertible-image");let image=null;if(("emoji"in request.query||"avatar"in request.query)&&isConvertibleImage){if(!isAnimationConvertibleImage&&!("static"in request.query)){image={data:fs.createReadStream(file.path),ext:file.ext,type:file.mime}}else{const data=sharp(file.path,{animated:!("static"in request.query)}).resize({height:"emoji"in request.query?128:320,withoutEnlargement:true}).webp(webpDefault);image={data,ext:"webp",type:"image/webp"}}}else if("static"in request.query&&isConvertibleImage){image=convertToWebpStream(file.path,498,280)}else if("preview"in request.query&&isConvertibleImage){image=convertToWebpStream(file.path,200,200)}else if("badge"in request.query){if(!isConvertibleImage){throw new StatusError("Unexpected mime",404)}const mask=sharp(file.path).resize(96,96,{fit:"inside",withoutEnlargement:false}).greyscale().normalise().linear(1.75,-128*1.75+128).flatten({background:"#000"}).toColorspace("b-w");const stats=await mask.clone().stats();if(stats.entropy<.1){throw new StatusError("Skip to provide badge",404)}const data=sharp({create:{width:96,height:96,channels:4,background:{r:0,g:0,b:0,alpha:0}}}).pipelineColorspace("b-w").boolean(await mask.png().toBuffer(),"eor");image={data:await data.png().toBuffer(),ext:"png",type:"image/png"}}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),ext:file.ext,type:file.mime}}if("cleanup"in file){if("pipe"in image.data&&typeof image.data.pipe==="function"){image.data.on("end",file.cleanup);image.data.on("close",file.cleanup)}else{file.cleanup()}}reply.header("Content-Type",image.type);reply.header("Cache-Control","max-age=31536000, immutable");return image.data}catch(e){if("cleanup"in file)file.cleanup();throw e}}async function downloadAndDetectTypeFromUrl(url){const[path,cleanup]=await createTemp();try{await downloadUrl(url,path);const{mime,ext}=await detectType(path);return{state:"remote",mime,ext,path,cleanup}}catch(e){cleanup();throw e}}
|
import * as fs from 'node:fs';
|
||||||
|
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';
|
||||||
|
import { StatusError } from './status-error.js';
|
||||||
|
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/`;
|
||||||
|
let config = defaultDownloadConfig;
|
||||||
|
export function setMediaProxyConfig(setting) {
|
||||||
|
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, options, done) {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
fastify.register(fastifyStatic, {
|
||||||
|
root: _dirname,
|
||||||
|
serve: false,
|
||||||
|
});
|
||||||
|
fastify.get('/:url*', async (request, reply) => {
|
||||||
|
return await proxyHandler(request, reply)
|
||||||
|
.catch(err => errorHandler(request, reply, err));
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
function errorHandler(request, reply, err) {
|
||||||
|
console.log(`${err}`);
|
||||||
|
reply.header('Cache-Control', 'max-age=300');
|
||||||
|
if (request.query && 'fallback' in request.query) {
|
||||||
|
return reply.sendFile('/dummy.png', assets);
|
||||||
|
}
|
||||||
|
if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
|
||||||
|
reply.code(err.statusCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reply.code(500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
async function proxyHandler(request, reply) {
|
||||||
|
const url = 'url' in request.query ? request.query.url : (request.params.url && 'https://' + request.params.url);
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
reply.code(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create temp file
|
||||||
|
const file = await downloadAndDetectTypeFromUrl(url);
|
||||||
|
try {
|
||||||
|
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image');
|
||||||
|
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
|
||||||
|
let image = null;
|
||||||
|
if (('emoji' in request.query || 'avatar' in request.query) && isConvertibleImage) {
|
||||||
|
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||||
|
image = {
|
||||||
|
data: fs.createReadStream(file.path),
|
||||||
|
ext: file.ext,
|
||||||
|
type: file.mime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const data = sharp(file.path, { animated: !('static' in request.query) })
|
||||||
|
.resize({
|
||||||
|
height: 'emoji' in request.query ? 128 : 320,
|
||||||
|
withoutEnlargement: true,
|
||||||
|
})
|
||||||
|
.webp(webpDefault);
|
||||||
|
image = {
|
||||||
|
data,
|
||||||
|
ext: 'webp',
|
||||||
|
type: 'image/webp',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ('static' in request.query && isConvertibleImage) {
|
||||||
|
image = convertToWebpStream(file.path, 498, 280);
|
||||||
|
}
|
||||||
|
else if ('preview' in request.query && isConvertibleImage) {
|
||||||
|
image = convertToWebpStream(file.path, 200, 200);
|
||||||
|
}
|
||||||
|
else if ('badge' in request.query) {
|
||||||
|
if (!isConvertibleImage) {
|
||||||
|
// 画像でないなら404でお茶を濁す
|
||||||
|
throw new StatusError('Unexpected mime', 404);
|
||||||
|
}
|
||||||
|
const mask = sharp(file.path)
|
||||||
|
.resize(96, 96, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: false,
|
||||||
|
})
|
||||||
|
.greyscale()
|
||||||
|
.normalise()
|
||||||
|
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
|
||||||
|
.flatten({ background: '#000' })
|
||||||
|
.toColorspace('b-w');
|
||||||
|
const stats = await mask.clone().stats();
|
||||||
|
if (stats.entropy < 0.1) {
|
||||||
|
// エントロピーがあまりない場合は404にする
|
||||||
|
throw new StatusError('Skip to provide badge', 404);
|
||||||
|
}
|
||||||
|
const data = sharp({
|
||||||
|
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
||||||
|
})
|
||||||
|
.pipelineColorspace('b-w')
|
||||||
|
.boolean(await mask.png().toBuffer(), 'eor');
|
||||||
|
image = {
|
||||||
|
data: await data.png().toBuffer(),
|
||||||
|
ext: 'png',
|
||||||
|
type: 'image/png',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
ext: file.ext,
|
||||||
|
type: file.mime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if ('cleanup' in file) {
|
||||||
|
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
|
||||||
|
// image.dataがstreamなら、stream終了後にcleanup
|
||||||
|
image.data.on('end', file.cleanup);
|
||||||
|
image.data.on('close', file.cleanup);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// image.dataがstreamでないなら直ちにcleanup
|
||||||
|
file.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reply.header('Content-Type', image.type);
|
||||||
|
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||||
|
return image.data;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if ('cleanup' in file)
|
||||||
|
file.cleanup();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function downloadAndDetectTypeFromUrl(url) {
|
||||||
|
const [path, cleanup] = await createTemp();
|
||||||
|
try {
|
||||||
|
await downloadUrl(url, path, config);
|
||||||
|
const { mime, ext } = await detectType(path);
|
||||||
|
return {
|
||||||
|
state: 'remote',
|
||||||
|
mime, ext,
|
||||||
|
path, cleanup,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
cleanup();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
6
built/status-error.d.ts
vendored
Normal file
6
built/status-error.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export declare class StatusError extends Error {
|
||||||
|
statusCode: number;
|
||||||
|
statusMessage?: string;
|
||||||
|
isClientError: boolean;
|
||||||
|
constructor(message: string, statusCode: number, statusMessage?: string);
|
||||||
|
}
|
@ -1 +1,9 @@
|
|||||||
export class StatusError extends Error{constructor(message,statusCode,statusMessage){super(message);this.name="StatusError";this.statusCode=statusCode;this.statusMessage=statusMessage;this.isClientError=typeof this.statusCode==="number"&&this.statusCode>=400&&this.statusCode<500}}
|
export class StatusError extends Error {
|
||||||
|
constructor(message, statusCode, statusMessage) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'StatusError';
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.statusMessage = statusMessage;
|
||||||
|
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
"packageManager": "pnpm@7.26.0",
|
"packageManager": "pnpm@7.26.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "swc src -d built -D",
|
"build": "tsc -p tsconfig.json",
|
||||||
"watch": "swc src -d built -D -w & fastify start -w -l info -P ./built/index.js",
|
"watch": "swc src -d built -D -w & fastify start -w -l info -P ./built/index.js",
|
||||||
"start": "fastify start ./built/index.js"
|
"start": "fastify start ./server.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -23,8 +23,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/cli": "^0.1.61",
|
"@swc/cli": "^0.1.61",
|
||||||
"@swc/core": "^1.3.32",
|
"@swc/core": "^1.3.32",
|
||||||
|
"@types/node": "^18.11.19",
|
||||||
"@types/sharp": "^0.31.1",
|
"@types/sharp": "^0.31.1",
|
||||||
"@types/tmp": "^0.2.3"
|
"@types/tmp": "^0.2.3",
|
||||||
|
"typescript": "^4.9.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/static": "^6.8.0",
|
"@fastify/static": "^6.8.0",
|
||||||
|
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@ -4,6 +4,7 @@ specifiers:
|
|||||||
'@fastify/static': ^6.8.0
|
'@fastify/static': ^6.8.0
|
||||||
'@swc/cli': ^0.1.61
|
'@swc/cli': ^0.1.61
|
||||||
'@swc/core': ^1.3.32
|
'@swc/core': ^1.3.32
|
||||||
|
'@types/node': ^18.11.19
|
||||||
'@types/sharp': ^0.31.1
|
'@types/sharp': ^0.31.1
|
||||||
'@types/tmp': ^0.2.3
|
'@types/tmp': ^0.2.3
|
||||||
cacheable-lookup: ^7.0.0
|
cacheable-lookup: ^7.0.0
|
||||||
@ -17,6 +18,7 @@ specifiers:
|
|||||||
private-ip: ^3.0.0
|
private-ip: ^3.0.0
|
||||||
sharp: ^0.31.3
|
sharp: ^0.31.3
|
||||||
tmp: ^0.2.1
|
tmp: ^0.2.1
|
||||||
|
typescript: ^4.9.5
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fastify/static': 6.8.0
|
'@fastify/static': 6.8.0
|
||||||
@ -35,8 +37,10 @@ dependencies:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@swc/cli': 0.1.61_@swc+core@1.3.32
|
'@swc/cli': 0.1.61_@swc+core@1.3.32
|
||||||
'@swc/core': 1.3.32
|
'@swc/core': 1.3.32
|
||||||
|
'@types/node': 18.11.19
|
||||||
'@types/sharp': 0.31.1
|
'@types/sharp': 0.31.1
|
||||||
'@types/tmp': 0.2.3
|
'@types/tmp': 0.2.3
|
||||||
|
typescript: 4.9.5
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -47,6 +51,7 @@ packages:
|
|||||||
/@fastify/accept-negotiator/1.1.0:
|
/@fastify/accept-negotiator/1.1.0:
|
||||||
resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==}
|
resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@fastify/ajv-compiler/3.5.0:
|
/@fastify/ajv-compiler/3.5.0:
|
||||||
resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==}
|
resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==}
|
||||||
@ -78,6 +83,7 @@ packages:
|
|||||||
fast-decode-uri-component: 1.0.1
|
fast-decode-uri-component: 1.0.1
|
||||||
http-errors: 2.0.0
|
http-errors: 2.0.0
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@fastify/static/6.8.0:
|
/@fastify/static/6.8.0:
|
||||||
resolution: {integrity: sha512-MNQp7KM0NIC+722OPN3MholnfvM+Vg2ao4OwbWWNJhAJEWOKGe4fJsEjIh3OkN0z5ymhklc7EXGCG0zDaIU5ZQ==}
|
resolution: {integrity: sha512-MNQp7KM0NIC+722OPN3MholnfvM+Vg2ao4OwbWWNJhAJEWOKGe4fJsEjIh3OkN0z5ymhklc7EXGCG0zDaIU5ZQ==}
|
||||||
@ -94,6 +100,7 @@ packages:
|
|||||||
/@lukeed/ms/2.0.1:
|
/@lukeed/ms/2.0.1:
|
||||||
resolution: {integrity: sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==}
|
resolution: {integrity: sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@mole-inc/bin-wrapper/8.0.1:
|
/@mole-inc/bin-wrapper/8.0.1:
|
||||||
resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==}
|
resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==}
|
||||||
@ -289,7 +296,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-cache-semantics': 4.0.1
|
'@types/http-cache-semantics': 4.0.1
|
||||||
'@types/keyv': 3.1.4
|
'@types/keyv': 3.1.4
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.19
|
||||||
'@types/responselike': 1.0.0
|
'@types/responselike': 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@ -299,23 +306,23 @@ packages:
|
|||||||
/@types/keyv/3.1.4:
|
/@types/keyv/3.1.4:
|
||||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.19
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/node/18.11.18:
|
/@types/node/18.11.19:
|
||||||
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
|
resolution: {integrity: sha512-YUgMWAQBWLObABqrvx8qKO1enAvBUdjZOAWQ5grBAkp5LQv45jBvYKZ3oFS9iKRCQyFjqw6iuEa1vmFqtxYLZw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/responselike/1.0.0:
|
/@types/responselike/1.0.0:
|
||||||
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
|
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.19
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/sharp/0.31.1:
|
/@types/sharp/0.31.1:
|
||||||
resolution: {integrity: sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==}
|
resolution: {integrity: sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.19
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/tmp/0.2.3:
|
/@types/tmp/0.2.3:
|
||||||
@ -646,6 +653,7 @@ packages:
|
|||||||
/depd/2.0.0:
|
/depd/2.0.0:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/detect-libc/2.0.1:
|
/detect-libc/2.0.1:
|
||||||
resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
|
resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
|
||||||
@ -664,6 +672,7 @@ packages:
|
|||||||
|
|
||||||
/escape-html/1.0.3:
|
/escape-html/1.0.3:
|
||||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/escape-string-regexp/5.0.0:
|
/escape-string-regexp/5.0.0:
|
||||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||||
@ -1063,6 +1072,7 @@ packages:
|
|||||||
setprototypeof: 1.2.0
|
setprototypeof: 1.2.0
|
||||||
statuses: 2.0.1
|
statuses: 2.0.1
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/http2-wrapper/1.0.3:
|
/http2-wrapper/1.0.3:
|
||||||
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
|
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
|
||||||
@ -1291,6 +1301,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mimic-fn/2.1.0:
|
/mimic-fn/2.1.0:
|
||||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||||
@ -1435,6 +1446,7 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dependencies:
|
dependencies:
|
||||||
yocto-queue: 0.1.0
|
yocto-queue: 0.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/p-locate/3.0.0:
|
/p-locate/3.0.0:
|
||||||
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
||||||
@ -1755,6 +1767,7 @@ packages:
|
|||||||
|
|
||||||
/setprototypeof/1.2.0:
|
/setprototypeof/1.2.0:
|
||||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sharp/0.31.3:
|
/sharp/0.31.3:
|
||||||
resolution: {integrity: sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==}
|
resolution: {integrity: sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==}
|
||||||
@ -1865,6 +1878,7 @@ packages:
|
|||||||
/statuses/2.0.1:
|
/statuses/2.0.1:
|
||||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/string_decoder/1.3.0:
|
/string_decoder/1.3.0:
|
||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
@ -1965,6 +1979,7 @@ packages:
|
|||||||
/toidentifier/1.0.1:
|
/toidentifier/1.0.1:
|
||||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/token-types/5.0.1:
|
/token-types/5.0.1:
|
||||||
resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==}
|
resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==}
|
||||||
@ -1986,6 +2001,12 @@ packages:
|
|||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/typescript/4.9.5:
|
||||||
|
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
|
||||||
|
engines: {node: '>=4.2.0'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/uri-js/4.4.1:
|
/uri-js/4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2034,3 +2055,4 @@ packages:
|
|||||||
/yocto-queue/0.1.0:
|
/yocto-queue/0.1.0:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
dev: false
|
||||||
|
6
server.js
Normal file
6
server.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import config from './config.js';
|
||||||
|
import app from './built/index.js';
|
||||||
|
|
||||||
|
export default function (fastify, opts, next) {
|
||||||
|
return app(fastify, { ...config, ...opts }, next);
|
||||||
|
}
|
@ -1,16 +1,35 @@
|
|||||||
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 * as http from 'node:http';
|
||||||
|
import * as https from 'node:https';
|
||||||
import got, * as Got from 'got';
|
import got, * as Got from 'got';
|
||||||
import IPCIDR from 'ip-cidr';
|
import IPCIDR from 'ip-cidr';
|
||||||
import PrivateIp from 'private-ip';
|
import PrivateIp from 'private-ip';
|
||||||
import { StatusError } from './status-error.js';
|
import { StatusError } from './status-error.js';
|
||||||
import config from '../config.js';
|
import { getAgents } from './http.js';
|
||||||
import { httpAgent, httpsAgent } from './http.js';
|
|
||||||
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
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} ...`);
|
if (process.env.NODE_ENV !== 'production') console.log(`Downloading ${url} to ${path} ...`);
|
||||||
|
|
||||||
const timeout = 30 * 1000;
|
const timeout = 30 * 1000;
|
||||||
@ -18,7 +37,7 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||||||
|
|
||||||
const req = got.stream(url, {
|
const req = got.stream(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': config.userAgent,
|
'User-Agent': settings.userAgent,
|
||||||
},
|
},
|
||||||
timeout: {
|
timeout: {
|
||||||
lookup: timeout,
|
lookup: timeout,
|
||||||
@ -30,8 +49,8 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||||||
request: operationTimeout, // whole operation timeout
|
request: operationTimeout, // whole operation timeout
|
||||||
},
|
},
|
||||||
agent: {
|
agent: {
|
||||||
http: httpAgent,
|
http: settings.httpAgent,
|
||||||
https: httpsAgent,
|
https: settings.httpsAgent,
|
||||||
},
|
},
|
||||||
http2: true,
|
http2: true,
|
||||||
retry: {
|
retry: {
|
||||||
@ -39,8 +58,8 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||||||
},
|
},
|
||||||
enableUnixSockets: false,
|
enableUnixSockets: false,
|
||||||
}).on('response', (res: Got.Response) => {
|
}).on('response', (res: Got.Response) => {
|
||||||
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) {
|
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !settings.proxy && res.ip) {
|
||||||
if (isPrivateIp(res.ip)) {
|
if (isPrivateIp(res.ip, settings.allowedPrivateNetworks)) {
|
||||||
console.log(`Blocked address: ${res.ip}`);
|
console.log(`Blocked address: ${res.ip}`);
|
||||||
req.destroy();
|
req.destroy();
|
||||||
}
|
}
|
||||||
@ -49,14 +68,14 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||||||
const contentLength = res.headers['content-length'];
|
const contentLength = res.headers['content-length'];
|
||||||
if (contentLength != null) {
|
if (contentLength != null) {
|
||||||
const size = Number(contentLength);
|
const size = Number(contentLength);
|
||||||
if (size > config.maxSize) {
|
if (size > settings.maxSize) {
|
||||||
console.log(`maxSize exceeded (${size} > ${config.maxSize}) on response`);
|
console.log(`maxSize exceeded (${size} > ${settings.maxSize}) on response`);
|
||||||
req.destroy();
|
req.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).on('downloadProgress', (progress: Got.Progress) => {
|
}).on('downloadProgress', (progress: Got.Progress) => {
|
||||||
if (progress.transferred > config.maxSize) {
|
if (progress.transferred > settings.maxSize) {
|
||||||
console.log(`maxSize exceeded (${progress.transferred} > ${config.maxSize}) on downloadProgress`);
|
console.log(`maxSize exceeded (${progress.transferred} > ${settings.maxSize}) on downloadProgress`);
|
||||||
req.destroy();
|
req.destroy();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -75,8 +94,8 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function isPrivateIp(ip: string): boolean {
|
function isPrivateIp(ip: string, allowedPrivateNetworks: string[]): boolean {
|
||||||
for (const net of config.allowedPrivateNetworks ?? []) {
|
for (const net of allowedPrivateNetworks ?? []) {
|
||||||
const cidr = new IPCIDR(net);
|
const cidr = new IPCIDR(net);
|
||||||
if (cidr.contains(ip)) {
|
if (cidr.contains(ip)) {
|
||||||
return false;
|
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 * 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 config from "../config.js";
|
|
||||||
|
|
||||||
const cache = new CacheableLookup({
|
const cache = new CacheableLookup({
|
||||||
maxTtl: 3600, // 1hours
|
maxTtl: 3600, // 1hours
|
||||||
@ -22,24 +21,31 @@ const _https = new https.Agent({
|
|||||||
lookup: cache.lookup,
|
lookup: cache.lookup,
|
||||||
} as https.AgentOptions);
|
} as https.AgentOptions);
|
||||||
|
|
||||||
export const httpAgent = config.proxy
|
export function getAgents(proxy?: string) {
|
||||||
? new HttpProxyAgent({
|
const httpAgent = proxy
|
||||||
keepAlive: true,
|
? new HttpProxyAgent({
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAlive: true,
|
||||||
maxSockets: 256,
|
keepAliveMsecs: 30 * 1000,
|
||||||
maxFreeSockets: 256,
|
maxSockets: 256,
|
||||||
scheduling: 'lifo',
|
maxFreeSockets: 256,
|
||||||
proxy: config.proxy,
|
scheduling: 'lifo',
|
||||||
})
|
proxy: proxy,
|
||||||
: _http;
|
})
|
||||||
|
: _http;
|
||||||
|
|
||||||
export const httpsAgent = config.proxy
|
const httpsAgent = proxy
|
||||||
? new HttpsProxyAgent({
|
? new HttpsProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
maxSockets: 256,
|
maxSockets: 256,
|
||||||
maxFreeSockets: 256,
|
maxFreeSockets: 256,
|
||||||
scheduling: 'lifo',
|
scheduling: 'lifo',
|
||||||
proxy: config.proxy,
|
proxy: proxy,
|
||||||
})
|
})
|
||||||
: _https;
|
: _https;
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpAgent,
|
||||||
|
httpsAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
58
src/index.ts
58
src/index.ts
@ -1,4 +1,6 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import * as https from 'node:https';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import fastifyStatic from '@fastify/static';
|
import fastifyStatic from '@fastify/static';
|
||||||
@ -9,14 +11,60 @@ import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOption
|
|||||||
import { detectType, isMimeImage } from './file-info.js';
|
import { detectType, isMimeImage } from './file-info.js';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { StatusError } from './status-error.js';
|
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 _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
|
|
||||||
const assets = `${_dirname}/../../server/file/assets/`;
|
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) => {
|
fastify.addHook('onRequest', (request, reply, done) => {
|
||||||
reply.header('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`);
|
reply.header('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`);
|
||||||
done();
|
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) {
|
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);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -171,7 +219,7 @@ async function downloadAndDetectTypeFromUrl(url: string): Promise<
|
|||||||
> {
|
> {
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
try {
|
try {
|
||||||
await downloadUrl(url, path);
|
await downloadUrl(url, path, config);
|
||||||
|
|
||||||
const { mime, ext } = await detectType(path);
|
const { mime, ext } = await detectType(path);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"declaration": false,
|
"declaration": true,
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"target": "es2021",
|
"target": "es2021",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user