From a2b2bf0aafc5b9841739da93fdab4215e8dbded9 Mon Sep 17 00:00:00 2001 From: daniel-j Date: Wed, 12 Aug 2020 23:10:04 +0200 Subject: [PATCH] better filetype validation, support pdf/cbz/cbr/html/txt --- index.js | 59 +++++++++++++++++++++++++++++----------- package-lock.json | 68 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + upload.html | 2 +- 4 files changed, 114 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 6252029..220a569 100644 --- a/index.js +++ b/index.js @@ -9,11 +9,18 @@ const mkdirp = require('mkdirp') const fs = require('fs') const { spawn } = require('child_process') const { extname, basename, dirname } = require('path') +const FileType = require('file-type') const port = 3001 const expireDelay = 30 // 30 seconds const maxExpireDuration = 2 * 60 * 60 // 2 hours -const maxFileSize = 1024 * 1024 * 400 // 400 MB +const maxFileSize = 1024 * 1024 * 500 // 500 MB + +const TYPE_EPUB = 'application/epub+zip' +const TYPE_MOBI = 'application/x-mobipocket-ebook' + +const allowedTypes = [TYPE_EPUB, TYPE_MOBI, 'application/pdf', 'application/vnd.comicbook+zip', 'application/vnd.comicbook-rar', 'text/html', 'text/plain', 'application/zip', 'application/x-rar-compressed'] +const allowedExtensions = ['epub', 'mobi', 'pdf', 'cbz', 'cbr', 'html', 'txt'] const keyChars = "3469ACEGHLMNPRTY" const keyLength = 4 @@ -75,7 +82,6 @@ const upload = multer({ }, filename: function (req, file, cb) { const uniqueSuffix = Date.now() + '-' + Math.floor(Math.random() * 1E9) - console.log(file) cb(null, file.fieldname + '-' + uniqueSuffix + extname(file.originalname).toLowerCase()) } }), @@ -84,15 +90,15 @@ const upload = multer({ files: 1 }, fileFilter: (req, file, cb) => { + console.log('Incoming file:', file) const key = req.body.key.toUpperCase() if (!app.context.keys.has(key)) { console.error('FileFilter: Unknown key: ' + key) cb(null, false) return } - const nameLower = file.originalname.toLowerCase() - if (!nameLower.endsWith('.epub') && !nameLower.endsWith('.mobi')) { - console.error('FileFilter: Filename does not end with .epub or .mobi: ' + file.originalname) + if (!allowedTypes.includes(file.mimetype) || !allowedExtensions.includes(extname(file.originalname.toLowerCase()).substr(1))) { + console.error('FileFilter: File is of an invalid type ', file) cb(null, false) return } @@ -156,33 +162,56 @@ router.get('/download/:key', async ctx => { router.post('/upload', upload.single('file'), async ctx => { const key = ctx.request.body.key.toUpperCase() + if (ctx.request.file) { + console.log('Uploaded file:', ctx.request.file) + } + if (!ctx.keys.has(key)) { flash(ctx, { message: 'Unknown key ' + key, success: false }) ctx.redirect('back', '/') + if (ctx.request.file) { + fs.unlink(ctx.request.file.path, (err) => { + if (err) console.error(err) + else console.log('Removed file', ctx.request.file.path) + }) + } return } + if (!ctx.request.file || ctx.request.file.size === 0) { flash(ctx, { message: 'Invalid file submitted', success: false, key: key }) - if (ctx.request.file) console.error(ctx.request.file) ctx.redirect('back', '/') + if (ctx.request.file) { + fs.unlink(ctx.request.file.path, (err) => { + if (err) console.error(err) + else console.log('Removed file', ctx.request.file.path) + }) + } return } - const nameLower = ctx.request.file.originalname.toLowerCase() - if (!nameLower.endsWith('.epub') && !nameLower.endsWith('.mobi')) { + + const mimetype = ctx.request.file.mimetype + + const type = await FileType.fromFile(ctx.request.file.path) + + if (!type || !allowedTypes.includes(type.mime)) { flash(ctx, { - message: 'Uploaded file does not end with .epub or .mobi ' + ctx.request.file.originalname, + message: 'Uploaded file is of an invalid type: ' + ctx.request.file.originalname + ' (' + (type? type.mime : 'unknown mimetype') + ')', success: false, key: key }) - console.error(ctx.request.file) ctx.redirect('back', '/') + fs.unlink(ctx.request.file.path, (err) => { + if (err) console.error(err) + else console.log('Removed file', ctx.request.file.path) + }) return } @@ -193,7 +222,7 @@ router.post('/upload', upload.single('file'), async ctx => { let filename = ctx.request.file.originalname let conversion = null - if (nameLower.endsWith('.epub') && info.agent.includes('Kindle')) { + if (mimetype === TYPE_EPUB && info.agent.includes('Kindle')) { // convert to .mobi conversion = 'kindlegen' const outname = ctx.request.file.path.replace(/\.epub$/i, '.mobi') @@ -207,7 +236,7 @@ router.post('/upload', upload.single('file'), async ctx => { kindlegen.once('close', (code) => { fs.unlink(ctx.request.file.path, (err) => { if (err) console.error(err) - else console.log('Remove file', ctx.request.file.path) + else console.log('Removed file', ctx.request.file.path) }) fs.unlink(ctx.request.file.path.replace(/\.epub$/i, '.mobi8'), (err) => { if (err) console.error(err) @@ -221,7 +250,7 @@ router.post('/upload', upload.single('file'), async ctx => { }) }) - } else if (nameLower.endsWith('.epub') && info.agent.includes('Kobo') && ctx.request.body.kepubify) { + } else if (mimetype === TYPE_EPUB && info.agent.includes('Kobo') && ctx.request.body.kepubify) { // convert to Kobo EPUB conversion = 'kepubify' const outname = ctx.request.file.path.replace(/\.epub$/i, '.kepub.epub') @@ -235,7 +264,7 @@ router.post('/upload', upload.single('file'), async ctx => { kepubify.once('close', (code) => { fs.unlink(ctx.request.file.path, (err) => { if (err) console.error(err) - else console.log('Remove file', ctx.request.file.path) + else console.log('Removed file', ctx.request.file.path) }) if (code !== 0) { reject('kepubify error code ' + code) @@ -264,7 +293,7 @@ router.post('/upload', upload.single('file'), async ctx => { // size: ctx.request.file.size, uploaded: new Date() } - console.log(info.file) + flash(ctx, { message: 'Upload successful!
'+(conversion ? ' Ebook was converted with ' + conversion + ' and sent' : ' Sent')+' to a '+(info.agent.includes('Kobo') ? 'Kobo' : 'Kindle')+' device.
Filename: ' + filename, success: true, diff --git a/package-lock.json b/package-lock.json index 417b186..b912b63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,16 @@ "path-to-regexp": "^6.1.0" } }, + "@tokenizer/token": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.1.1.tgz", + "integrity": "sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w==" + }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -240,6 +250,17 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "file-type": { + "version": "14.7.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-14.7.1.tgz", + "integrity": "sha512-sXAMgFk67fQLcetXustxfKX+PZgHIUFn96Xld9uH8aXPdX3xOp0/jg9OdouVTvQrf7mrn+wAa4jN/y9fUOOiRA==", + "requires": { + "readable-web-to-node-stream": "^2.0.0", + "strtok3": "^6.0.3", + "token-types": "^2.0.0", + "typedarray-to-buffer": "^3.1.5" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -295,6 +316,11 @@ "resolved": "https://registry.npmjs.org/humanize-number/-/humanize-number-0.0.2.tgz", "integrity": "sha1-EcCvakcWQ2M1iFiASPF5lUFInBg=" }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -305,6 +331,11 @@ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -529,6 +560,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==" }, + "peek-readable": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-3.1.0.tgz", + "integrity": "sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA==" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -545,6 +581,11 @@ "string_decoder": "~0.10.x" } }, + "readable-web-to-node-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz", + "integrity": "sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA==" + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -570,6 +611,16 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, + "strtok3": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.0.4.tgz", + "integrity": "sha512-rqWMKwsbN9APU47bQTMEYTPcwdpKDtmf1jVhHzNW2cL1WqAxaM9iBb9t5P2fj+RV2YsErUWgQzHD5JwV0uCTEQ==", + "requires": { + "@tokenizer/token": "^0.1.1", + "@types/debug": "^4.1.5", + "peek-readable": "^3.1.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -599,6 +650,15 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "token-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-2.0.0.tgz", + "integrity": "sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw==", + "requires": { + "@tokenizer/token": "^0.1.0", + "ieee754": "^1.1.13" + } + }, "tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", @@ -618,6 +678,14 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 09c575d..8331008 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@koa/multer": "^3.0.0", "@koa/router": "^9.3.1", + "file-type": "^14.7.1", "koa": "^2.13.0", "koa-logger": "^3.2.1", "koa-sendfile": "^2.0.1", diff --git a/upload.html b/upload.html index 32468e6..ed11d62 100644 --- a/upload.html +++ b/upload.html @@ -14,7 +14,7 @@
- +