From 14dc090e08787fd573777bc142ba4e17d39c94ed Mon Sep 17 00:00:00 2001 From: Thomas Lynch Date: Sat, 22 Aug 2020 05:10:45 +0000 Subject: [PATCH] Migration, and a change that will make it not get completely destroyed by ddos over TOR --- configs/main.js.example | 3 ++- controllers/forms.js | 2 +- controllers/pages.js | 2 +- db/captchas.js | 8 ++++++++ helpers/captcha/generators/grid.js | 2 +- helpers/captcha/generators/text.js | 2 +- helpers/captcha/{captchaverify.js => verify.js} | 0 migrations/index.js | 1 + migrations/migration-0.0.11.js | 6 ++++++ models/pages/captcha.js | 17 +++++++++++++---- package.json | 2 +- 11 files changed, 35 insertions(+), 10 deletions(-) rename helpers/captcha/{captchaverify.js => verify.js} (100%) create mode 100644 migrations/migration-0.0.11.js diff --git a/configs/main.js.example b/configs/main.js.example index af2d3e83..e7ef051f 100644 --- a/configs/main.js.example +++ b/configs/main.js.example @@ -42,7 +42,8 @@ nano module.exports = { //settings for captchas captchaOptions: { type: 'grid', //"text", "grid" or "google" - google: { + generateLimit: 1000, //max number of captchas to have generated at any time, prevent mass unsolved captcha spam, especially on TOR. + google: { //options for google captcha, when captcha type is google siteKey: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', secretKey: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' }, diff --git a/controllers/forms.js b/controllers/forms.js index 2d2f80a5..3d9184c0 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -13,7 +13,7 @@ const express = require('express') , numFiles = require(__dirname+'/../helpers/numfiles.js') , banCheck = require(__dirname+'/../helpers/checks/bancheck.js') , isLoggedIn = require(__dirname+'/../helpers/checks/isloggedin.js') - , verifyCaptcha = require(__dirname+'/../helpers/captcha/captchaverify.js') + , verifyCaptcha = require(__dirname+'/../helpers/captcha/verify.js') , csrf = require(__dirname+'/../helpers/checks/csrfmiddleware.js') , useSession = require(__dirname+'/../helpers/usesession.js') , sessionRefresh = require(__dirname+'/../helpers/sessionrefresh.js') diff --git a/controllers/pages.js b/controllers/pages.js index 5a835b64..ae7e2db7 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -68,7 +68,7 @@ router.get('/globalmanage/settings.html', useSession, sessionRefresh, isLoggedIn //captcha if (captchaOptions.type !== 'google') { - router.get('/captcha', processIp, captcha); //get captcha image and cookie + router.get('/captcha', geoAndTor, processIp, captcha); //get captcha image and cookie } router.get('/captcha.html', captchaPage); //iframed for noscript users router.get('/bypass.html', blockBypass); //block bypass page diff --git a/db/captchas.js b/db/captchas.js index acd14d32..a8086a4d 100644 --- a/db/captchas.js +++ b/db/captchas.js @@ -25,6 +25,14 @@ module.exports = { }); }, + randomSample: () => { + return db.aggregate([ + { + $sample: { size: 1 } + } + ]).toArray().then(res => res[0]); + }, + deleteAll: () => { return db.deleteMany({}); }, diff --git a/helpers/captcha/generators/grid.js b/helpers/captcha/generators/grid.js index ba026d95..cea1a445 100644 --- a/helpers/captcha/generators/grid.js +++ b/helpers/captcha/generators/grid.js @@ -75,7 +75,7 @@ module.exports = async () => { if (err) { return reject(err); } - return resolve({ id: captchaId, boolArray }); + return resolve({ id: captchaId }); }); }); diff --git a/helpers/captcha/generators/text.js b/helpers/captcha/generators/text.js index cf67f6d5..c7b6d6c8 100644 --- a/helpers/captcha/generators/text.js +++ b/helpers/captcha/generators/text.js @@ -86,7 +86,7 @@ module.exports = async () => { if (err) { return reject(err); } - return resolve({ id: captchaId, text }); + return resolve({ id: captchaId }); }); }); diff --git a/helpers/captcha/captchaverify.js b/helpers/captcha/verify.js similarity index 100% rename from helpers/captcha/captchaverify.js rename to helpers/captcha/verify.js diff --git a/migrations/index.js b/migrations/index.js index 2f6a7dd9..bcd3b388 100644 --- a/migrations/index.js +++ b/migrations/index.js @@ -11,4 +11,5 @@ module.exports = { '0.0.8': require(__dirname+'/migration-0.0.8.js'), //option to auto reset triggers after hour is over '0.0.9': require(__dirname+'/migration-0.0.9.js'), //ip changes '0.0.10': require(__dirname+'/migration-0.0.10.js'), //add links to modlog for new logs + '0.0.11': require(__dirname+'/migration-0.0.11.js'), //rename captcha "text" field to "answer" since we support multiple captcha types now } diff --git a/migrations/migration-0.0.11.js b/migrations/migration-0.0.11.js new file mode 100644 index 00000000..5d313566 --- /dev/null +++ b/migrations/migration-0.0.11.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = async(db, redis) => { + console.log('Expiring existing captchas, so new ones get new answer format'); + await db.collection('captcha').deleteMany({}); +}; diff --git a/models/pages/captcha.js b/models/pages/captcha.js index e7a76585..7bbea1d8 100644 --- a/models/pages/captcha.js +++ b/models/pages/captcha.js @@ -1,6 +1,6 @@ 'use strict'; -const { Ratelimits } = require(__dirname+'/../../db/') +const { Captchas, Ratelimits } = require(__dirname+'/../../db/') , { secureCookies, rateLimitCost, captchaOptions } = require(__dirname+'/../../configs/main.js') , generateCaptcha = require(__dirname+`/../../helpers/captcha/generators/${captchaOptions.type}.js`) , production = process.env.NODE_ENV === 'production'; @@ -12,6 +12,7 @@ module.exports = async (req, res, next) => { } let captchaId; + let maxAge = 5*60*1000; try { if (!res.locals.tor) { const ratelimit = await Ratelimits.incrmentQuota(res.locals.ip.single, 'captcha', rateLimitCost.captcha); @@ -19,7 +20,15 @@ module.exports = async (req, res, next) => { return res.status(429).redirect('/file/ratelimit.png'); } } - const { id } = await generateCaptcha(); + let id; + if ((await Captchas.db.estimatedDocumentCount()) >= captchaOptions.generateLimit) { + //TODOs: round robin sample? store in redis? only sample random with longer than x expiry? + const randomCaptcha = await Captchas.randomSample(); + id = randomCaptcha._id; + maxAge = Math.abs((randomCaptcha.expireAt.getTime()+maxAge) - Date.now()); //abs in case mongo hasn't pruned, and will not be too big since it can't be too far away from pruning anyway + } else { + ({ id } = await generateCaptcha()); + } captchaId = id; } catch (err) { return next(err); @@ -27,9 +36,9 @@ module.exports = async (req, res, next) => { return res .cookie('captchaid', captchaId.toString(), { - 'maxAge': 5*60*1000, //5 minute cookie 'secure': production && secureCookies, - 'sameSite': 'strict' + 'sameSite': 'strict', + maxAge, }) .redirect(`/captcha/${captchaId}.jpg`); diff --git a/package.json b/package.json index dfbde8a3..91d02536 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jschan", "version": "0.0.1", - "migrateVersion": "0.0.10", + "migrateVersion": "0.0.11", "description": "", "main": "server.js", "dependencies": {