references #209 , add optional google recaptcha. implementation could use some polish, but it will work for now.

merge-requests/208/head
Thomas Lynch 4 years ago
parent 900665f1d0
commit 09e0bcb518
  1. 5
      configs/main.js.example
  2. 5
      controllers/pages.js
  3. 3
      gulp/res/js/captcha.js
  4. 23
      gulp/res/js/captchaformsection.js
  5. 14
      gulp/res/js/forms.js
  6. 120
      helpers/captcha/captchaverify.js
  7. 4
      helpers/render.js
  8. 4
      helpers/usesession.js
  9. 22
      models/forms/blockbypass.js
  10. 4
      server.js
  11. 11
      views/includes/captcha.pug
  12. 3
      views/includes/head.pug

@ -41,6 +41,11 @@ module.exports = {
//settings for captchas, if you make them too weak they could probably be solved with OCR.
captchaOptions: {
google: {
enabled: true, //OVERRIDES OTHER CAPTCHA OPTIONS. only used in google-recaptcha branch
siteKey: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
secretKey: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
},
distortion: 13,
numDistorts: {
min: 3,

@ -4,6 +4,7 @@ const express = require('express')
, router = express.Router()
, Boards = require(__dirname+'/../db/boards.js')
, Posts = require(__dirname+'/../db/posts.js')
, { captchaOptions } = require(__dirname+'/../configs/main.js')
//middlewares
, processIp = require(__dirname+'/../helpers/processip.js')
, calcPerms = require(__dirname+'/../helpers/checks/calcpermsmiddleware.js')
@ -65,7 +66,9 @@ router.get('/globalmanage/accounts.html', useSession, sessionRefresh, isLoggedIn
router.get('/globalmanage/settings.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageSettings);
//captcha
router.get('/captcha', processIp, captcha); //get captcha image and cookie
if (!captchaOptions.google.enabled) {
router.get('/captcha', processIp, captcha); //get captcha image and cookie
}
router.get('/captcha.html', captchaPage); //iframed for noscript users
router.get('/bypass.html', blockBypass); //block bypass page
router.get('/bypass_minimal.html', setMinimal, blockBypass); //block bypass page

@ -49,16 +49,17 @@ class CaptchaController {
}
addMissingCaptcha() {
/* not implemented in recaptcha branch
const postSubmitButton = document.getElementById('submitpost');
const captchaFormSectionHtml = captchaformsection();
postSubmitButton.insertAdjacentHTML('beforebegin', captchaFormSectionHtml);
const captchaFormSection = postSubmitButton.previousSibling;
const captchaField = captchaFormSection.querySelector('.captchafield');
this.loadCaptcha(captchaField);
*/
}
loadCaptcha(field) {
console.log(field)
const captchaDiv = field.previousSibling;
const captchaImg = document.createElement('img');
const refreshDiv = document.createElement('div');

@ -1,7 +1,24 @@
function pug_attr(t,e,n,r){if(!1===e||null==e||!e&&("class"===t||"style"===t))return"";if(!0===e)return" "+(r?t:t+'="'+t+'"');var f=typeof e;return"object"!==f&&"function"!==f||"function"!=typeof e.toJSON||(e=e.toJSON()),"string"==typeof e||(e=JSON.stringify(e),n||-1===e.indexOf('"'))?(n&&(e=pug_escape(e))," "+t+'="'+e+'"'):" "+t+"='"+e.replace(/'/g,"'")+"'"}
function pug_escape(e){var a=""+e,t=pug_match_html.exec(a);if(!t)return e;var r,c,n,s="";for(r=t.index,c=0;r<a.length;r++){switch(a.charCodeAt(r)){case 34:n="&quot;";break;case 38:n="&amp;";break;case 60:n="&lt;";break;case 62:n="&gt;";break;default:continue}c!==r&&(s+=a.substring(c,r)),c=r+1,s+=n}return c!==r?s+a.substring(c,r):s}
var pug_match_html=/["&<>]/;function captchaformsection(locals) {var pug_html = "", pug_mixins = {}, pug_interp;pug_mixins["captchaformsection"] = pug_interp = function(){
var pug_match_html=/["&<>]/;function captchaformsection(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;
var locals_for_with = (locals || {});
(function (googleRecaptchaEnabled, googleRecaptchaSiteKey) {
pug_mixins["captchaformsection"] = pug_interp = function(){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Csection class=\"row\"\u003E\u003Cdiv class=\"label\"\u003E\u003Cspan\u003ECaptcha\u003Cspan class=\"required\"\u003E*\u003C\u002Fspan\u003E\u003C\u002Fspan\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"col\"\u003E\u003Cnoscript class=\"no-m-p\"\u003E\u003Ciframe" + (" class=\"captcha\""+" src=\"\u002Fcaptcha.html\""+pug_attr("width=210", true, true, false)+" height=\"80\" scrolling=\"no\" loading=\"lazy\"") + "\u003E\u003C\u002Fiframe\u003E\u003C\u002Fnoscript\u003E\u003Cdiv class=\"jsonly captcha\" style=\"display:none;\"\u003E\u003C\u002Fdiv\u003E\u003Cinput" + (" class=\"captchafield\""+" type=\"text\" name=\"captcha\" autocomplete=\"off\" placeholder=\"Captcha text\" pattern=\".{6}\""+pug_attr("required", true, true, false)+" title=\"6 characters\"") + "\u002F\u003E\u003C\u002Fdiv\u003E\u003C\u002Fsection\u003E";
pug_html = pug_html + "\u003Csection class=\"row\"\u003E\u003Cdiv class=\"label\"\u003E\u003Cspan\u003ECaptcha\u003Cspan class=\"required\"\u003E*\u003C\u002Fspan\u003E\u003C\u002Fspan\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"col\"\u003E";
if (googleRecaptchaEnabled) {
pug_html = pug_html + "\u003Cdiv" + (" class=\"g-recaptcha\""+pug_attr("data-sitekey", `${googleRecaptchaSiteKey}`, true, false)+" data-theme=\"dark\" data-size=\"compact\" data-callback=\"recaptchaCallback\"") + "\u003E\u003C\u002Fdiv\u003E";
}
else {
pug_html = pug_html + "\u003Cnoscript class=\"no-m-p\"\u003E\u003Ciframe" + (" class=\"captcha\""+" src=\"\u002Fcaptcha.html\""+pug_attr("width=210", true, true, false)+" height=\"80\" scrolling=\"no\" loading=\"lazy\"") + "\u003E\u003C\u002Fiframe\u003E\u003C\u002Fnoscript\u003E\u003Cdiv class=\"jsonly captcha\" style=\"display:none;\"\u003E\u003C\u002Fdiv\u003E\u003Cinput" + (" class=\"captchafield\""+" type=\"text\" name=\"captcha\" autocomplete=\"off\" placeholder=\"Captcha text\" pattern=\".{6}\""+pug_attr("required", true, true, false)+" title=\"6 characters\"") + "\u002F\u003E";
}
pug_html = pug_html + "\u003C\u002Fdiv\u003E\u003C\u002Fsection\u003E";
};
pug_mixins["captchaformsection"]();;return pug_html;}
pug_mixins["captchaformsection"]();
}.call(this, "googleRecaptchaEnabled" in locals_for_with ?
locals_for_with.googleRecaptchaEnabled :
typeof googleRecaptchaEnabled !== 'undefined' ? googleRecaptchaEnabled : undefined, "googleRecaptchaSiteKey" in locals_for_with ?
locals_for_with.googleRecaptchaSiteKey :
typeof googleRecaptchaSiteKey !== 'undefined' ? googleRecaptchaSiteKey : undefined));
;;return pug_html;}

@ -63,6 +63,11 @@ function formToJSON(form) {
return JSON.stringify(data);
}
let recaptchaResponse = null;
function recaptchaCallback(response) {
recaptchaResponse = response;
}
class formHandler {
constructor(form) {
@ -123,6 +128,9 @@ class formHandler {
if (this.enctype === 'multipart/form-data') {
this.fileInput && (this.fileInput.disabled = true);
postData = new FormData(this.form);
if (recaptchaResponse) {
postData.append('captcha', recaptchaResponse);
}
this.fileInput && (this.fileInput.disabled = false);
if (this.files && this.files.length > 0) {
//add files to file input element
@ -132,6 +140,9 @@ class formHandler {
}
} else {
postData = new URLSearchParams([...(new FormData(this.form))]);
if (recaptchaResponse) {
postData.set('captcha', recaptchaResponse);
}
}
if (this.banned
|| this.minimal
@ -157,6 +168,9 @@ class formHandler {
}
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (recaptchaResponse && grecaptcha) {
grecaptcha.reset();
}
this.submit.disabled = false;
this.submit.value = this.originalSubmitText;
let json;

@ -1,7 +1,10 @@
'use strict';
const { Captchas, Ratelimits } = require(__dirname+'/../../db/')
, { captchaOptions } = require(__dirname+'/../../configs/main.js')
, FormData = require('form-data')
, { ObjectId } = require(__dirname+'/../../db/db.js')
, fetch = require('node-fetch')
, remove = require('fs-extra').remove
, dynamicResponse = require(__dirname+'/../dynamic.js')
, deleteTempFiles = require(__dirname+'/../files/deletetempfiles.js')
@ -21,8 +24,8 @@ module.exports = async (req, res, next) => {
}
//check if captcha field in form is valid
const input = req.body.captcha;
if (!input || input.length !== 6) {
const input = req.body.captcha || req.body['g-recaptcha-response'];
if (!input || (input.length !== 6 && !captchaOptions.google.enabled)) {
deleteTempFiles(req).catch(e => console.error);
if (isBypass) {
return res.status(403).render('bypass', {
@ -37,55 +40,88 @@ module.exports = async (req, res, next) => {
});
}
//make sure they have captcha cookie and its 24 chars
const captchaId = req.cookies.captchaid;
if (!captchaId || captchaId.length !== 24) {
deleteTempFiles(req).catch(e => console.error);
if (isBypass) {
return res.status(403).render('bypass', {
'minimal': req.body.minimal,
if (!captchaOptions.google.enabled) {
//make sure they have captcha cookie and its 24 chars
const captchaId = req.cookies.captchaid;
if (!captchaId || captchaId.length !== 24) {
deleteTempFiles(req).catch(e => console.error);
if (isBypass) {
return res.status(403).render('bypass', {
'minimal': req.body.minimal,
'message': 'Captcha expired',
});
}
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Captcha expired',
'redirect': req.headers.referer,
});
}
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Captcha expired',
'redirect': req.headers.referer,
});
}
// try to get the captcha from the DB
let captcha;
try {
const captchaMongoId = ObjectId(captchaId);
captcha = await Captchas.findOneAndDelete(captchaMongoId, input);
} catch (err) {
return next(err);
}
// try to get the captcha from the DB
let captcha;
try {
const captchaMongoId = ObjectId(captchaId);
captcha = await Captchas.findOneAndDelete(captchaMongoId, input);
} catch (err) {
return next(err);
}
//check that it exists and matches captcha in DB
if (!captcha || !captcha.value || captcha.value.text !== input) {
deleteTempFiles(req).catch(e => console.error);
if (isBypass) {
return res.status(403).render('bypass', {
'minimal': req.body.minimal,
if (!captcha || !captcha.value || captcha.value.text !== input) {
deleteTempFiles(req).catch(e => console.error);
if (isBypass) {
return res.status(403).render('bypass', {
'minimal': req.body.minimal,
'message': 'Incorrect captcha answer',
});
}
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Incorrect captcha answer',
'redirect': req.headers.referer,
});
}
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Incorrect captcha answer',
'redirect': req.headers.referer,
});
}
//it was correct, so delete the file, the cookie and reset their quota
res.locals.solvedCaptcha = true;
res.clearCookie('captchaid');
await Promise.all([
Ratelimits.resetQuota(res.locals.ip.single, 'captcha'),
remove(`${uploadDirectory}/captcha/${captchaId}.jpg`)
]);
//it was correct, so delete the file, the cookie and reset their quota
res.locals.solvedCaptcha = true;
res.clearCookie('captchaid');
await Promise.all([
Ratelimits.resetQuota(res.locals.ip.single, 'captcha'),
remove(`${uploadDirectory}/captcha/${captchaId}.jpg`)
]);
} else {
//using google recaptcha
let recaptchaResponse;
try {
const form = new FormData();
form.append('secret', captchaOptions.google.secretKey);
form.append('response', input);
recaptchaResponse = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
body: form,
}).then(res => res.json());
} catch (e) {
//no special error, user will jsut get captcha failed error
}
if (!recaptchaResponse || !recaptchaResponse.success) {
//not sure if hostname and timestamp checks are needed here
if (isBypass) {
return res.status(403).render('bypass', {
'minimal': req.body.minimal,
'message': 'Incorrect captcha answer',
});
}
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Incorrect captcha answer',
'redirect': req.headers.referer,
});
}
res.locals.solvedCaptcha = true;
}
return next();

@ -2,7 +2,7 @@
const { enableUserBoardCreation, enableUserAccountCreation,
lockWait, globalLimits, boardDefaults, cacheTemplates,
meta, enableWebring } = require(__dirname+'/../configs/main.js')
meta, enableWebring, captchaOptions } = require(__dirname+'/../configs/main.js')
, { outputFile } = require('fs-extra')
, formatSize = require(__dirname+'/files/formatsize.js')
, pug = require('pug')
@ -25,6 +25,8 @@ module.exports = async (htmlName, templateName, options, json=null) => {
enableUserBoardCreation,
globalLimits,
enableWebring,
googleRecaptchaEnabled: captchaOptions.google.enabled,
googleRecaptchaSiteKey: captchaOptions.google.siteKey,
});
const lock = await redlock.lock(`locks:${htmlName}`, lockWait);
const htmlPromise = outputFile(`${uploadDirectory}/html/${htmlName}`, html);

@ -2,13 +2,13 @@
const session = require('express-session')
, redisStore = require('connect-redis')(session)
, { sessionSecret, secureCookies } = require(__dirname+'/../configs/main.js')
, { cookieSecret, secureCookies } = require(__dirname+'/../configs/main.js')
, { redisClient } = require(__dirname+'/../redis.js')
, production = process.env.NODE_ENV === 'production'
, { DAY } = require(__dirname+'/timeutils.js');
module.exports = session({
secret: sessionSecret,
secret: cookieSecret,
store: new redisStore({
client: redisClient,
}),

@ -11,16 +11,16 @@ module.exports = async (req, res, next) => {
const bypassId = bypass.insertedId;
res.locals.blockBypass = bypass.ops[0];
return res
.cookie('bypassid', bypassId.toString(), {
'maxAge': blockBypass.expireAfterTime,
'secure': production && secureCookies,
'sameSite': 'strict'
})
.render('message', {
'minimal': req.body.minimal, //todo: make use x- header for ajax once implm.
'title': 'Success',
'message': 'Completed block bypass, you may go back and make your post.',
});
res.cookie('bypassid', bypassId.toString(), {
'maxAge': blockBypass.expireAfterTime,
'secure': production && secureCookies,
'sameSite': 'strict',
})
return dynamicResponse(req, res, 200, 'message', {
'minimal': req.body.minimal, //todo: make use x- header for ajax once implm.
'title': 'Success',
'message': 'Completed block bypass, you may go back and make your post.',
});
}

@ -9,7 +9,7 @@ const express = require('express')
, app = express()
, server = require('http').createServer(app)
, cookieParser = require('cookie-parser')
, { cacheTemplates, boardDefaults, globalLimits,
, { cacheTemplates, boardDefaults, globalLimits, captchaOptions,
enableUserBoardCreation, enableUserAccountCreation,
debugLogs, ipHashPermLevel, meta, port, enableWebring } = require(__dirname+'/configs/main.js')
, referrerCheck = require(__dirname+'/helpers/referrercheck.js')
@ -67,6 +67,8 @@ const express = require('express')
//default settings
app.locals.enableUserAccountCreation = enableUserAccountCreation;
app.locals.enableUserBoardCreation = enableUserBoardCreation;
app.locals.googleRecaptchaEnabled = captchaOptions.google.enabled;
app.locals.googleRecaptchaSiteKey = captchaOptions.google.siteKey;
app.locals.defaultTheme = boardDefaults.theme;
app.locals.defaultCodeTheme = boardDefaults.codeTheme;
app.locals.globalLimits = globalLimits;

@ -1,4 +1,7 @@
noscript.no-m-p
iframe.captcha(src='/captcha.html' 'width=210' height='80' scrolling='no' loading='lazy')
.jsonly.captcha(style='display:none;')
input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='Captcha text' pattern=".{6}" required title='6 characters')
if googleRecaptchaEnabled
div(class="g-recaptcha" data-sitekey=`${googleRecaptchaSiteKey}` data-theme="dark" data-size="compact" data-callback="recaptchaCallback")
else
noscript.no-m-p
iframe.captcha(src='/captcha.html' 'width=210' height='80' scrolling='no' loading='lazy')
.jsonly.captcha(style='display:none;')
input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='Captcha text' pattern=".{6}" required title='6 characters')

@ -17,3 +17,6 @@ if isBoard && board.settings.customCss
link#codetheme(rel='stylesheet' data-theme=codeTheme href=`/css/codethemes/${codeTheme}.css`)
include ./favicon.pug
script(src=`/js/all.js?v=${commit}`)
if googleRecaptchaEnabled
script(src="https://www.google.com/recaptcha/api.js" async defer)

Loading…
Cancel
Save