From cfaf017c1507aa2496e406d7a1bdb596398b6f29 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 28 Feb 2023 15:22:00 +0000 Subject: [PATCH] =?UTF-8?q?Content-Disposition=E3=81=A7=E3=83=80=E3=82=A6?= =?UTF-8?q?=E3=83=B3=E3=83=AD=E3=83=BC=E3=83=89=E6=99=82=E3=81=AE=E5=90=8D?= =?UTF-8?q?=E5=89=8D=E3=82=92=E6=8C=87=E5=AE=9A=20Fix=20https://github.com?= =?UTF-8?q?/misskey-dev/media-proxy/issues/6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.cjs | 32 -------------------------------- built/download.d.ts | 4 +++- built/download.js | 14 ++++++++++++++ built/index.js | 21 ++++++++++++++++++++- package.json | 2 ++ pnpm-lock.yaml | 8 ++++++++ src/download.ts | 20 +++++++++++++++++++- src/index.ts | 25 +++++++++++++++++++++++-- 8 files changed, 89 insertions(+), 37 deletions(-) delete mode 100644 .eslintrc.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 5a06889..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: [ - '../shared/.eslintrc.js', - ], - rules: { - 'import/order': ['warn', { - 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - 'pathGroups': [ - { - 'pattern': '@/**', - 'group': 'external', - 'position': 'after' - } - ], - }], - 'no-restricted-globals': [ - 'error', - { - 'name': '__dirname', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - }, - { - 'name': '__filename', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - } - ] - }, -}; diff --git a/built/download.d.ts b/built/download.d.ts index 7bcd2ca..80455fc 100644 --- a/built/download.d.ts +++ b/built/download.d.ts @@ -19,4 +19,6 @@ export declare const defaultDownloadConfig: { maxSize: number; proxy: boolean; }; -export declare function downloadUrl(url: string, path: string, settings?: DownloadConfig): Promise; +export declare function downloadUrl(url: string, path: string, settings?: DownloadConfig): Promise<{ + filename: string; +}>; diff --git a/built/download.js b/built/download.js index 9b32599..4d77dcd 100644 --- a/built/download.js +++ b/built/download.js @@ -6,6 +6,7 @@ import IPCIDR from 'ip-cidr'; import PrivateIp from 'private-ip'; import { StatusError } from './status-error.js'; import { getAgents } from './http.js'; +import { parse } from 'content-disposition'; const pipeline = util.promisify(stream.pipeline); export const defaultDownloadConfig = { userAgent: `MisskeyMediaProxy/0.0.0`, @@ -19,6 +20,8 @@ export async function downloadUrl(url, path, settings = defaultDownloadConfig) { console.log(`Downloading ${url} to ${path} ...`); const timeout = 30 * 1000; const operationTimeout = 60 * 1000; + const urlObj = new URL(url); + let filename = urlObj.pathname.split('/').pop() ?? 'unknown'; const req = got.stream(url, { headers: { 'User-Agent': settings.userAgent, @@ -56,6 +59,13 @@ export async function downloadUrl(url, path, settings = defaultDownloadConfig) { req.destroy(); } } + const contentDisposition = res.headers['content-disposition']; + if (contentDisposition != null) { + const parsed = parse(contentDisposition); + if (parsed.parameters.filename) { + filename = parsed.parameters.filename; + } + } }).on('downloadProgress', (progress) => { if (progress.transferred > settings.maxSize) { console.log(`maxSize exceeded (${progress.transferred} > ${settings.maxSize}) on downloadProgress`); @@ -75,6 +85,10 @@ export async function downloadUrl(url, path, settings = defaultDownloadConfig) { } if (process.env.NODE_ENV !== 'production') console.log(`Download finished: ${url}`); + + return { + filename, + }; } function isPrivateIp(ip, allowedPrivateNetworks) { for (const net of allowedPrivateNetworks ?? []) { diff --git a/built/index.js b/built/index.js index ac17872..531263f 100644 --- a/built/index.js +++ b/built/index.js @@ -10,6 +10,7 @@ import sharp from 'sharp'; import { StatusError } from './status-error.js'; import { defaultDownloadConfig, downloadUrl } from './download.js'; import { getAgents } from './http.js'; +import _contentDisposition from 'content-disposition'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); const assets = `${_dirname}/../assets/`; @@ -182,6 +183,7 @@ async function proxyHandler(request, reply) { } reply.header('Content-Type', image.type); reply.header('Cache-Control', 'max-age=31536000, immutable'); + reply.header('Content-Disposition', contentDisposition('inline', file.filename)); return reply.send(image.data); } catch (e) { @@ -193,12 +195,13 @@ async function proxyHandler(request, reply) { async function downloadAndDetectTypeFromUrl(url) { const [path, cleanup] = await createTemp(); try { - await downloadUrl(url, path, config); + const { filename } = await downloadUrl(url, path, config); const { mime, ext } = await detectType(path); return { state: 'remote', mime, ext, path, cleanup, + filename: correctFilename(filename, ext), }; } catch (e) { @@ -206,3 +209,19 @@ async function downloadAndDetectTypeFromUrl(url) { throw e; } } +function correctFilename(filename, ext) { + if (!ext) + return filename; + const dotExt = `.${ext}`; + if (filename.endsWith(dotExt)) { + return filename; + } + if (ext === 'jpg' && filename.endsWith('.jpeg')) { + return filename; + } + return `${filename}${dotExt}`; +} +function contentDisposition(type, filename) { + const fallback = filename.replace(/[^\w.-]/g, '_'); + return _contentDisposition(filename, { type, fallback }); +} diff --git a/package.json b/package.json index 01dad90..9fd55af 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@swc/cli": "^0.1.61", "@swc/core": "^1.3.32", + "@types/content-disposition": "^0.5.5", "@types/node": "^18.11.19", "@types/sharp": "^0.31.1", "@types/tmp": "^0.2.3", @@ -36,6 +37,7 @@ "dependencies": { "@fastify/static": "^6.8.0", "cacheable-lookup": "^7.0.0", + "content-disposition": "^0.5.4", "fastify": "^4.12.0", "fastify-cli": "^5.7.1", "file-type": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f38951..700c683 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,10 +4,12 @@ specifiers: '@fastify/static': ^6.8.0 '@swc/cli': ^0.1.61 '@swc/core': ^1.3.32 + '@types/content-disposition': ^0.5.5 '@types/node': ^18.11.19 '@types/sharp': ^0.31.1 '@types/tmp': ^0.2.3 cacheable-lookup: ^7.0.0 + content-disposition: ^0.5.4 fastify: ^4.12.0 fastify-cli: ^5.7.1 file-type: ^18.2.0 @@ -23,6 +25,7 @@ specifiers: dependencies: '@fastify/static': 6.8.0 cacheable-lookup: 7.0.0 + content-disposition: 0.5.4 fastify: 4.12.0 fastify-cli: 5.7.1 file-type: 18.2.0 @@ -37,6 +40,7 @@ dependencies: devDependencies: '@swc/cli': 0.1.61_@swc+core@1.3.32 '@swc/core': 1.3.32 + '@types/content-disposition': 0.5.5 '@types/node': 18.11.19 '@types/sharp': 0.31.1 '@types/tmp': 0.2.3 @@ -300,6 +304,10 @@ packages: '@types/responselike': 1.0.0 dev: true + /@types/content-disposition/0.5.5: + resolution: {integrity: sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==} + dev: true + /@types/http-cache-semantics/4.0.1: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} diff --git a/src/download.ts b/src/download.ts index 2920b85..c5ce518 100644 --- a/src/download.ts +++ b/src/download.ts @@ -8,6 +8,7 @@ import IPCIDR from 'ip-cidr'; import PrivateIp from 'private-ip'; import { StatusError } from './status-error.js'; import { getAgents } from './http.js'; +import { parse } from 'content-disposition'; const pipeline = util.promisify(stream.pipeline); @@ -29,12 +30,17 @@ export const defaultDownloadConfig = { ...getAgents() } -export async function downloadUrl(url: string, path: string, settings:DownloadConfig = defaultDownloadConfig): Promise { +export async function downloadUrl(url: string, path: string, settings:DownloadConfig = defaultDownloadConfig): Promise<{ + filename: string; +}> { if (process.env.NODE_ENV !== 'production') console.log(`Downloading ${url} to ${path} ...`); const timeout = 30 * 1000; const operationTimeout = 60 * 1000; + const urlObj = new URL(url); + let filename = urlObj.pathname.split('/').pop() ?? 'unknown'; + const req = got.stream(url, { headers: { 'User-Agent': settings.userAgent, @@ -73,6 +79,14 @@ export async function downloadUrl(url: string, path: string, settings:DownloadCo req.destroy(); } } + + const contentDisposition = res.headers['content-disposition']; + if (contentDisposition != null) { + const parsed = parse(contentDisposition); + if (parsed.parameters.filename) { + filename = parsed.parameters.filename; + } + } }).on('downloadProgress', (progress: Got.Progress) => { if (progress.transferred > settings.maxSize) { console.log(`maxSize exceeded (${progress.transferred} > ${settings.maxSize}) on downloadProgress`); @@ -91,6 +105,10 @@ export async function downloadUrl(url: string, path: string, settings:DownloadCo } if (process.env.NODE_ENV !== 'production') console.log(`Download finished: ${url}`); + + return { + filename, + } } function isPrivateIp(ip: string, allowedPrivateNetworks: string[]): boolean { diff --git a/src/index.ts b/src/index.ts index a2d3c9a..0956515 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import sharp from 'sharp'; import { StatusError } from './status-error.js'; import { DownloadConfig, defaultDownloadConfig, downloadUrl } from './download.js'; import { getAgents } from './http.js'; +import _contentDisposition from 'content-disposition'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -230,6 +231,7 @@ async function proxyHandler(request: FastifyRequest<{ Params: { url: string; }; reply.header('Content-Type', image.type); reply.header('Cache-Control', 'max-age=31536000, immutable'); + reply.header('Content-Disposition', contentDisposition('inline', file.filename)); return reply.send(image.data); } catch (e) { if ('cleanup' in file) file.cleanup(); @@ -238,11 +240,11 @@ async function proxyHandler(request: FastifyRequest<{ Params: { url: string; }; } async function downloadAndDetectTypeFromUrl(url: string): Promise< - { state: 'remote'; mime: string; ext: string | null; path: string; cleanup: () => void; } + { state: 'remote'; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } > { const [path, cleanup] = await createTemp(); try { - await downloadUrl(url, path, config); + const { filename } = await downloadUrl(url, path, config); const { mime, ext } = await detectType(path); @@ -250,9 +252,28 @@ async function downloadAndDetectTypeFromUrl(url: string): Promise< state: 'remote', mime, ext, path, cleanup, + filename: correctFilename(filename, ext), } } catch (e) { cleanup(); throw e; } } + +function correctFilename(filename: string, ext: string | null) { + if (!ext) return filename; + + const dotExt = `.${ext}`; + if (filename.endsWith(dotExt)) { + return filename; + } + if (ext === 'jpg' && filename.endsWith('.jpeg')) { + return filename; + } + return `${filename}${dotExt}`; +} + +function contentDisposition(type: 'inline' | 'attachment', filename: string): string { + const fallback = filename.replace(/[^\w.-]/g, '_'); + return _contentDisposition(filename, { type, fallback }); +}