Make a bit more maintainable, support different captcha types with some config options

merge-requests/208/head
Thomas Lynch 4 years ago
parent f751436ae4
commit 60d36bbb6a
  1. 12
      configs/main.js.example
  2. 2
      controllers/pages.js
  3. 13
      gulp/res/css/style.css
  4. 12
      gulp/res/js/captcha.js
  5. 31
      gulp/res/js/captchaformsection.js
  6. 4
      gulp/res/js/locals.js
  7. 15
      gulpfile.js
  8. 6
      helpers/captcha/captchaverify.js
  9. 8
      helpers/captcha/generators/grid.js
  10. 94
      helpers/captcha/generators/text.js
  11. 89
      helpers/checks/captcha.js
  12. 25
      helpers/render.js
  13. 6
      models/pages/captcha.js
  14. 13
      server.js
  15. 3
      views/includes/actionfooter.pug
  16. 5
      views/includes/banform.pug
  17. 31
      views/includes/captcha.pug
  18. 3
      views/includes/captchaexpand.pug
  19. 3
      views/includes/captchafieldrow.pug
  20. 6
      views/includes/captchasidelabel.pug
  21. 2
      views/includes/head.pug
  22. 5
      views/includes/postform.pug
  23. 3
      views/pages/bypass.pug
  24. 5
      views/pages/changepassword.pug
  25. 5
      views/pages/create.pug
  26. 5
      views/pages/register.pug

