From a7467a511a5dfa1849b28e13b7ff04f84d339b99 Mon Sep 17 00:00:00 2001 From: "A. Vatsaev" Date: Wed, 7 Sep 2016 12:46:38 +0200 Subject: [PATCH] Implement reCAPTCHA for abuse prevention --- Dockerfile | 2 +- Procfile | 2 +- app.json | 8 +++++ bin/slackin | 11 ++++-- lib/assets/client.js | 6 ++-- lib/index.js | 83 +++++++++++++++++++++++++++++++++++++------- lib/splash.js | 4 ++- 7 files changed, 96 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index f642b402..7e7a97e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,4 +10,4 @@ RUN npm install --unsafe-perm EXPOSE 3000 -CMD ./bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN +CMD ./bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN $GOOGLE_CAPTCHA_SECRET $GOOGLE_CAPTCHA_SITEKEY diff --git a/Procfile b/Procfile index 4ef841b6..618b7fb5 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN +web: bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN $GOOGLE_CAPTCHA_SECRET $GOOGLE_CAPTCHA_SITEKEY diff --git a/app.json b/app.json index f82aeddd..4059674d 100644 --- a/app.json +++ b/app.json @@ -13,6 +13,14 @@ "description": "A Slack API token (find it on https://api.slack.com/web)", "required": true }, + "GOOGLE_CAPTCHA_SECRET": { + "description": "Google captcha secret key", + "required": true + }, + "GOOGLE_CAPTCHA_SITEKEY": { + "description": "Google captcha site key", + "required": true + }, "SLACK_COC": { "description": "A URL to a Code of Conduct people must agree on before joining.", "required": false diff --git a/bin/slackin b/bin/slackin index f6b896b7..be0708b4 100755 --- a/bin/slackin +++ b/bin/slackin @@ -17,7 +17,7 @@ args .option(['?', 'help'], 'Show the usage information') var flags = args.parse(process.argv, { - value: ' ', + value: ' ', help: false }) @@ -25,16 +25,23 @@ var org = args.sub[0] || process.env.SLACK_SUBDOMAIN var token = args.sub[1] || process.env.SLACK_API_TOKEN var emails = process.env.EMAIL_SLACK_LIST || '' +var gcaptcha_secret = args.sub[2] || process.env.GOOGLE_CAPTCHA_SECRET +var gcaptcha_sitekey = args.sub[3] || process.env.GOOGLE_CAPTCHA_SITEKEY + + + if (flags.help) { args.showHelp() } -if (!org || !token) { +if (!org || !token || !gcaptcha_sitekey || !gcaptcha_secret) { args.showHelp() } else { flags.org = org flags.token = token flags.emails = emails + flags.gcaptcha_secret = gcaptcha_secret + flags.gcaptcha_sitekey = gcaptcha_sitekey } var port = flags.port diff --git a/lib/assets/client.js b/lib/assets/client.js index fe7f14b4..9b86107a 100644 --- a/lib/assets/client.js +++ b/lib/assets/client.js @@ -19,7 +19,8 @@ body.addEventListener('submit', function (ev){ button.disabled = true button.className = '' button.innerHTML = 'Please Wait' - invite(channel ? channel.value : null, coc && coc.checked ? 1 : 0, email.value, function (err, msg){ + var gcaptcha_response = form.elements['g-recaptcha-response'] + invite(channel ? channel.value : null, coc && coc.checked ? 1 : 0, email.value, gcaptcha_response.value, function (err, msg){ if (err) { button.removeAttribute('disabled') button.className = 'error' @@ -31,10 +32,11 @@ body.addEventListener('submit', function (ev){ }) }) -function invite (channel, coc, email, fn){ +function invite (channel, coc, email, gcaptcha_response_value, fn){ request .post(data.path + 'invite') .send({ + "g-recaptcha-response": gcaptcha_response_value, coc: coc, channel: channel, email: email diff --git a/lib/index.js b/lib/index.js index d970b6b9..864a6998 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,6 +9,7 @@ import { Server as http } from 'http' import remail from 'email-regex' import dom from 'vd' import cors from 'cors' +import request from 'superagent'; // our code import Slack from './slack' @@ -22,6 +23,8 @@ export default function slackin ({ token, interval = 5000, // jshint ignore:line org, + gcaptcha_secret, + gcaptcha_sitekey, css, coc, cors: useCors = false, @@ -33,6 +36,8 @@ export default function slackin ({ // must haves if (!token) throw new Error('Must provide a `token`.') if (!org) throw new Error('Must provide an `org`.') + if (!gcaptcha_secret) throw new Error('Must provide a `gcaptcha_secret`.') + if (!gcaptcha_sitekey) throw new Error('Must provide an `gcaptcha_sitekey`.') if (channels) { // convert to an array @@ -84,11 +89,12 @@ export default function slackin ({ dom('title', 'Join ', name, ' on Slack!' ), + dom("script src=https://www.google.com/recaptcha/api.js"), dom('meta name=viewport content="width=device-width,initial-scale=1.0,minimum-scale=1.0,user-scalable=no"'), dom('link rel="shortcut icon" href=https://slack.global.ssl.fastly.net/272a/img/icons/favicon-32.png'), css && dom('link rel=stylesheet', { href: css }) ), - splash({ coc, path, css, name, org, logo, channels, active, total }) + splash({ coc, path, css, name, org, logo, channels, active, total, gcaptcha_sitekey}) ) res.type('html') res.send(page.toHTML()) @@ -130,6 +136,7 @@ export default function slackin ({ } let email = req.body.email + let captcha_response = req.body['g-recaptcha-response']; if (!email) { return res @@ -137,6 +144,12 @@ export default function slackin ({ .json({ msg: 'No email provided' }) } + if(captcha_response == undefined || !captcha_response.length){ + return res + .status(400) + .send({ msg: 'Invalid captcha' }); + } + if (!remail().test(email)) { return res .status(400) @@ -156,23 +169,67 @@ export default function slackin ({ .json({ msg: 'Agreement to CoC is mandatory' }) } - invite({ token, org, email, channel: chanId }, err => { - if (err) { - if (err.message === `Sending you to Slack...`) { - return res - .status(303) - .json({ msg: err.message, redirectUrl: `https://${org}.slack.com` }) - } + ///////////////////////////////////////////////////////////////////////// + + const captcha_data = { + secret: gcaptcha_secret, + response: captcha_response, + remoteip: req.connection.remoteAddress + } + + + const captcha_callback = (err, resp) => { + + if (err) { return res .status(400) - .json({ msg: err.message }) + .send({ msg: err }); + + }else{ + + if(resp.body.success){ + + let chanId = slack.channel ? slack.channel.id : null; + + invite({ token, org, email, channel: chanId }, err => { + if (err) { + if (err.message === `Sending you to Slack...`) { + return res + .status(303) + .json({ msg: err.message, redirectUrl: `https://${org}.slack.com` }) + } + + return res + .status(400) + .json({ msg: err.message }) + } + + res + .status(200) + .json({ msg: 'WOOT. Check your email!' }) + }); + + }else{ + + if (err) { + return res + .status(400) + .send({ msg: "Captcha check failed" }); + } + } + } - res - .status(200) - .json({ msg: 'WOOT. Check your email!' }) - }) + } + + + request.post('https://www.google.com/recaptcha/api/siteverify') + .type('form') + .send(captcha_data) + .end(captcha_callback); + + }) // iframe diff --git a/lib/splash.js b/lib/splash.js index bae0af27..91d11c55 100644 --- a/lib/splash.js +++ b/lib/splash.js @@ -1,6 +1,6 @@ import dom from 'vd' -export default function splash ({ path, name, org, coc, logo, active, total, channels, large, iframe }){ +export default function splash ({ path, name, org, coc, logo, active, total, channels, large, iframe, gcaptcha_sitekey }){ let div = dom('.splash', !iframe && dom('.logos', logo && dom('.logo.org'), @@ -34,6 +34,8 @@ export default function splash ({ path, name, org, coc, logo, active, total, cha ), dom('input.form-item type=email name=email placeholder=you@yourdomain.com ' + (!iframe ? 'autofocus' : '')), + dom('br'), + dom(`div class="g-recaptcha" data-sitekey="${gcaptcha_sitekey}"`), coc && dom('.coc', dom('label', dom('input type=checkbox name=coc value=1'),