Some response delays to make brute force harder; Code Reformat
This commit is contained in:
@@ -27,7 +27,7 @@ const app = express();
|
|||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
if(config.accessLog) {
|
if (config.accessLog) {
|
||||||
app.use(morgan(config.accessLog));
|
app.use(morgan(config.accessLog));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,13 +41,12 @@ app.get('/robots.txt', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Upload App
|
// Upload App
|
||||||
//
|
|
||||||
//
|
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
if (config.uploadAppPath != '/')
|
if (config.uploadAppPath !== '/') {
|
||||||
res.status(304).redirect(config.uploadAppPath);
|
res.status(304).redirect(config.uploadAppPath);
|
||||||
else
|
} else {
|
||||||
res.sendFile(path.join(__dirname, '../public/html/upload.html'));
|
res.sendFile(path.join(__dirname, '../public/html/upload.html'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get(config.uploadAppPath, (req, res) => {
|
app.get(config.uploadAppPath, (req, res) => {
|
||||||
@@ -65,19 +64,29 @@ app.get('/config.json', (req, res) => {
|
|||||||
|
|
||||||
|
|
||||||
app.get('/admin', (req, res, next) => {
|
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'));
|
res.sendFile(path.join(__dirname, '../public/html/admin.html'));
|
||||||
});
|
});
|
||||||
app.get('/admin/data.json', (req, res, next) => {
|
app.get('/admin/data.json', (req, res, next) => {
|
||||||
if(!config.adminPass) return 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');
|
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)
|
const result = _.chain(db.db)
|
||||||
.cloneDeep()
|
.cloneDeep()
|
||||||
.forEach(bucket => {
|
.forEach(bucket => {
|
||||||
bucket.forEach(file => {
|
bucket.forEach(file => {
|
||||||
if(file.metadata.password) {
|
if (file.metadata.password) {
|
||||||
file.metadata._password = true;
|
file.metadata._password = true;
|
||||||
delete file.metadata.password;
|
delete file.metadata.password;
|
||||||
delete file.metadata.key;
|
delete file.metadata.key;
|
||||||
@@ -88,24 +97,21 @@ app.get('/admin/data.json', (req, res, next) => {
|
|||||||
})
|
})
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
// make bruteforce attack more difficult
|
setTimeout(() => res.json(result), bfTimeout);
|
||||||
setTimeout(() => {
|
|
||||||
res.json(result);
|
|
||||||
},250);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// List files / Download App
|
// List files / Download App
|
||||||
app.get('/:sid', (req, res, next) => {
|
app.get('/:sid', (req, res, next) => {
|
||||||
if(req.url.endsWith('.json')) {
|
if (req.url.endsWith('.json')) {
|
||||||
const sid = req.params.sid.substr(0, req.params.sid.length-5);
|
const sid = req.params.sid.substr(0, req.params.sid.length - 5);
|
||||||
if(!db.get(sid)) return res.status(404).end();
|
if (!db.get(sid)) return res.status(404).end();
|
||||||
|
|
||||||
res.header('Cache-control', 'private, max-age=0, no-cache, no-store, must-revalidate');
|
res.header('Cache-control', 'private, max-age=0, no-cache, no-store, must-revalidate');
|
||||||
res.json({
|
res.json({
|
||||||
items: db.get(sid).map(data => {
|
items: db.get(sid).map(data => {
|
||||||
const item = Object.assign(data, {url: `/files/${sid}++${data.key}`});
|
const item = Object.assign(data, { url: `/files/${ sid }++${ data.key }` });
|
||||||
if(item.metadata.password) {
|
if (item.metadata.password) {
|
||||||
return AES.encrypt(JSON.stringify(data), item.metadata.password).toString();
|
return AES.encrypt(JSON.stringify(data), item.metadata.password).toString();
|
||||||
} else {
|
} else {
|
||||||
return item;
|
return item;
|
||||||
@@ -116,34 +122,33 @@ app.get('/:sid', (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} 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'));
|
res.sendFile(path.join(__dirname, '../public/html/download.html'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Download files
|
// 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
|
// 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
|
// Download all files
|
||||||
if(req.params.fid.match(/^[a-z0-9+]+\.(tar\.gz|zip)$/)) {
|
if (req.params.fid.match(/^[a-z0-9+]+\.(tar\.gz|zip)$/)) {
|
||||||
const sid = req.params.fid.split('++')[0];
|
|
||||||
const format = req.params.fid.endsWith('.zip') ? 'zip' : 'tar.gz';
|
const format = req.params.fid.endsWith('.zip') ? 'zip' : 'tar.gz';
|
||||||
const bucket = db.get(sid);
|
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'));
|
res.status(404).send(errorPage.replace('%%ERROR%%', 'Invalid link'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debug(`Download Bucket ${sid}`);
|
debug(`Download Bucket ${ sid }`);
|
||||||
|
|
||||||
if(format === 'zip') res.header('ContentType', 'application/zip');
|
if (format === 'zip') res.header('ContentType', 'application/zip');
|
||||||
if(format === 'tar.gz') res.header('ContentType', 'application/x-gtar');
|
if (format === 'tar.gz') res.header('ContentType', 'application/x-gtar');
|
||||||
res.header('Content-Disposition', `attachment; filename="${sid}.${format}"`);
|
res.header('Content-Disposition', `attachment; filename="${ sid }.${ format }"`);
|
||||||
|
|
||||||
const archive = archiver(format === 'zip' ? 'zip' : 'tar');
|
const archive = archiver(format === 'zip' ? 'zip' : 'tar');
|
||||||
archive.on('error', function(err) {
|
archive.on('error', function(err) {
|
||||||
@@ -153,11 +158,11 @@ app.get('/files/:fid', async(req, res, next) => {
|
|||||||
bucket.forEach(info => {
|
bucket.forEach(info => {
|
||||||
archive.append(
|
archive.append(
|
||||||
fs.createReadStream(store.getFilename(info.metadata.sid + '++' + info.key)),
|
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);
|
archive.pipe(zlib.createGzip()).pipe(res);
|
||||||
} else {
|
} else {
|
||||||
archive.pipe(res);
|
archive.pipe(res);
|
||||||
@@ -167,14 +172,15 @@ app.get('/files/:fid', async(req, res, next) => {
|
|||||||
try {
|
try {
|
||||||
res.on('finish', async () => {
|
res.on('finish', async () => {
|
||||||
bucket.forEach(async info => {
|
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);
|
await db.remove(info.metadata.sid, info.metadata.key);
|
||||||
} else {
|
} else {
|
||||||
await db.updateLastDownload(info.metadata.sid, info.metadata.key);
|
await db.updateLastDownload(info.metadata.sid, info.metadata.key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch(e) {
|
}
|
||||||
|
catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,20 +188,21 @@ app.get('/files/:fid', async(req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Download single file
|
// Download single file
|
||||||
debug(`Download ${req.params.fid}`);
|
debug(`Download ${ req.params.fid }`);
|
||||||
try {
|
try {
|
||||||
const info = await store.info(req.params.fid); // throws on 404
|
const info = await store.info(req.params.fid); // throws on 404
|
||||||
res.download(store.getFilename(req.params.fid), info.metadata.name);
|
res.download(store.getFilename(req.params.fid), info.metadata.name);
|
||||||
|
|
||||||
// remove one-time files after download
|
// remove one-time files after download
|
||||||
res.on('finish', async () => {
|
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);
|
await db.remove(info.metadata.sid, info.metadata.key);
|
||||||
} else {
|
} else {
|
||||||
await db.updateLastDownload(info.metadata.sid, info.metadata.key);
|
await db.updateLastDownload(info.metadata.sid, info.metadata.key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch(e) {
|
}
|
||||||
|
catch (e) {
|
||||||
res.status(404).send(errorPage.replace('%%ERROR%%', e.message));
|
res.status(404).send(errorPage.replace('%%ERROR%%', e.message));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -204,9 +211,9 @@ app.get('/files/:fid', async(req, res, next) => {
|
|||||||
// Upload file
|
// Upload file
|
||||||
app.use('/files',
|
app.use('/files',
|
||||||
function(req, res, next) {
|
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
|
// validate meta-data
|
||||||
// !! tusMeta.encode supports only strings !!
|
// !! tusMeta.encode supports only strings !!
|
||||||
const meta = tusMeta.decode(req.get('Upload-Metadata'));
|
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.sid, 'tus meta prop missing: sid');
|
||||||
assert(meta.retention, 'tus meta prop missing: retention');
|
assert(meta.retention, 'tus meta prop missing: retention');
|
||||||
assert(Object.keys(config.retentions).indexOf(meta.retention) >= 0,
|
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.key = uuid();
|
||||||
meta.createdAt = Date.now().toString();
|
meta.createdAt = Date.now().toString();
|
||||||
@@ -231,7 +238,7 @@ app.use('/files',
|
|||||||
metadata: meta
|
metadata: meta
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch(e) {
|
catch (e) {
|
||||||
return res.status(400).end(e.message);
|
return res.status(400).end(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,7 +251,7 @@ app.use('/files',
|
|||||||
getKey: req => req.FID,
|
getKey: req => req.FID,
|
||||||
afterComplete: (req, upload, fid) => {
|
afterComplete: (req, upload, fid) => {
|
||||||
db.add(upload.metadata.sid, upload.metadata.key, upload);
|
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 }`);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user