From f9d7b06b7a5a3d16b4df0addcf2b0534f596ae74 Mon Sep 17 00:00:00 2001 From: Alan Chandler Date: Fri, 31 Jul 2020 18:00:35 +0100 Subject: [PATCH] Add re-captcha element part of #23 --- client/elements/re-captcha.js | 167 +++++++++++++++++++++++++++++ client/modules/globals.js | 8 +- server/config/config.js | 2 +- server/db-init/database.sql | 8 +- server/db-init/upgrade_13.sql | 3 +- server/session/recaptcha_verify.js | 50 +++++++++ 6 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 client/elements/re-captcha.js create mode 100644 server/session/recaptcha_verify.js diff --git a/client/elements/re-captcha.js b/client/elements/re-captcha.js new file mode 100644 index 0000000..fcf38ff --- /dev/null +++ b/client/elements/re-captcha.js @@ -0,0 +1,167 @@ +/** +@licence + Copyright (c) 2020 Alan Chandler, all rights reserved + + This file is part of Football-Mobile. + + Football-Mobile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Football-Mobile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Football-Mobile. If not, see . +*/ +import { LitElement, html } from '../libs/lit-element.js'; +import api from '../modules/api.js'; + +let instance = 1; + +const recaptcha = new Promise(function (resolve, reject) { + window.recaptchaElementCallback = function () { + resolve(window.grecaptcha); //grecaptcha is a global object that the script element I am about to add delivers + }; + + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('async', ''); + script.setAttribute('id', 'grecaptchaLibrary'); + script.setAttribute('defer', ''); + script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=recaptchaElementCallback&render=explicit'); + head.appendChild(script); +}); + + +/* + +*/ +class ReCaptcha extends LitElement { + static get styles() { + return []; + } + static get properties() { + return { + theme: {type: String}, + invalid: {type: Boolean}, //failed to provide capture response + message: {type: String} //Error message to display when invalid + }; + } + constructor() { + super(); + this.theme = 'light'; + this.invalid = false; + this.captureCompleted = false; + this.message = 'Captcha Not Completed'; + this._setLocationOfBodyFloat = this._setLocationOfBodyFloat.bind(this); + this._scroll = this._scroll.bind(this); + this._captured = this._captured.bind(this); + this.isScrolling = false; + this.resizeObserver = new ResizeObserver(this._setLocationOfBodyFloat); + + } + connectedCallback() { + super.connectedCallback(); + + //append an absolute div to the body in which to render our div. + this.float = document.createElement('div'); + const captchaId = `captcha_${instance++}`; + this.float.setAttribute('id', this.captchaId) + this.float.style = 'position:absolute;'; + document.body.appendChild(this.float); + recaptcha.then(grepcaptcha => grepcaptcha.render(this.captureId,{ + 'sitekey': global.recaptchaKey, + 'theme': this.theme, + 'callback': this._captured + })); + if (this.reserver !== undefined) { + this.resizeObserver.observe(this.reserve); + } + let parent = this; + while (parent = parent.parentNode) { + parent.addEventListener('scroll', this._scroll); + } + + } + disconnectedCallback() { + super.disconnectedCallback(); + this.float.remove(); //take our recapture element away + this.isScrolling = false; + this.captureCompleted = false; + this.resizeObserver.unobserve(this.reserve); + let parent = this; + while (parent = parent.parentNode) { + parent.removeEventListener('scroll', this._scroll); + } + } + update(changed) { + super.update(changed); + } + firstUpdated() { + this.reserve = this.shadowRoot.querySelector('#reserve'); + this.resizeObserver.observe(this.reserve); + + } + updated(changed) { + super.updated(changed); + } + render() { + return html` + +
${this.invalid? this.message: ' '}
+ +
+ `; + } + validate() { + if (!this.captureCompleted) { + this.invalid = false; + } + return !this.invalid; + } + _captured(token) { +console.log('captured called with token', token); + //I don't want the client to see my secret key, so I am going to send the token to my server, and it can do the validation + api('session/recaptcha_verify',{token:token}).then(response => { + if (response.success) { + this.captureCompleted = true; + this.invalid = false; + } else { + this.invalid = true; + window.grecaptcha.reset(this.captchaId); + } + }) + } + _scroll() { + if (this.isScrolling) return; + this.isScrolling = true; + requestAnimationFrame(() => { + this._setLocationOfBodyFloat(); + this.isScrolling = false; + }); + + } + _setLocationOfBodyFloat() { + if (this.reserve !== undefined) { + const rect = this.reserve.getBoundingClientRect(); + this.float.style.left = rect.left + 'px'; + this.float.style.top = rect.top + 'px'; + this.float.style.width = rect.width + 'px'; + this.float.style.height = rect.height + 'px'; + } + } +} +customElements.define('re-captcha', ReCaptcha); + + + + diff --git a/client/modules/globals.js b/client/modules/globals.js index d164d3b..ac2c733 100644 --- a/client/modules/globals.js +++ b/client/modules/globals.js @@ -24,12 +24,12 @@ let clientLogUid = 0; let version = 'v0.0.0'; let copyrightYear = '2020'; let cookieName = ''; -let cookieVisitName = ''; let webmaster = ''; let siteLogo = '/appimages/site-logo.png'; let verifyExpires = 12; let minPassLen = 6; let dwellTime = 2000; +let reCaptchaKey = ''; let lrid = 0; let lcid = 0; let luid = 0; @@ -62,12 +62,12 @@ const global = { version = conf.version; copyrightYear = conf.copyrightYear; cookieName = conf.cookieName; - cookieVisitName = conf.cookieVisitName; webmaster = conf.webmaster; siteLogo = conf.siteLogo; verifyExpires = conf.verifyExpires; minPassLen = conf.minPassLen; dwellTime = conf.dwellTime; + reCaptchaKey = conf.reCaptchaKey; lrid = conf.lrid; lcid = conf.lcid; luid = conf.luid; @@ -117,8 +117,8 @@ const global = { get cookieName () { return cookieName; }, - get cookieVisitName () { - return cookieVisitName; + get reCaptchaKey () { + return reCaptchaKey; }, get webmaster () { return webmaster; diff --git a/server/config/config.js b/server/config/config.js index e62d92f..5054345 100644 --- a/server/config/config.js +++ b/server/config/config.js @@ -38,12 +38,12 @@ config.clientLog = s.get('client_log'); config.clientLogUid = s.get('client_log_uid'); config.cookieName = s.get('cookie_name'); - config.cookieVisitName = s.get('cookie_visit_name'); config.webmaster = s.get('webmaster'); config.siteLogo = s.get('site_logo'); config.verifyExpires = s.get('verify_expires'); config.minPassLen = s.get('min_pass_len'); config.dwellTime = s.get('dwell_time'); + config.reCaptchaKey = s.get('recaptcha_key'); const row = last.get(); config.lcid = row.cid; diff --git a/server/db-init/database.sql b/server/db-init/database.sql index 50de140..3c08dac 100644 --- a/server/db-init/database.sql +++ b/server/db-init/database.sql @@ -393,20 +393,19 @@ INSERT INTO settings (name,value) VALUES('version',14); --version of this config INSERT INTO settings (name,value) VALUES('client_log',''); --if none empty string should specify colon separated function areas client should log or 'all' for every thing. INSERT INTO settings (name,value) VALUES('client_log_uid',0); --if non zero limit client logging to that uid. -INSERT INTO settings (name,value) VALUES('cookie_visit_name','FMVISIT'); --name used for a cookie to record a visit where the user logged on. - INSERT INTO settings (name,value) VALUES('main_menu_icon','menu'); --character from material icon font to use as the main menu. INSERT INTO settings (name,value) VALUES('webmaster','webmaster@example.com'); --site webmaster. INSERT INTO settings (name,value) VALUES('site_logo','/appimages/site_logo.png'); --url of the site_logo image to be used on info pages and in mail INSERT INTO settings (name,value) VALUES('min_pass_len', 6); --minimum password length INSERT INTO settings (name,value) VALUES('dwell_time', 2000); --time to elapse before new urls get to be pushed to the history stack - +INSERT INTO settings (name,value) VALUES('recaptcha_key',''); --stardard recaptcha key for the recapcha element --values for server config INSERT INTO settings (name,value) VALUES('cache_age',0);--cache age before invalid (in hours), 0 is infinite INSERT INTO settings (name,value) VALUES('server_port', 2040); --port the api server should listen on. INSERT INTO settings (name,value) VALUES('cookie_name', 'FOOTBALL'); --name used for our main cookie INSERT INTO settings (name,value) VALUES('cookie_key', 'newCookieKey'); --key used to encrypt/decrypt cookie token INSERT INTO settings (name,value) VALUES('cookie_expires', 720); --hours until expire for standard logged on token +INSERT INTO settings (name,value) VALUES('recaptch_secret',''); -- secret key or verification of recaptcha. INSERT INTO settings (name,value) VALUES('verify_expires', 24); --hours until expire for verification tokens. INSERT INTO settings (name,value) VALUES('rate_limit', 30); --minutes that must elapse by verification emails INSERT INTO settings (name,value) VALUES('email_from', 'admin@example.com'); --email address that mail comes from (do not reply) @@ -454,7 +453,8 @@ INSERT INTO styles (name,style) VALUES('pw-input-length','100px'); --input field -- UPDATE settings SET value = '/images/signature.png;Joe Bloggs' WHERE name = 'mail_signature'; --NOTE, site specific images should be in a different directory -- UPDATE settings SET value = '/images/site_logo.png' WHERE name = 'site_logo'; --As above. -- UPDATE settings SET value = 'newCookieKey' WHERE name = 'cookie_key'; - +-- UPDATE settings SET value = 'recaptcha_key' WHERE name = 'recaptcha_key'; +-- UPDATE settings SET value = 'recaptcha_secret_key' WHERE name = 'recaptcha_secret'; COMMIT; VACUUM; -- set it all up as Write Ahead Log for max performance and minimum contention with other users. diff --git a/server/db-init/upgrade_13.sql b/server/db-init/upgrade_13.sql index 181f4a0..371dc92 100644 --- a/server/db-init/upgrade_13.sql +++ b/server/db-init/upgrade_13.sql @@ -236,17 +236,18 @@ INSERT INTO settings (name,value) VALUES('version',14); --version of this config INSERT INTO settings (name,value) VALUES('client_log',''); --if none empty string should specify colon separated function areas client should log or 'all' for every thing. INSERT INTO settings (name,value) VALUES('client_log_uid',0); --if non zero limit client logging to that uid. -INSERT INTO settings (name,value) VALUES('cookie_visit_name','FMVISIT'); --name used for a cookie to record a visit where the user logged on. INSERT INTO settings (name,value) VALUES('webmaster','webmaster@example.com'); --site webmaster. INSERT INTO settings (name,value) VALUES('site_logo','/appimages/site_logo.png'); --url of the site_logo image to be used on info pages and in mail INSERT INTO settings (name,value) VALUES('min_pass_len', 6); --minimum password length INSERT INTO settings (name,value) VALUES('dwell_time', 2000); --time to elapse before new urls get to be pushed to the history stack +INSERT INTO settings (name,value) VALUES('recaptcha_key',''); --standard recaptcha key for the recapcha element --values for server config INSERT INTO settings (name,value) VALUES('cache_age',0);--cache age before invalid (in hours), 0 is infinite INSERT INTO settings (name,value) VALUES('server_port', 2040); --port the api server should listen on. INSERT INTO settings (name,value) VALUES('cookie_name', 'MBBall'); --name used for our main cookie INSERT INTO settings (name,value) VALUES('cookie_key', 'newCookieKey'); --key used to encrypt/decrypt cookie token INSERT INTO settings (name,value) VALUES('cookie_expires', 720); --hours until expire for standard logged on token +INSERT INTO settings (name,value) VALUES('recaptch_secret',''); -- secret key or verification of recaptcha. INSERT INTO settings (name,value) VALUES('verify_expires', 24); --hours until expire for verification tokens. INSERT INTO settings (name,value) VALUES('rate_limit', 30); --minutes that must elapse by verification emails INSERT INTO settings (name,value) VALUES('email_from', 'admin@example.com'); --email address that mail comes from (do not reply) diff --git a/server/session/recaptcha_verify.js b/server/session/recaptcha_verify.js new file mode 100644 index 0000000..8d51eb5 --- /dev/null +++ b/server/session/recaptcha_verify.js @@ -0,0 +1,50 @@ +/** +@licence + Copyright (c) 2020 Alan Chandler, all rights reserved + + This file is part of Football Mobile. + + Football Mobile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Football Mobile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Football Mobile. If not, see . +*/ + +(function() { + 'use strict'; + + const debug = require('debug')('football:api:recaptcha'); + const db = require('../utils/database'); + const secret = db.prepare('SELECT value FROM settings WHERE name = ?').pluck().get('recaptcha_secret'); + https = require('https'); + + + module.exports = async function(params) { + debug('new request with token', params.token); + return new Promise((accept, reject) => { + https.request({`https://www.google.com/recaptcha/api/siteverify?secret=${secret}&response=${params.token}`,{ + method: 'POST', + }, (resp) => { + let data = ''; + resp.on('data', (chunk) => { + data += chunk; + }); + // The whole response has been received. Print out the result. + resp.on('end', () => { + accept(JSON.parse(data)); + }); + + }).on("error", (err) => { + reject(err); + }); + }); + }; +})(); \ No newline at end of file