diff --git a/lib/endpoints.js b/lib/endpoints.js index b4b03d4..6d52c6b 100644 --- a/lib/endpoints.js +++ b/lib/endpoints.js @@ -27,7 +27,7 @@ const app = express(); app.disable('x-powered-by'); app.use(compression()); -if(config.accessLog) { +if (config.accessLog) { app.use(morgan(config.accessLog)); } @@ -41,13 +41,12 @@ app.get('/robots.txt', (req, res) => { }); // Upload App -// -// app.get('/', (req, res) => { - if (config.uploadAppPath != '/') - res.status(304).redirect(config.uploadAppPath); - else - res.sendFile(path.join(__dirname, '../public/html/upload.html')); + if (config.uploadAppPath !== '/') { + res.status(304).redirect(config.uploadAppPath); + } else { + res.sendFile(path.join(__dirname, '../public/html/upload.html')); + } }); app.get(config.uploadAppPath, (req, res) => { @@ -65,19 +64,29 @@ app.get('/config.json', (req, res) => { app.get('/admin', (req, res, next) => { - if(!config.adminPass) return next(); + if (!config.adminPass) return next(); res.sendFile(path.join(__dirname, '../public/html/admin.html')); }); app.get('/admin/data.json', (req, res, next) => { - if(!config.adminPass) return next(); - if(!req.get('x-passwd')) return res.status(401).send('Unauthorized'); - if(req.get('x-passwd') !== config.adminPass) return res.status(403).send('Forbidden'); + if (!config.adminPass) return next(); + + const bfTimeout = 500; + + if (!req.get('x-passwd')) { + // delay answer to make brute force attacks more difficult + setTimeout(() => res.status(401).send('Unauthorized'), bfTimeout); + return; + } + if (req.get('x-passwd') !== config.adminPass) { + setTimeout(() => res.status(403).send('Forbidden'), bfTimeout); + return; + } const result = _.chain(db.db) .cloneDeep() .forEach(bucket => { bucket.forEach(file => { - if(file.metadata.password) { + if (file.metadata.password) { file.metadata._password = true; delete file.metadata.password; delete file.metadata.key; @@ -88,24 +97,21 @@ app.get('/admin/data.json', (req, res, next) => { }) .value(); - // make bruteforce attack more difficult - setTimeout(() => { - res.json(result); - },250); + setTimeout(() => res.json(result), bfTimeout); }); // List files / Download App app.get('/:sid', (req, res, next) => { - if(req.url.endsWith('.json')) { - const sid = req.params.sid.substr(0, req.params.sid.length-5); - if(!db.get(sid)) return res.status(404).end(); + if (req.url.endsWith('.json')) { + const sid = req.params.sid.substr(0, req.params.sid.length - 5); + if (!db.get(sid)) return res.status(404).end(); res.header('Cache-control', 'private, max-age=0, no-cache, no-store, must-revalidate'); res.json({ items: db.get(sid).map(data => { - const item = Object.assign(data, {url: `/files/${sid}++${data.key}`}); - if(item.metadata.password) { + const item = Object.assign(data, { url: `/files/${ sid }++${ data.key }` }); + if (item.metadata.password) { return AES.encrypt(JSON.stringify(data), item.metadata.password).toString(); } else { return item; @@ -116,34 +122,33 @@ app.get('/:sid', (req, res, next) => { } }); } else { - if(!db.get(req.params.sid)) return next(); + if (!db.get(req.params.sid)) return next(); res.sendFile(path.join(__dirname, '../public/html/download.html')); } }); // Download files -app.get('/files/:fid', async(req, res, next) => { +app.get('/files/:fid', async (req, res, next) => { // let tusboy handle HEAD requests with Tus Header - if(req.method === 'HEAD' && req.get('Tus-Resumable')) return next(); + if (req.method === 'HEAD' && req.get('Tus-Resumable')) return next(); // Download all files - if(req.params.fid.match(/^[a-z0-9+]+\.(tar\.gz|zip)$/)) { - const sid = req.params.fid.split('++')[0]; + if (req.params.fid.match(/^[a-z0-9+]+\.(tar\.gz|zip)$/)) { const format = req.params.fid.endsWith('.zip') ? 'zip' : 'tar.gz'; const bucket = db.get(sid); - if(!bucket) return res.status(404).send(errorPage.replace('%%ERROR%%', 'Download bucket not found.')); + if (!bucket) return res.status(404).send(errorPage.replace('%%ERROR%%', 'Download bucket not found.')); - if(req.params.fid !== sid + '++' + MD5(bucket.map(f => f.key).join()).toString() + '.' + format) { + if (req.params.fid !== sid + '++' + MD5(bucket.map(f => f.key).join()).toString() + '.' + format) { res.status(404).send(errorPage.replace('%%ERROR%%', 'Invalid link')); return; } - debug(`Download Bucket ${sid}`); + debug(`Download Bucket ${ sid }`); - if(format === 'zip') res.header('ContentType', 'application/zip'); - if(format === 'tar.gz') res.header('ContentType', 'application/x-gtar'); - res.header('Content-Disposition', `attachment; filename="${sid}.${format}"`); + if (format === 'zip') res.header('ContentType', 'application/zip'); + if (format === 'tar.gz') res.header('ContentType', 'application/x-gtar'); + res.header('Content-Disposition', `attachment; filename="${ sid }.${ format }"`); const archive = archiver(format === 'zip' ? 'zip' : 'tar'); archive.on('error', function(err) { @@ -153,11 +158,11 @@ app.get('/files/:fid', async(req, res, next) => { bucket.forEach(info => { archive.append( fs.createReadStream(store.getFilename(info.metadata.sid + '++' + info.key)), - {name: info.metadata.name} + { name: info.metadata.name } ); }); - if(format === 'tar.gz') { + if (format === 'tar.gz') { archive.pipe(zlib.createGzip()).pipe(res); } else { archive.pipe(res); @@ -167,14 +172,15 @@ app.get('/files/:fid', async(req, res, next) => { try { res.on('finish', async () => { bucket.forEach(async info => { - if(info.metadata.retention === 'one-time') { + if (info.metadata.retention === 'one-time') { await db.remove(info.metadata.sid, info.metadata.key); } else { await db.updateLastDownload(info.metadata.sid, info.metadata.key); } }); }); - } catch(e) { + } + catch (e) { console.error(e); } @@ -182,20 +188,21 @@ app.get('/files/:fid', async(req, res, next) => { } // Download single file - debug(`Download ${req.params.fid}`); + debug(`Download ${ req.params.fid }`); try { const info = await store.info(req.params.fid); // throws on 404 res.download(store.getFilename(req.params.fid), info.metadata.name); // remove one-time files after download res.on('finish', async () => { - if(info.metadata.retention === 'one-time') { + if (info.metadata.retention === 'one-time') { await db.remove(info.metadata.sid, info.metadata.key); } else { await db.updateLastDownload(info.metadata.sid, info.metadata.key); } }); - } catch(e) { + } + catch (e) { res.status(404).send(errorPage.replace('%%ERROR%%', e.message)); } }); @@ -204,9 +211,9 @@ app.get('/files/:fid', async(req, res, next) => { // Upload file app.use('/files', function(req, res, next) { - if(req.method === 'GET') return res.status(405).end(); + if (req.method === 'GET') return res.status(405).end(); - if(req.method === 'POST') { + if (req.method === 'POST') { // validate meta-data // !! tusMeta.encode supports only strings !! const meta = tusMeta.decode(req.get('Upload-Metadata')); @@ -216,7 +223,7 @@ app.use('/files', assert(meta.sid, 'tus meta prop missing: sid'); assert(meta.retention, 'tus meta prop missing: retention'); assert(Object.keys(config.retentions).indexOf(meta.retention) >= 0, - `invalid tus meta prop retention. Value ${meta.retention} not in [${Object.keys(config.retentions).join(',')}]`); + `invalid tus meta prop retention. Value ${ meta.retention } not in [${ Object.keys(config.retentions).join(',') }]`); meta.key = uuid(); meta.createdAt = Date.now().toString(); @@ -231,7 +238,7 @@ app.use('/files', metadata: meta }); } - catch(e) { + catch (e) { return res.status(400).end(e.message); } } @@ -244,7 +251,7 @@ app.use('/files', getKey: req => req.FID, afterComplete: (req, upload, fid) => { db.add(upload.metadata.sid, upload.metadata.key, upload); - debug(`Completed upload ${fid}, size=${upload.size} name=${upload.metadata.name}`); + debug(`Completed upload ${ fid }, size=${ upload.size } name=${ upload.metadata.name }`); }, }) );