import * as fs from 'fs'; import * as URL from 'url'; import * as tmp from 'tmp'; import * as Koa from 'koa'; import * as request from 'request'; import fileType from 'file-type'; import { serverLogger } from '..'; import config from '../../config'; import { IImage, ConvertToPng, ConvertToJpeg } from '../../services/drive/image-processor'; import checkSvg from '../../misc/check-svg'; export async function proxyMedia(ctx: Koa.BaseContext) { const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; // Create temp file const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { tmp.file((e, path, fd, cleanup) => { if (e) return rej(e); res([path, cleanup]); }); }); try { await fetch(url, path); const [type, ext] = await detectMine(path); let image: IImage; if ('static' in ctx.query && ['image/png', 'image/gif'].includes(type)) { image = await ConvertToPng(path, 498, 280); } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif'].includes(type)) { image = await ConvertToJpeg(path, 200, 200); } else { image = { data: fs.readFileSync(path), ext, type, }; } ctx.set('Content-Type', type); ctx.set('Cache-Control', 'max-age=31536000, immutable'); ctx.body = image.data; } catch (e) { serverLogger.error(e); if (typeof e == 'number' && e >= 400 && e < 500) { ctx.status = e; } else { ctx.status = 500; } } finally { cleanup(); } } async function fetch(url: string, path: string) { await new Promise((res, rej) => { const writable = fs.createWriteStream(path); writable.on('finish', () => { res(); }); writable.on('error', error => { rej(error); }); const requestUrl = URL.parse(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url; const req = request({ url: requestUrl, proxy: config.proxy, timeout: 10 * 1000, headers: { 'User-Agent': config.userAgent } }); req.pipe(writable); req.on('response', response => { if (response.statusCode !== 200) { writable.close(); rej(response.statusCode); } }); req.on('error', error => { writable.close(); rej(error); }); }); } async function detectMine(path: string) { return new Promise<[string, string]>((res, rej) => { const readable = fs.createReadStream(path); readable .on('error', rej) .once('data', (buffer: Buffer) => { readable.destroy(); const type = fileType(buffer); if (type) { if (type.mime == 'application/xml' && checkSvg(path)) { res(['image/svg+xml', 'svg']); } else { res([type.mime, type.ext]); } } else if (checkSvg(path)) { res(['image/svg+xml', 'svg']); } else { // 種類が同定できなかったら application/octet-stream にする res(['application/octet-stream', null]); } }) .on('end', () => { // maybe 0 bytes res(['application/octet-stream', null]); }); }); }