@ -39,20 +39,20 @@ nano module.exports = {
url: 'https://domain.com'
},
//settings for captchas, if you make them too weak they could probably be solved with OCR.
//settings for captchas
captchaOptions: {
type: 'grid', //"text", "grid" or "google"
google: {
enabled: false, //Overrides builtin captcha
siteKey: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
secretKey: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
},
distortion: 13,
distortion: 9,
numDistorts: {
min: 3,
max: 5
max: 4
},
paintAmount: 2,
fontPaths: [], //optional list of file paths to fonts for captchas
imageSize: 150,
gridSize: 4,
},
/* dnsbl, will add a small delay for uncached requests. You could also install some

@ -67,7 +67,7 @@ router.get('/globalmanage/accounts.html', useSession, sessionRefresh, isLoggedIn
router.get('/globalmanage/settings.html', useSession, sessionRefresh, isLoggedIn, calcPerms, hasPerms(0), csrf, globalManageSettings);
//captcha
if (!captchaOptions.google.enabled) {
if (captchaOptions.type !== 'google') {
router.get('/captcha', processIp, captcha); //get captcha image and cookie
}
router.get('/captcha.html', captchaPage); //iframed for noscript users

@ -3,7 +3,8 @@
--spoiler-img: url('/file/spoiler.png');
--audio-img: url('/file/audio.png');
--thumbnail-size: 250px;
--captcha-size: 200px;
--captcha-w: 200px;
--captcha-h: 200px;
}
body {
@ -604,8 +605,8 @@ div.captchafield {
padding: 0px;
grid-template-columns: 1fr 1fr 1fr 1fr;
display: grid;
width: var(--captcha-size);
height: var(--captcha-size);
width: var(--captcha-w);
height: var(--captcha-w);
margin-left: 1px;
box-sizing: border-box;
border: 1px solid var(--font-color);
@ -613,7 +614,7 @@ div.captchafield {
label.captchachecklabel {
border: 1px solid var(--font-color);
max-width: 50px;
/*max-width: 50px;*/
padding: 0px;
}
@ -1085,8 +1086,8 @@ iframe.bypass {
border: 1px solid var(--input-borders);
background: white;
margin-bottom: 1px;
width: var(--captcha-size);
height: var(--captcha-size);
width: var(--captcha-w);
height: var(--captcha-h);
box-sizing: border-box;
object-fit: contain;
overflow: hidden;

@ -14,13 +14,19 @@ class CaptchaController {
}
setupCaptchaField(captcha) {
if (captcha.firstChild && captcha.firstChild.firstChild
&& captcha.firstChild.firstChild.form
&& captcha.firstChild.firstChild.form.dataset.captchaPreload == 'true') {
this.loadCaptcha(captcha);
} else {
return this.loadCaptcha(captcha);
}
if (captchaType === 'grid') {
const hoverListener = captcha.parentElement.previousSibling.tagName === 'SUMMARY' ? captcha.parentElement.previousSibling : captcha.parentElement;
hoverListener.addEventListener('mouseover', () => this.loadCaptcha(captcha), { once: true });
} else {
captcha.placeholder = 'focus to load captcha';
captcha.addEventListener('focus', () => this.loadCaptcha(captcha), { once: true });
}
}
@ -73,8 +79,10 @@ class CaptchaController {
refreshDiv.classList.add('captcharefresh', 'noselect');
refreshDiv.addEventListener('click', (e) => this.refreshCaptchas(e), true);
refreshDiv.textContent = '↻';
field.placeholder = 'loading';
captchaImg.src = '/captcha';
captchaImg.onload = function() {
field.placeholder = '';
captchaDiv.appendChild(captchaImg);
captchaDiv.appendChild(refreshDiv);
}

@ -3,26 +3,33 @@ function pug_escape(e){var a=""+e,t=pug_match_html.exec(a);if(!t)return e;var r,
var pug_match_html=/["&<>]/;function captchaformsection(locals) {var pug_html = "", pug_mixins = {}, pug_interp;;
var locals_for_with = (locals || {});
(function (googleRecaptchaEnabled, googleRecaptchaSiteKey) {
(function (captchaGridSize, captchaType, googleRecaptchaSiteKey) {
pug_mixins["captchaexpand"] = pug_interp = function(){
var block = (this && this.block), attributes = (this && this.attributes) || {};
pug_html = pug_html + "\u003Cdetails class=\"row label mr-0\"\u003E\u003Csummary class=\"pv-5\"\u003ECaptcha\u003Cspan class=\"required\"\u003E*\u003C\u002Fspan\u003E\u003C\u002Fsummary\u003E\u003Cdiv class=\"catalog\"\u003E";
if (googleRecaptchaEnabled) {
pug_html = pug_html + "\u003Cdetails class=\"row label mr-0\"\u003E\u003Csummary class=\"pv-5\"\u003ECaptcha\u003Cspan class=\"required\"\u003E*\u003C\u002Fspan\u003E\u003C\u002Fsummary\u003E";
switch (captchaType){
case 'google':
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\" width=\"150\" height=\"150\" scrolling=\"no\" loading=\"lazy\"\u003E\u003C\u002Fiframe\u003E\u003C\u002Fnoscript\u003E\u003Cdiv class=\"jsonly captcha\" style=\"display:none\"\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"captchafield noselect\"\u003E";
for(let i = 0; i < 16; i++) {
break;
case 'text':
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";
break;
case 'grid':
pug_html = pug_html + "\u003Cdiv class=\"catalog\"\u003E\u003Cnoscript class=\"no-m-p\"\u003E\u003Ciframe class=\"captcha\" src=\"\u002Fcaptcha.html\" width=\"150\" height=\"150\" scrolling=\"no\" loading=\"lazy\"\u003E\u003C\u002Fiframe\u003E\u003C\u002Fnoscript\u003E\u003Cdiv class=\"jsonly captcha\" style=\"display:none\"\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\"captchafield noselect\"\u003E";
for(let i = 0; i < captchaGridSize**2; i++) {
pug_html = pug_html + "\u003Clabel class=\"captchachecklabel\"\u003E\u003Cinput" + (" type=\"checkbox\" name=\"captcha\""+pug_attr("value", i, true, false)) + "\u002F\u003E\u003Cspan class=\"captchacheckbox\"\u003E\u003C\u002Fspan\u003E\u003C\u002Flabel\u003E";
}
pug_html = pug_html + "\u003C\u002Fdiv\u003E";
pug_html = pug_html + "\u003C\u002Fdiv\u003E\u003C\u002Fdiv\u003E";
break;
}
pug_html = pug_html + "\u003C\u002Fdiv\u003E\u003C\u002Fdetails\u003E";
pug_html = pug_html + "\u003C\u002Fdetails\u003E";
};
pug_mixins["captchaexpand"]();
}.call(this, "googleRecaptchaEnabled" in locals_for_with ?
locals_for_with.googleRecaptchaEnabled :
typeof googleRecaptchaEnabled !== 'undefined' ? googleRecaptchaEnabled : undefined, "googleRecaptchaSiteKey" in locals_for_with ?
}.call(this, "captchaGridSize" in locals_for_with ?
locals_for_with.captchaGridSize :
typeof captchaGridSize !== 'undefined' ? captchaGridSize : undefined, "captchaType" in locals_for_with ?
locals_for_with.captchaType :
typeof captchaType !== 'undefined' ? captchaType : undefined, "googleRecaptchaSiteKey" in locals_for_with ?
locals_for_with.googleRecaptchaSiteKey :
typeof googleRecaptchaSiteKey !== 'undefined' ? googleRecaptchaSiteKey : undefined));
;;return pug_html;}

@ -0,0 +1,4 @@
const themes = ['amoled', 'chaos', 'choc', 'clear', 'darkblue', 'gurochan', 'lain', 'miku', 'navy', 'pink', 'rei-zero', 'robot', 'tomorrow', 'tomorrow2', 'win95', 'yotsuba b', 'yotsuba'];
const codeThemes = ['a11y-dark', 'a11y-light', 'agate', 'an-old-hope', 'androidstudio', 'arduino-light', 'arta', 'ascetic', 'atelier-cave-dark', 'atelier-cave-light', 'atelier-dune-dark', 'atelier-dune-light', 'atelier-estuary-dark', 'atelier-estuary-light', 'atelier-forest-dark', 'atelier-forest-light', 'atelier-heath-dark', 'atelier-heath-light', 'atelier-lakeside-dark', 'atelier-lakeside-light', 'atelier-plateau-dark', 'atelier-plateau-light', 'atelier-savanna-dark', 'atelier-savanna-light', 'atelier-seaside-dark', 'atelier-seaside-light', 'atelier-sulphurpool-dark', 'atelier-sulphurpool-light', 'atom-one-dark-reasonable', 'atom-one-dark', 'atom-one-light', 'brown-paper', 'brown-papersq', 'codepen-embed', 'color-brewer', 'darcula', 'dark', 'default', 'docco', 'dracula', 'far', 'foundation', 'github-gist', 'github', 'gml', 'googlecode', 'gradient-dark', 'grayscale', 'gruvbox-dark', 'gruvbox-light', 'hopscotch', 'hybrid', 'idea', 'ir-black', 'isbl-editor-dark', 'isbl-editor-light', 'kimbie.dark', 'kimbie.light', 'lightfair', 'lioshi', 'magula', 'mono-blue', 'monokai-sublime', 'monokai', 'night-owl', 'nnfx-dark', 'nnfx', 'nord', 'obsidian', 'ocean', 'paraiso-dark', 'paraiso-light', 'pojoaque', 'pojoaque', 'purebasic', 'qtcreator_dark', 'qtcreator_light', 'railscasts', 'rainbow', 'routeros', 'school-book', 'school-book', 'shades-of-purple', 'solarized-dark', 'solarized-light', 'srcery', 'sunburst', 'tomorrow-night-blue', 'tomorrow-night-bright', 'tomorrow-night-eighties', 'tomorrow-night', 'tomorrow', 'vs', 'vs2015', 'xcode', 'xt256', 'zenburn'];
const captchaType = 'grid';
const SERVER_TIMEZONE = 'UTC'

@ -186,8 +186,9 @@ function custompages() {
defaultTheme: configs.boardDefaults.theme,
defaultCodeTheme: configs.boardDefaults.codeTheme,
postFilesSize: formatSize(configs.globalLimits.postFilesSize.max),
googleRecaptchaEnabled: configs.captchaOptions.google.enabled,
captchaType: configs.captchaOptions.type,
googleRecaptchaSiteKey: configs.captchaOptions.google.siteKey,
captchaGridSize: configs.captchaOptions.gridSize,
commit,
}
}))
@ -196,10 +197,11 @@ function custompages() {
function scripts() {
try {
const themelist = `const themes = ['${themes.join("', '")}'];const codeThemes = ['${codeThemes.join("', '")}'];`;
fs.writeFileSync('gulp/res/js/themelist.js', themelist);
const serverTimeZone = `const SERVER_TIMEZONE = '${Intl.DateTimeFormat().resolvedOptions().timeZone}'`;
fs.writeFileSync('gulp/res/js/timezone.js', serverTimeZone);
const locals = `const themes = ['${themes.join("', '")}'];
const codeThemes = ['${codeThemes.join("', '")}'];
const captchaType = '${configs.captchaOptions.type}';
const SERVER_TIMEZONE = '${Intl.DateTimeFormat().resolvedOptions().timeZone}'`;
fs.writeFileSync('gulp/res/js/locals.js', locals);
fs.writeFileSync('gulp/res/js/post.js', pug.compileFileClient(`${paths.pug.src}/includes/post.pug`, { compileDebug: false, debug: false, name: 'post' }));
fs.writeFileSync('gulp/res/js/modal.js', pug.compileFileClient(`${paths.pug.src}/includes/modal.pug`, { compileDebug: false, debug: false, name: 'modal' }));
fs.writeFileSync('gulp/res/js/uploaditem.js', pug.compileFileClient(`${paths.pug.src}/includes/uploaditem.pug`, { compileDebug: false, debug: false, name: 'uploaditem' }));
@ -212,8 +214,7 @@ function scripts() {
}
gulp.src([
//put scripts in order for dependencies
`${paths.scripts.src}/themelist.js`,
`${paths.scripts.src}/timezone.js`,
`${paths.scripts.src}/locals.js`,
`${paths.scripts.src}/localstorage.js`,
`${paths.scripts.src}/modal.js`,
`${paths.scripts.src}/post.js`,

@ -45,11 +45,11 @@ module.exports = async (req, res, next) => {
//it was correct, so mark as solved for other middleware
res.locals.solvedCaptcha = true;
if (!captchaOptions.google.enabled) {
//for builtin captcha, clear captchaid cookie, delete file and reset quota
if (captchaOptions.type !== 'google') {
//for builtin captchas, clear captchaid cookie, delete file and reset quota
res.clearCookie('captchaid');
await Promise.all([
!res.locals.tor && !captchaOptions.google.enabled && Ratelimits.resetQuota(res.locals.ip.single, 'captcha'),
!res.locals.tor && Ratelimits.resetQuota(res.locals.ip.single, 'captcha'),
remove(`${uploadDirectory}/captcha/${captchaId}.jpg`)
]);
}

@ -1,7 +1,7 @@
const gm = require('gm').subClass({ imageMagick: true })
, { Captchas } = require(__dirname+'/../../db/')
, { captchaOptions } = require(__dirname+'/../../configs/main.js')
, uploadDirectory = require(__dirname+'/../files/uploadDirectory.js')
, { Captchas } = require(__dirname+'/../../../db/')
, { captchaOptions } = require(__dirname+'/../../../configs/main.js')
, uploadDirectory = require(__dirname+'/../../files/uploadDirectory.js')
, { promisify } = require('util')
, randomBytes = promisify(require('crypto').randomBytes)
, randomRange = async (min, max) => {
@ -50,7 +50,7 @@ module.exports = async () => {
return new Promise(async(resolve, reject) => {
const captcha = gm(width,height,'#ffffff')
.fill('#000000')
.font(__dirname+'/font.ttf');
.font(__dirname+'/../font.ttf');
const spaceSize = (width-crop)/gridSize;
for(let i = 0, j = 0; i < boolArray.length; i++) {

@ -0,0 +1,94 @@
const gm = require('gm').subClass({ imageMagick: true })
, { Captchas } = require(__dirname+'/../../../db/')
, { captchaOptions } = require(__dirname+'/../../../configs/main.js')
, uploadDirectory = require(__dirname+'/../../files/uploadDirectory.js')
, { promisify } = require('util')
, randomBytes = promisify(require('crypto').randomBytes)
, characterWidth = (char) => {
switch (char) {
case 'w':
case 'm':
return 45;
case 'i':
case 'l':
return 12;
case 'f':
case 'j':
case 't':
return 15;
default:
return 30;
}
}
, totalWidth = (text) => {
return text.split('').reduce((acc, char) => {
return characterWidth(char) + acc + 1;
}, 0);
}
, width = 210
, height = 80
, distortion = captchaOptions.distortion
, minVal = parseInt('1000000', 36)
, maxVal = parseInt('1zzzzzz', 36);
const randomRange = async (min, max) => {
if (max <= min) return min;
const mod = max - min + 1;
const div = (((0xffffffff - (mod-1)) / mod) | 0) + 1;
let g
do {
g = (await randomBytes(4)).readUInt32LE();
} while (g > div * mod - 1);
return ((g / div) | 0) + min;
};
module.exports = async () => {
// generate between 1000000 and 1zzzzzz and not 0 and zzzzzz, so toString
// will have enough characters
const textInt = await randomRange(minVal, maxVal);
const text = textInt.toString(36).substr(-6, 6);
const captchaId = await Captchas.insertOne(text).then(r => r.insertedId);
const distorts = [];
const numDistorts = await randomRange(
captchaOptions.numDistorts.min,captchaOptions.numDistorts.max);
const div = width/numDistorts;
for (let i = 0; i < numDistorts; i++) {
const divStart = (div*i)
, divEnd = (div*(i+1));
const originx = await randomRange(divStart, divEnd)
, originy = await randomRange(0,height);
const destx = await randomRange(Math.max(distortion,originx-distortion),Math.min(width-distortion,originx+distortion))
, desty = await randomRange(Math.max(distortion,originy-distortion*2),Math.min(height-distortion,originy+distortion*2));
distorts.push([
{x:originx,y:originy}, //origin
{x:destx,y:desty} //dest
]);
}
const lineY = await randomRange(35,45);
return new Promise((resolve, reject) => {
const captcha = gm(width,height,'#ffffff')
.fill('#000000')
.fontSize(65);
const startX = (width-totalWidth(text))/2;
let charX = startX;
for (let i = 0; i < 6; i++) {
captcha.drawText(charX, 60, text[i]);
charX += characterWidth(text[i]);
}
captcha
.drawRectangle(startX, lineY, charX, lineY+4)
.distort(distorts, 'Shepards')
.paint(2)
.write(`${uploadDirectory}/captcha/${captchaId}.jpg`, (err) => {
if (err) {
return reject(err);
}
return resolve({ id: captchaId, text });
});
});
}

@ -10,47 +10,68 @@ const { Captchas } = require(__dirname+'/../../db/')
module.exports = async (captchaInput, captchaId) => {
//check if captcha field in form is valid
if (!captchaInput /* || (captchaInput.length !== 6 && !captchaOptions.google.enabled)*/) {
if (!captchaInput
|| (captchaInput.length !== 6 && !captchaOptions.type === 'text')) {
throw 'Incorrect captcha answer';
}
//make sure they have captcha cookie and its 24 chars
if (!captchaOptions.google.enabled && (!captchaId || captchaId.length !== 24)) {
if (captchaOptions.type !== 'google'
&& (!captchaId || captchaId.length !== 24)) {
throw 'Captcha expired';
}
if (!captchaOptions.google.enabled) { //using builtin captcha
// try to get the captcha from the DB
const captchaMongoId = ObjectId(captchaId);
captchaInput = Array.isArray(captchaInput) ? captchaInput : [captchaInput];
const normalisedAnswer = new Array(captchaOptions.gridSize**2).fill(false);
captchaInput.forEach(num => {
normalisedAnswer[+num] = true;
});
let captcha = await Captchas.findOneAndDelete(captchaMongoId, normalisedAnswer);
//check that it exists and matches captcha in DB
if (!captcha || !captcha.value
|| !timingSafeEqual(Buffer.from(captcha.value.answer.join(',')), Buffer.from(normalisedAnswer.join(',')))) {
throw 'Incorrect captcha answer';
}
} else { //using google recaptcha
//get a response from google
let recaptchaResponse;
try {
const form = new FormData();
form.append('secret', captchaOptions.google.secretKey);
form.append('response', captchaInput);
recaptchaResponse = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
body: form,
}).then(res => res.json());
} catch (e) {
throw 'Captcha error occurred';
//no special error, user will jsut get captcha failed error
}
if (!recaptchaResponse || !recaptchaResponse.success) {
throw 'Incorrect captcha answer';
}
captchaInput = Array.isArray(captchaInput) ? captchaInput : [captchaInput];
switch (captchaOptions.type) {
case 'grid': //grid captcha
const gridCaptchaMongoId = ObjectId(captchaId);
const normalisedAnswer = new Array(captchaOptions.gridSize**2).fill(false);
captchaInput.forEach(num => {
normalisedAnswer[+num] = true;
});
let gridCaptcha = await Captchas.findOneAndDelete(gridCaptchaMongoId, normalisedAnswer);
if (!gridCaptcha || !gridCaptcha.value
|| !timingSafeEqual(
Buffer.from(gridCaptcha.value.answer.join(',')),
Buffer.from(normalisedAnswer.join(','))
)
) {
throw 'Incorrect captcha answer';
}
break;
case 'text': //text captcha
const textCaptchaMongoId = ObjectId(captchaId);
let textCaptcha = await Captchas.findOneAndDelete(textCaptchaMongoId, captchaInput[0]);
if (!textCaptcha || !textCaptcha.value
|| !timingSafeEqual(
Buffer.from(textCaptcha.value.answer),
Buffer.from(captchaInput[0])
)
) {
throw 'Incorrect captcha answer';
}
break;
case 'google': //google captcha
let recaptchaResponse;
try {
const form = new FormData();
form.append('secret', captchaOptions.google.secretKey);
form.append('response', captchaInput[0]);
recaptchaResponse = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
body: form,
}).then(res => res.json());
} catch (e) {
throw 'Captcha error occurred';
}
if (!recaptchaResponse || !recaptchaResponse.success) {
throw 'Incorrect captcha answer';
}
break;
default:
throw 'Captcha config error';
break;
}
return true;

@ -11,10 +11,7 @@ const { enableUserBoardCreation, enableUserAccountCreation,
, uploadDirectory = require(__dirname+'/files/uploadDirectory.js')
, redlock = require(__dirname+'/../redlock.js')
, templateDirectory = path.join(__dirname+'/../views/pages/')
module.exports = async (htmlName, templateName, options, json=null) => {
const html = pug.renderFile(`${templateDirectory}${templateName}`, {
...options,
, renderLocals = {
cache: cacheTemplates,
meta,
commit,
@ -25,8 +22,24 @@ module.exports = async (htmlName, templateName, options, json=null) => {
enableUserBoardCreation,
globalLimits,
enableWebring,
googleRecaptchaEnabled: captchaOptions.google.enabled,
googleRecaptchaSiteKey: captchaOptions.google.siteKey,
captchaType: captchaOptions.type
}
switch (captchaOptions.type) {
case 'google':
renderLocals.googleRecaptchaSiteKey = captchaOptions.google.siteKey;
break;
case 'grid':
renderLocals.captchaGridSize = captchaOptions.gridSize;
break;
default:
break;
}
module.exports = async (htmlName, templateName, options, json=null) => {
const html = pug.renderFile(`${templateDirectory}${templateName}`, {
...options,
...renderLocals,
});
const lock = await redlock.lock(`locks:${htmlName}`, lockWait);
const htmlPromise = outputFile(`${uploadDirectory}/html/${htmlName}`, html);

@ -1,8 +1,8 @@
'use strict';
const { Ratelimits } = require(__dirname+'/../../db/')
, generateCaptcha = require(__dirname+'/../../helpers/captcha/captchagenerate.js')
, { secureCookies, rateLimitCost } = require(__dirname+'/../../configs/main.js')
, { secureCookies, rateLimitCost, captchaOptions } = require(__dirname+'/../../configs/main.js')
, generateCaptcha = require(__dirname+`/../../helpers/captcha/generators/${captchaOptions.type}.js`)
, production = process.env.NODE_ENV === 'production';
module.exports = async (req, res, next) => {
@ -19,7 +19,7 @@ module.exports = async (req, res, next) => {
return res.status(429).redirect('/file/ratelimit.png');
}
}
const { id, text } = await generateCaptcha();
const { id } = await generateCaptcha();
captchaId = id;
} catch (err) {
return next(err);

@ -67,8 +67,6 @@ 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;
@ -76,6 +74,17 @@ const express = require('express')
app.locals.enableWebring = enableWebring;
app.locals.commit = commit;
app.locals.meta = meta;
app.locals.captchaType = captchaOptions.type;
switch (captchaOptions.type) {
case 'google':
app.locals.googleRecaptchaSiteKey = captchaOptions.google.siteKey;
break;
case 'grid':
app.locals.captchaGridSize = captchaOptions.gridSize;
break;
default:
break;
}
// routes
if (!production) {

@ -23,6 +23,5 @@ details.toggle-label#actionform
input#report(type='text', name='report_reason', placeholder='Report reason' autocomplete='off')
.actions
h4.no-m-p Captcha:
.catalog
include ./captcha.pug
include ./captcha.pug
input(type='submit', value='submit')

@ -10,5 +10,8 @@ form.form-post(action=`/forms/appeal`, enctype='application/x-www-form-urlencode
.row
.label Message
textarea(rows='5' name='message' required)
include ./captchafieldrow.pug
if captchaType === 'text'
include ./captchasidelabel.pug
else
include ./captchafieldrow.pug
input(type='submit', value='submit')

@ -1,12 +1,19 @@
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='150' height='150' scrolling='no' loading='lazy')
.jsonly.captcha(style='display:none')
.captchafield.noselect
- for(let i = 0; i < 16; i++) {
label.captchachecklabel
input(type='checkbox' name='captcha' value=i)
span.captchacheckbox
- }
case captchaType
when 'google'
div(class="g-recaptcha" data-sitekey=`${googleRecaptchaSiteKey}` data-theme="dark" data-size="compact" data-callback="recaptchaCallback")
when 'text'
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')
when 'grid'
.catalog
noscript.no-m-p
iframe.captcha(src='/captcha.html' width='150' height='150' scrolling='no' loading='lazy')
.jsonly.captcha(style='display:none')
.captchafield.noselect
- for(let i = 0; i < captchaGridSize**2; i++) {
label.captchachecklabel
input(type='checkbox' name='captcha' value=i)
span.captchacheckbox
- }

@ -1,5 +1,4 @@
details.row.label.mr-0
summary.pv-5 Captcha
span.required *
.catalog
include ./captcha.pug
include ./captcha.pug

@ -1,5 +1,4 @@
.row.label.mr-0
.pv-5 Captcha
span.required *
.catalog
include ./captcha.pug
include ./captcha.pug

@ -0,0 +1,6 @@
section.row
.label
span Captcha
span.required *
.col
include ./captcha.pug

@ -18,5 +18,5 @@ link#codetheme(rel='stylesheet' data-theme=codeTheme href=`/css/codethemes/${cod
include ./favicon.pug
script(src=`/js/all.js?v=${commit}`)
if googleRecaptchaEnabled
if captchaType === 'google'
script(src="https://www.google.com/recaptcha/api.js" async defer)

@ -65,6 +65,9 @@ section.form-wrapper.flex-center
.label Password
input(type='password', name='postpassword', placeholder='Password to delete/spoiler/unlink later' maxlength='50')
if ((board.settings.captchaMode === 1 && !isThread) || board.settings.captchaMode === 2) && !modview
include ./captchaexpand.pug
if captchaType === 'text'
include ./captchasidelabel.pug
else
include ./captchaexpand.pug
input#submitpost(type='submit', value=`New ${isThread ? 'Reply' : 'Thread'}`)
a.toggle-summary.no-decoration(href='#postform') [New Post]

@ -9,8 +9,7 @@ block content
if message
p.title #{message}
form.form-post(action='/forms/blockbypass' method='POST' data-captcha-preload='true')
.catalog
include ../includes/captcha.pug
include ../includes/captcha.pug
if minimal
input(type='hidden' name='minimal' value='1')
input(type='submit', value='Submit')

@ -19,7 +19,10 @@ block content
.row
.label Confirm New Password
input(type='password', name='newpasswordconfirm', maxlength='100' required)
include ../includes/captchafieldrow.pug
if captchaType === 'text'
include ../includes/captchasidelabel.pug
else
include ../includes/captchafieldrow.pug
input(type='submit', value='Change Password')
p: a(href='/login.html') Login
if enableUserAccountCreation

@ -19,6 +19,9 @@ block content
.row
.label Tags
textarea(name='tags' placeholder='Newline separated, max 10')
include ../includes/captchafieldrow.pug
if captchaType === 'text'
include ../includes/captchasidelabel.pug
else
include ../includes/captchafieldrow.pug
input(type='submit', value='submit')

@ -16,7 +16,10 @@ block content
.row
.label Confirm Password
input(type='password', name='passwordconfirm', maxlength='100' required)
include ../includes/captchafieldrow.pug
if captchaType === 'text'
include ../includes/captchasidelabel.pug
else
include ../includes/captchafieldrow.pug
input(type='submit', value='Register')
p: a(href='/login.html') Login
p: a(href='/changepassword.html') Change Password

Loading…
Cancel
Save