diff --git a/controllers/forms/globalsettings.js b/controllers/forms/globalsettings.js index dbe84735..ea1d679f 100644 --- a/controllers/forms/globalsettings.js +++ b/controllers/forms/globalsettings.js @@ -79,7 +79,7 @@ module.exports = { { result: lengthBody(req.body.ip_header, 0, 100), expected: false, error: 'IP header length must not exceed 100 characters' }, { result: lengthBody(req.body.meta_site_name, 0, 100), expected: false, error: 'Meta site name must not exceed 100 characters' }, { result: lengthBody(req.body.meta_url, 0, 100), expected: false, error: 'Meta url must not exceed 100 characters' }, - { result: inArrayBody(req.body.captcha_options_type, ['grid', 'text', 'google', 'hcaptcha']), expected: true, error: 'Invalid captcha options type' }, + { result: inArrayBody(req.body.captcha_options_type, ['grid', 'grid2', 'text', 'google', 'hcaptcha']), expected: true, error: 'Invalid captcha options type' }, { result: numberBody(req.body.captcha_options_generate_limit, 1), expected: true, error: 'Captcha options generate limit must be a number > 0' }, { result: numberBody(req.body.captcha_options_grid_size, 2, 6), expected: true, error: 'Captcha options grid size must be a number from 2-6' }, { result: numberBody(req.body.captcha_options_grid_image_size, 50, 500), expected: true, error: 'Captcha options grid image size must be a number from 50-500' }, diff --git a/gulpfile.js b/gulpfile.js index a70dc370..402382d2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -154,6 +154,9 @@ async function ips() { async function wipe() { const db = Mongo.db; + const defaultConfig = require(__dirname+'/configs/template.js.example'); + await Mongo.setConfig(defaultConfig); + const collectionNames = ['accounts', 'bans', 'custompages', 'boards', 'captcha', 'files', 'modlog','news', 'posts', 'poststats', 'ratelimit', 'bypass', 'roles']; for (const name of collectionNames) { @@ -283,17 +286,28 @@ async function wipe() { async function css() { try { //a little more configurable - let bypassHeight = (config.get.captchaOptions.type === 'google' || config.get.captchaOptions.type === 'hcaptcha') - ? 500 - : config.get.captchaOptions.type === 'grid' - ? 330 - : 235; - let captchaHeight = config.get.captchaOptions.type === 'text' ? 80 - : config.get.captchaOptions.type === 'grid' ? config.get.captchaOptions.grid.imageSize+30 - : 200; //google/hcaptcha doesnt need this set - let captchaWidth = config.get.captchaOptions.type === 'text' ? 210 - : config.get.captchaOptions.type === 'grid' ? config.get.captchaOptions.grid.imageSize+30 - : 200; //google/hcaptcha doesnt need this set + let bypassHeight + , captchaHeight + , captchaWidth; + switch (config.get.captchaOptions.type) { + case 'google': + case 'hcaptcha': + bypassHeight = 500; + captchaHeight = 200; + captchaWidth = 200; + break; + case 'grid': + case 'grid2': + bypassHeight = 330; + captchaHeight = config.get.captchaOptions.grid.imageSize+30; + captchaWidth = config.get.captchaOptions.grid.imageSize+30; + break; + case 'text': + bypassHeight = 235; + captchaHeight = 80; + captchaWidth = 210; + break; + } const cssLocals = `:root { --attachment-img: url('/file/attachment.png'); --spoiler-img: url('/file/spoiler.png'); diff --git a/lib/captcha/captcha.js b/lib/captcha/captcha.js index b5b360c9..32b13e5d 100644 --- a/lib/captcha/captcha.js +++ b/lib/captcha/captcha.js @@ -31,7 +31,8 @@ module.exports = async (captchaInput, captchaId) => { captchaInput = Array.isArray(captchaInput) ? captchaInput : [captchaInput]; switch (captchaOptions.type) { - case 'grid': { //grid captcha + case 'grid': + case 'grid2': { //grid captcha const gridCaptchaMongoId = ObjectId(captchaId); const normalisedAnswer = new Array(captchaOptions.grid.size**2).fill(false); captchaInput.forEach(num => { diff --git a/lib/captcha/generators/grid2.js b/lib/captcha/generators/grid2.js new file mode 100644 index 00000000..06926ad3 --- /dev/null +++ b/lib/captcha/generators/grid2.js @@ -0,0 +1,147 @@ +'use strict'; + +const gm = require('@fatchan/gm').subClass({ imageMagick: true }) + , { promisify } = require('util') + , { Captchas } = require(__dirname+'/../../../db/') + , config = require(__dirname+'/../../misc/config.js') + , uploadDirectory = require(__dirname+'/../../file/uploaddirectory.js') + , getDistorts = require(__dirname+'/../getdistorts.js') + , randomRange = promisify(require('crypto').randomInt) + , randomBytes = promisify(require('crypto').randomBytes) + , padding = 30 //pad edge of image to account for character size + distortion + , nArrows = ['↑', '↟', '↥', '↾', '↿', '⇑', '⇡'] + , eArrows = ['➸', '→', '➳', '➵', '→', '↛', '↠', '↣', '↦', '↪', '↬', '↱', '↳', '⇉', '⇏', '⇒', '⇛', '⇝', '⇢'] + , wArrows = [ '←', '↚', '↞', '↜', '↢', '↩', '↤', '↫', '↰', '↲', '↵', '⇇', '⇍', '⇐', '⇚', '⇜', '⇠'] + , sArrows = ['↓', '↡', '↧', '↴', '⇂', '⇃', '⇊', '⇓', '⇣'] + , allArrows = [...nArrows, ...eArrows, ...wArrows, ...sArrows] + , randomBool = async (p) => { return ((await randomBytes(1))[0] > p); } + , randomOf = async (arr) => { return arr[(await randomRange(0, arr.length))]; }; + //TODO: last two could belong in lib/misc/(random?) + +module.exports = async () => { + + const { captchaOptions } = config.get; + + const { size, trues, falses, imageSize, noise, edge } = captchaOptions.grid; + const width = imageSize+padding; //TODO: these will never be different, right? + const height = imageSize+padding; + + const charMatrix = new Array(size).fill(false) + .map(() => new Array(size).fill(false)); + const answerMatrix = new Array(size).fill(false) + .map(() => new Array(size).fill(false)); + + //put the icon arrows should point at + const correctRow = await randomRange(0, size); + const correctCol = await randomRange(0, size); + charMatrix[correctRow][correctCol] = await randomOf(trues); + + //put correct and incorrect arrows in the row/column + const numArray = [...new Array(size).keys()]; + const perpendicularRows = numArray.filter(x => x !== correctRow); + for (let row of perpendicularRows) { + /*TODO: necessary to memoize these "inverse" sets of arrows? or maybe instead of even doing a 50/50 + random, it should just pick a random from allArrows then set the isCorrect based on if its in the correct set?*/ + let arrows; + const isCorrect = await randomBool(127); + if (row < correctRow) { + arrows = isCorrect ? sArrows : [...nArrows, ...eArrows, ...wArrows]; + } else if (row > correctRow) { + arrows = isCorrect ? nArrows : [...sArrows, ...eArrows, ...wArrows]; + } + charMatrix[row][correctCol] = await randomOf(arrows); + answerMatrix[row][correctCol] = isCorrect; + } + const perpendicularCols = numArray.filter(x => x !== correctCol); + for (let col of perpendicularCols) { + let arrows; + const isCorrect = await randomBool(127); + if (col < correctCol) { + arrows = isCorrect ? eArrows : [...sArrows, ...nArrows, ...wArrows]; + charMatrix[correctRow][col] = await randomOf(arrows); + } else if (col > correctCol) { + arrows = isCorrect ? wArrows : [...sArrows, ...nArrows, ...eArrows]; + charMatrix[correctRow][col] = await randomOf(arrows); + } + charMatrix[correctRow][col] = await randomOf(arrows); + answerMatrix[correctRow][col] = isCorrect; + } +//TODO: diagonals? need more arrows + + //this sucks + if (!answerMatrix.flat().some(x => x === true)) { + if ((await randomBool(127)) === true) { + const randomRow = await randomOf(perpendicularRows); + const arrows = randomRow < correctRow ? sArrows : nArrows; + charMatrix[randomRow][correctCol] = await randomOf(arrows); + answerMatrix[randomRow][correctCol] = true; + } else { + const randomCol = await randomOf(perpendicularCols); + const arrows = randomCol < correctCol ? eArrows : wArrows; + charMatrix[correctRow][randomCol] = await randomOf(arrows); + answerMatrix[correctRow][randomCol] = true; + } + } + + //fill the rest with junk arrows/falses + for (let row = 0; row < size; row++) { + for (let col = 0; col < size; col++) { + if (charMatrix[row][col] === false) { + charMatrix[row][col] = (await randomBool(80)) + ? (await randomOf(allArrows)) + : (await randomOf(falses)); + } + } + } + + const captcha = gm(width, height, '#ffffff') + .fill('#000000') + .font(__dirname+'/../font.ttf'); + + const spaceSize = (width-padding)/size; + for (let row = 0; row < size; row++) { + let cxOffset = Math.floor(spaceSize * 1.5); + for (let col = 0; col < size; col++) { + const cyOffset = captchaOptions.grid.iconYOffset; + captcha.fontSize((await randomRange(20, 30))); + captcha.drawText( + (spaceSize * col) + cyOffset, + (spaceSize * row) + cxOffset, + charMatrix[row][col] + ); + } + } + +//console.log(charMatrix, answerMatrix); + + //insert the captcha to db and get id + const captchaId = await Captchas.insertOne(answerMatrix.flat()).then(r => r.insertedId); + + //create an array of distortions and apply to the image, if distortion is enabled + const { distortion, numDistorts } = captchaOptions; + if (distortion > 0) { + const distorts = await getDistorts(width, height, numDistorts, distortion); + captcha.distort(distorts, 'Shepards'); + } + + //add optional edge effect + if (edge > 0) { + captcha.edge(edge); + } + + //add optional noise effect + if (noise > 0) { + captcha.noise(noise); + } + + return new Promise((resolve, reject) => { + captcha + .write(`${uploadDirectory}/captcha/${captchaId}.jpg`, (err) => { + if (err) { + return reject(err); + } + return resolve({ captchaId }); + }); + }); + +}; diff --git a/models/pages/captcha.js b/models/pages/captcha.js index 1e033b24..aad8ad71 100644 --- a/models/pages/captcha.js +++ b/models/pages/captcha.js @@ -7,7 +7,7 @@ const { Captchas, Ratelimits } = require(__dirname+'/../../db/') module.exports = async (req, res, next) => { const { secureCookies, rateLimitCost, captchaOptions } = config.get; - if (!['grid', 'text'].includes(captchaOptions.type)) { + if (!['text', 'grid', 'grid2'].includes(captchaOptions.type)) { return next(); //only grid and text captcha continue } diff --git a/views/includes/captcha.pug b/views/includes/captcha.pug index 88004654..27a9f4a8 100644 --- a/views/includes/captcha.pug +++ b/views/includes/captcha.pug @@ -11,6 +11,7 @@ case captchaOptions.type .jsonly.captcha(style='display:none;') input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='Captcha text' pattern='.{6}' required title='6 characters') when 'grid' + when 'grid2' span.text-center #{captchaOptions.grid.question} .catalog noscript.no-m-p diff --git a/views/pages/globalmanagesettings.pug b/views/pages/globalmanagesettings.pug index 4002525b..e21c1e58 100644 --- a/views/pages/globalmanagesettings.pug +++ b/views/pages/globalmanagesettings.pug @@ -164,14 +164,14 @@ block content select(name='captcha_options_type') option(value='text', selected=settings.captchaOptions.type === 'text') Text option(value='grid', selected=settings.captchaOptions.type === 'grid') Grid v1 - option(value='grid', selected=settings.captchaOptions.type === 'grid2' disabled=true) Grid v2 (Coming soon) + option(value='grid2', selected=settings.captchaOptions.type === 'grid2') Grid v2 option(value='google', selected=settings.captchaOptions.type === 'google') Google option(value='hcaptcha', selected=settings.captchaOptions.type === 'hcaptcha') Hcaptcha .row .label Generate Limit input(type='number' name='captcha_options_generate_limit' value=settings.captchaOptions.generateLimit) .row - h4.mv-5 Grid v1 Captcha Options + h4.mv-5 Grid Captcha Options .row .label Image Size input(type='number' name='captcha_options_grid_image_size' value=settings.captchaOptions.grid.imageSize)