From 8a0160a924872b33318db05495ffa4c841ceb0ed Mon Sep 17 00:00:00 2001 From: fatchan Date: Wed, 24 Apr 2019 11:26:14 +0000 Subject: [PATCH] early captchas --- .gitignore | 2 + controllers/forms.js | 20 ++++---- controllers/pages.js | 14 ++++-- db/captchas.js | 25 ++++++++++ gulp/res/css/style.css | 26 ++++++---- helpers/{captcha.js => captchagenerate.js} | 8 ++-- helpers/captchaverify.js | 47 +++++++++++++++++++ ...{number-converter.js => paramconverter.js} | 0 models/pages/banners.js | 27 +++++++++++ models/pages/captcha.js | 32 +++++++++++++ server.js | 2 +- views/includes/actionfooter.pug | 4 ++ views/includes/boardheader.pug | 2 +- views/includes/postform.pug | 4 +- views/mixins/catalogtile.pug | 2 + views/pages/message.pug | 6 ++- wipe.js | 5 ++ 17 files changed, 196 insertions(+), 30 deletions(-) create mode 100644 db/captchas.js rename helpers/{captcha.js => captchagenerate.js} (79%) create mode 100644 helpers/captchaverify.js rename helpers/{number-converter.js => paramconverter.js} (100%) create mode 100644 models/pages/banners.js create mode 100644 models/pages/captcha.js diff --git a/.gitignore b/.gitignore index 65b7e5e7..0354a455 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules/ configs/*.json uploads/img/* +uploads/captcha/* gulp/dist/ +tmp/ diff --git a/controllers/forms.js b/controllers/forms.js index 4c5c67c8..1cabffee 100644 --- a/controllers/forms.js +++ b/controllers/forms.js @@ -4,6 +4,7 @@ const express = require('express') , router = express.Router() , Boards = require(__dirname+'/../db/boards.js') , Posts = require(__dirname+'/../db/posts.js') + , Captchas = require(__dirname+'/../db/captchas.js') , Trips = require(__dirname+'/../db/trips.js') , Bans = require(__dirname+'/../db/bans.js') , Mongo = require(__dirname+'/../db/db.js') @@ -27,8 +28,9 @@ const express = require('express') , registerAccount = require(__dirname+'/../models/forms/register.js') , checkPermsMiddleware = require(__dirname+'/../helpers/haspermsmiddleware.js') , checkPerms = require(__dirname+'/../helpers/hasperms.js') - , numberConverter = require(__dirname+'/../helpers/number-converter.js') + , paramConverter = require(__dirname+'/../helpers/paramconverter.js') , banCheck = require(__dirname+'/../helpers/bancheck.js') + , verifyCaptcha = require(__dirname+'/../helpers/captchaverify.js') , actionChecker = require(__dirname+'/../helpers/actionchecker.js'); // login to account @@ -117,7 +119,7 @@ router.post('/changepassword', async (req, res, next) => { }); //register account -router.post('/register', (req, res, next) => { +router.post('/register', verifyCaptcha, (req, res, next) => { const errors = []; @@ -159,7 +161,7 @@ router.post('/register', (req, res, next) => { }); // make new post -router.post('/board/:board/post', Boards.exists, banCheck, numberConverter, async (req, res, next) => { +router.post('/board/:board/post', Boards.exists, banCheck, paramConverter, verifyCaptcha, async (req, res, next) => { let numFiles = 0; if (req.files && req.files.file) { @@ -208,7 +210,7 @@ router.post('/board/:board/post', Boards.exists, banCheck, numberConverter, asyn }); //upload banners -router.post('/board/:board/addbanners', Boards.exists, banCheck, checkPermsMiddleware, numberConverter, async (req, res, next) => { +router.post('/board/:board/addbanners', Boards.exists, banCheck, checkPermsMiddleware, paramConverter, async (req, res, next) => { let numFiles = 0; if (req.files && req.files.file) { @@ -244,7 +246,7 @@ router.post('/board/:board/addbanners', Boards.exists, banCheck, checkPermsMiddl }); //delete banners -router.post('/board/:board/deletebanners', Boards.exists, banCheck, checkPermsMiddleware, numberConverter, async (req, res, next) => { +router.post('/board/:board/deletebanners', Boards.exists, banCheck, checkPermsMiddleware, paramConverter, async (req, res, next) => { const errors = []; @@ -280,7 +282,7 @@ router.post('/board/:board/deletebanners', Boards.exists, banCheck, checkPermsMi }); //report/delete/spoiler/ban -router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, async (req, res, next) => { +router.post('/board/:board/actions', Boards.exists, banCheck, paramConverter, verifyCaptcha, async (req, res, next) => { const errors = []; @@ -494,7 +496,7 @@ router.post('/board/:board/actions', Boards.exists, banCheck, numberConverter, a }); //unban -router.post('/board/:board/unban', Boards.exists, banCheck, checkPermsMiddleware, numberConverter, async (req, res, next) => { +router.post('/board/:board/unban', Boards.exists, banCheck, checkPermsMiddleware, paramConverter, async (req, res, next) => { //keep this for later in case i add other options to unbans const errors = []; @@ -526,7 +528,7 @@ router.post('/board/:board/unban', Boards.exists, banCheck, checkPermsMiddleware }); -router.post('/global/actions', checkPermsMiddleware, numberConverter, async(req, res, next) => { +router.post('/global/actions', checkPermsMiddleware, paramConverter, async(req, res, next) => { const errors = []; @@ -640,7 +642,7 @@ router.post('/global/actions', checkPermsMiddleware, numberConverter, async(req, }); -router.post('/global/unban', checkPermsMiddleware, numberConverter, async(req, res, next) => { +router.post('/global/unban', checkPermsMiddleware, paramConverter, async(req, res, next) => { const errors = []; diff --git a/controllers/pages.js b/controllers/pages.js index 1a82117f..49e96453 100644 --- a/controllers/pages.js +++ b/controllers/pages.js @@ -5,7 +5,7 @@ const express = require('express') , Boards = require(__dirname+'/../db/boards.js') , hasPerms = require(__dirname+'/../helpers/haspermsmiddleware.js') , isLoggedIn = require(__dirname+'/../helpers/isloggedin.js') - , numberConverter = require(__dirname+'/../helpers/number-converter.js') + , paramConverter = require(__dirname+'/../helpers/paramconverter.js') //page models , home = require(__dirname+'/../models/pages/home.js') , register = require(__dirname+'/../models/pages/register.js') @@ -15,6 +15,8 @@ const express = require('express') , login = require(__dirname+'/../models/pages/login.js') , board = require(__dirname+'/../models/pages/board.js') , catalog = require(__dirname+'/../models/pages/catalog.js') + , banners = require(__dirname+'/../models/pages/banners.js') + , captcha = require(__dirname+'/../models/pages/captcha.js') , thread = require(__dirname+'/../models/pages/thread.js'); //homepage with board list @@ -42,6 +44,12 @@ router.get('/logout', isLoggedIn, (req, res, next) => { }); +// get captcha +router.get('/captcha', captcha); + +// random board banner +router.get('/banners', banners); + //board manage page router.get('/:board/manage', Boards.exists, isLoggedIn, hasPerms, manage); @@ -49,10 +57,10 @@ router.get('/:board/manage', Boards.exists, isLoggedIn, hasPerms, manage); router.get('/globalmanage', isLoggedIn, hasPerms, globalManage); // board page/recents -router.get('/:board', Boards.exists, numberConverter, board); +router.get('/:board', Boards.exists, paramConverter, board); // thread view page -router.get('/:board/thread/:id(\\d+)', Boards.exists, numberConverter, thread); +router.get('/:board/thread/:id(\\d+)', Boards.exists, paramConverter, thread); // board catalog page router.get('/:board/catalog', Boards.exists, catalog); diff --git a/db/captchas.js b/db/captchas.js new file mode 100644 index 00000000..f5fb82b5 --- /dev/null +++ b/db/captchas.js @@ -0,0 +1,25 @@ +'use strict'; + +const Mongo = require(__dirname+'/db.js') + , db = Mongo.client.db('jschan').collection('captchas'); + +module.exports = { + + db, + + findOne: (id) => { + return db.findOne({ '_id': id }); + }, + + insertOne: (text) => { + return db.insertOne({ + 'text': text, + 'expireAt': new Date((new Date).getTime() + (5*1000*60)) //5 minute expiration + }); + }, + + deleteAll: () => { + return db.deleteMany({}); + }, + +} diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index 479ef502..f7847fae 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -41,6 +41,8 @@ object { align-items: center; } + + .catalog-tile-button { width: 100%; line-height: 30px; @@ -150,9 +152,7 @@ span { .post-container, .pages, .toggle-label { background: #D6DAF0; - border-color: #B7C5D9; - border-width: 0 1px 1px 0; - border-style: none solid solid none; + border: 1px solid #B7C5D9; } .actions { @@ -392,7 +392,10 @@ input[type="text"], input[type="submit"], input[type="password"], input[type="fi margin: 0; flex-grow: 1; border-radius: 0px; - min-height: 29px; +} + +input[type="submit"] { + min-height: 30px; } input[type="file"] { @@ -406,6 +409,17 @@ input[type="file"] { max-width: 100%; } +.postform-data { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.captcha { + margin: auto; + margin-bottom: 1px; +} + .postform-label { padding: 3px; border: 1px solid black; @@ -424,8 +438,6 @@ input[type="file"] { hr { color: lightgray; - /*border-top: 1px solid black; - background: lightgray;*/ } @media only screen and (max-width: 800px) { @@ -454,7 +466,6 @@ hr { .post-container { width: 100%; - border: none; } .catalog-tile { @@ -467,7 +478,6 @@ hr { .post-info { background-color: #B7C5D9; - /*width: 100%;*/ } } diff --git a/helpers/captcha.js b/helpers/captchagenerate.js similarity index 79% rename from helpers/captcha.js rename to helpers/captchagenerate.js index 2aae174e..0fe85136 100644 --- a/helpers/captcha.js +++ b/helpers/captchagenerate.js @@ -1,6 +1,4 @@ const gm = require('@tohru/gm') - , crypto = require('crypto') - , fs = require('fs') , rr = (min, max) => Math.floor(Math.random() * (max-min + 1) + min) , width = 200 , height = 80; @@ -14,9 +12,9 @@ function getShape() { return { x1, x2, y1, y2 }; } -module.exports = () => { +module.exports = (text, captchaId) => { return new Promise((resolve, reject) => { - const text = crypto.randomBytes(20).toString('hex').substring(0,6).split('') + text = text.split(''); //array of chars const x = gm(200, 80, '#fff') .fill('#000') .fontSize(80) @@ -31,7 +29,7 @@ module.exports = () => { x.wave(10, rr(50,80)) .blur(1, 2) .crop(200, 80, 0, 0) - .write('./static/img/captcha.jpg', (err) => { + .write(`./uploads/captcha/${captchaId}.png`, (err) => { if (err) { return reject(); } diff --git a/helpers/captchaverify.js b/helpers/captchaverify.js new file mode 100644 index 00000000..ed190dec --- /dev/null +++ b/helpers/captchaverify.js @@ -0,0 +1,47 @@ +'use strict'; + +const Captchas = require(__dirname+'/../db/captchas.js') + , Mongo = require(__dirname+'/../db/db.js'); + +module.exports = async (req, res, next) => { + + //check if captcha field in form is valid + const input = req.body.captcha; + if (!input || input.length !== 6) { + return res.status(403).render('message', { + 'title': 'Forbidden', + 'message': 'Incorrect captcha' + }); + } + + //make sure they have captcha cookie and its 24 chars + const captchaId = req.cookies.captchaid; + if (!captchaId || captchaId.length !== 24) { + return res.status(403).render('message', { + 'title': 'Forbidden', + 'message': 'Captcha expired' + }); + } + + // try to get the captcha from the DB + let captcha; + try { + const captchaMongoId = Mongo.ObjectId(captchaId); + captcha = await Captchas.findOne(captchaMongoId); + } catch (err) { + return next(err); + } + + //check that it exists and matches captcha in DB + if (!captcha || captcha.text !== input) { + return res.status(403).render('message', { + 'title': 'Forbidden', + 'message': 'Incorrect captcha' + }); + } + + //it was correct, so continue + res.clearCookie('captchaid'); + return next(); + +} diff --git a/helpers/number-converter.js b/helpers/paramconverter.js similarity index 100% rename from helpers/number-converter.js rename to helpers/paramconverter.js diff --git a/models/pages/banners.js b/models/pages/banners.js new file mode 100644 index 00000000..928bb363 --- /dev/null +++ b/models/pages/banners.js @@ -0,0 +1,27 @@ +'use strict'; + +const Boards = require(__dirname+'/../../db/boards.js'); + +module.exports = async (req, res, next) => { + + if (!req.query.board) { + return next(); + } + + // get all threads + let board; + try { + board = await Boards.findOne(req.query.board); + } catch (err) { + return next(err); + } + + if (!board) { + return next(); + } + + const randomBanner = board.banners[Math.floor(Math.random()*board.banners.length)]; + + return res.redirect(`/img/${randomBanner}`); + +} diff --git a/models/pages/captcha.js b/models/pages/captcha.js new file mode 100644 index 00000000..32aaee14 --- /dev/null +++ b/models/pages/captcha.js @@ -0,0 +1,32 @@ +'use strict'; + +const crypto = require('crypto') + , Captchas = require(__dirname+'/../../db/captchas.js') + , generateCaptcha = require(__dirname+'/../../helpers/captchagenerate.js'); + +module.exports = async (req, res, next) => { + + //will move captcha cookie check to nginx at some point + if (req.cookies.captchaid) { + return res.redirect(`/captcha/${req.cookies.captchaid}.png`); + } + + // if we got here, they dont have a cookie so we need to + // gen a captcha, set their cookie and redirect to the captcha + const text = crypto.randomBytes(20).toString('hex').substring(0,6); + let captchaId; + try { + captchaId = await Captchas.insertOne(text).then(r => r.insertedId); //get id of document as filename and captchaid + await generateCaptcha(text, captchaId); + } catch (err) { + return next(err); + } + + return res + .cookie('captchaid', captchaId, { + 'maxAge': 5*60*1000, //5 minute cookie + 'httpOnly': true + }) + .redirect(`/captcha/${captchaId}.png`); + +} diff --git a/server.js b/server.js index 1bc79799..e9e16ac8 100644 --- a/server.js +++ b/server.js @@ -53,7 +53,7 @@ const express = require('express') // use pug view engine app.set('view engine', 'pug'); app.set('views', path.join(__dirname, 'views/pages')); - app.enable('view cache'); +// app.enable('view cache'); // routes app.use('/forms', require(__dirname+'/controllers/forms.js')) diff --git a/views/includes/actionfooter.pug b/views/includes/actionfooter.pug index 40f3fb80..ee79bb4d 100644 --- a/views/includes/actionfooter.pug +++ b/views/includes/actionfooter.pug @@ -44,4 +44,8 @@ label.toggle-label Toggle Post Actions | Show Post In Ban label input#report(type='text', name='ban_reason', placeholder='ban reason' autocomplete='off') + .actions + h4.no-m-p Captcha: + img.captcha(src='/captcha' width=200 height=80) + input#captcha(type='text', name='captcha', autocomplete='off' placeholder='captcha text' maxlength='6') input(type='submit', value='submit') diff --git a/views/includes/boardheader.pug b/views/includes/boardheader.pug index 31fdfd64..6f3d3492 100644 --- a/views/includes/boardheader.pug +++ b/views/includes/boardheader.pug @@ -1,6 +1,6 @@ section.board-header if board.banners.length > 0 - object.board-banner(data=`/img/${board.banners[Math.floor(Math.random()*board.banners.length)]}` width='300' height='100') + object.board-banner(data=`/banners?board=${board._id}` width='300' height='100') a.no-decoration(href=`/${board._id}`) h1.board-title /#{board._id}/ - #{board.name} h4.board-description #{board.description} diff --git a/views/includes/postform.pug b/views/includes/postform.pug index b83f5248..56af0a6b 100644 --- a/views/includes/postform.pug +++ b/views/includes/postform.pug @@ -26,7 +26,9 @@ section.form-wrapper | Spoiler section.postform-section .postform-label Captcha - input#captcha(type='text', name='captcha', autocomplete='off' placeholder='under construction' maxlength='6') + span.postform-data + img.captcha(src='/captcha' width=200 height=80) + input#captcha(type='text', name='captcha', autocomplete='off' placeholder='captcha text' maxlength='6') input(type='submit', value='submit') diff --git a/views/mixins/catalogtile.pug b/views/mixins/catalogtile.pug index 440bab07..22378fa2 100644 --- a/views/mixins/catalogtile.pug +++ b/views/mixins/catalogtile.pug @@ -14,7 +14,9 @@ mixin catalogtile(board, post, truncate) object.catalog-thumb(data=`/img/thumb-${post.files[0].filename.split('.')[0]}.png` width='64' height='64') header.post-info span: a(href=postURL) ##{post.postId} + | span Replies: #{post.replyposts} + | span Images: #{post.replyfiles} br if post.message diff --git a/views/pages/message.pug b/views/pages/message.pug index a5fa5e21..03ef1a05 100644 --- a/views/pages/message.pug +++ b/views/pages/message.pug @@ -1,7 +1,8 @@ extends ../layout.pug block head - meta(http-equiv="refresh" content=`3;url=${redirect}`) + if redirect + meta(http-equiv="refresh" content=`3;url=${redirect}`) block content h1 #{title} @@ -16,4 +17,5 @@ block content if errors each error in errors li #{error} - p You will be redirected shortly. If you are not redirected automatically, you can #[a(href=redirect) click here]. + if redirect + p You will be redirected shortly. If you are not redirected automatically, you can #[a(href=redirect) click here]. diff --git a/wipe.js b/wipe.js index 8f2e6b15..9e16d2e5 100644 --- a/wipe.js +++ b/wipe.js @@ -14,7 +14,10 @@ const Mongo = require(__dirname+'/db/db.js') , Posts = require(__dirname+'/db/posts.js') , Bans = require(__dirname+'/db/bans.js') , Trips = require(__dirname+'/db/trips.js') + , Captchas = require(__dirname+'/db/captchas.js') , Accounts = require(__dirname+'/db/accounts.js'); + console.log('deleting captchas') + await Captchas.deleteAll(); console.log('deleting accounts') await Accounts.deleteAll(); console.log('deleting posts') @@ -47,6 +50,8 @@ const Mongo = require(__dirname+'/db/db.js') console.log('creating indexes') await Bans.db.dropIndexes(); await Bans.db.createIndex({ "expireAt": 1 }, { expireAfterSeconds: 0 }); + await Captchas.db.dropIndexes(); + await Captchas.db.createIndex({ "expireAt": 1 }, { expireAfterSeconds: 0 }); await Posts.db.dropIndexes(); //these are fucked await Posts.db.createIndex({