From 76d2a041b5265e0de70d37524c4e471c4d785490 Mon Sep 17 00:00:00 2001 From: HF Date: Sat, 16 May 2020 14:54:14 +0200 Subject: [PATCH] Add hCaptcha support --- README.md | 9 ++- src/:wq | 145 +++++++++++++++++++++++++++++++++++ src/actions/index.js | 8 +- src/client.js | 6 +- src/components/Converter.jsx | 2 +- src/components/HelpModal.jsx | 22 ++++-- src/components/Html.jsx | 20 ++++- src/components/Main.jsx | 2 +- src/core/config.js | 11 +-- src/socket/ProtocolClient.js | 1 + src/utils/captcha.js | 64 +++++++++++++--- 11 files changed, 253 insertions(+), 37 deletions(-) create mode 100644 src/:wq diff --git a/README.md b/README.md index 722fd38..fdef2ff 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,11 @@ Configuration takes place in the environment variables that are defined in ecosy | USE_PROXYCHECK | Check users for Proxies | 0 | | APISOCKET_KEY | Key for API Socket for SpecialAccess™ | "SDfasife3" | | ADMIN_IDS | Ids of users with Admin rights | "1,12,3" | -| RECAPTCHA_SECRET | reCaptcha secret key | "asdieewff" | -| RECAPTCHA_SITEKEY | reCaptcha site key | "23ksdfssd" | -| RECAPTCHA_TIME | time in minutes between captchas | 30 | -| SESSION_SECRET | random sting for expression sessions | "ayylmao" | +| CAPTCHA_METHOD | 0: none, 1: reCaptcha, 2: hCaptcha | 2 | +| CAPTCHA_SECRET | re/hCaptcha secret key | "asdieewff" | +| CAPTCHA_SITEKEY | re/hCaptcha site key | "23ksdfssd" | +| CAPTCHA_TIME | time in minutes between captchas | 30 | +| SESSION_SECRET | random sting for express sessions | "ayylmao" | | LOG_MYSQL | if sql queries should get logged | 0 | | USE_XREALIP | see cloudflare section | 1 | | BACKUP_URL | url of backup server (see Backup) | "http://localhost" | diff --git a/src/:wq b/src/:wq new file mode 100644 index 0000000..6d41b73 --- /dev/null +++ b/src/:wq @@ -0,0 +1,145 @@ +/** + * + * @flow + */ + +import fetch from 'isomorphic-fetch'; +import logger from '../core/logger'; +import redis from '../data/redis'; + +import { + CAPTCHA_METHOD, + CAPTCHA_SECRET, + CAPTCHA_TIME, +} from '../core/config'; + +const TTL_CACHE = CAPTCHA_TIME * 60; // seconds +// eslint-disable-next-line max-len +const RECAPTCHA_ENDPOINT = `https://www.google.com/recaptcha/api/siteverify?secret=${CAPTCHA_SECRET}`; +const HCAPTCHA_ENDPOINT = 'https://hcaptcha.com/siteverify'; + +/** + * https://stackoverflow.com/questions/27297067/google-recaptcha-how-to-get-user-response-and-validate-in-the-server-side + * + * @param token + * @param ip + * @returns {Promise.} + */ +async function verifyReCaptcha( + token: string, + ip: string, +): Promise { + const url = `${RECAPTCHA_ENDPOINT}&response=${token}&remoteip=${ip}`; + const response = await fetch(url); + if (response.ok) { + const { success } = await response.json(); + if (success) { + logger.info(`CAPTCHA ${ip} successfully solved captcha`); + return true; + } + logger.info(`CAPTCHA Token for ${ip} not ok`); + } else { + logger.warn(`CAPTCHA Recapcha answer for ${ip} not ok`); + } + return false; +} + +/* + * https://docs.hcaptcha.com/ + * + * @param token + * @param ip + * @return boolean, true if successful, false on error or fail + */ +async function verifyHCaptcha( + token: string, + ip: string, +): Promise { + const response = await fetch(HCAPTCHA_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `response=${token}&secret=${CAPTCHA_SECRET}&remoteip=${ip}`, + }); + if (response.ok) { + const { success } = await response.json(); + if (success) { + logger.info(`CAPTCHA ${ip} successfully solved captcha`); + return true; + } + logger.info(`CAPTCHA Token for ${ip} not ok`); + } else { + // eslint-disable-next-line max-len + logger.warn(`CAPTCHA hCapcha answer for ${ip} not ok ${await response.text()}`); + } + return false; +} + +/* + * verify captcha token from client + * + * @param token token of solved captcha from client + * @param ip + * @returns Boolean if successful + */ +export async function verifyCaptcha( + token: string, + ip: string, +): Promise { + try { + if (!CAPTCHA_METHOD) { + return true; + } + const key = `human:${ip}`; + + const ttl: number = await redis.ttlAsync(key); + if (ttl > 0) { + return true; + } + + switch (CAPTCHA_METHOD) { + case 1: + if (!await verifyReCaptcha(token, ip)) { + return false; + } + break; + case 2: + if (!await verifyHCaptcha(token, ip)) { + return false; + } + break; + default: + // nothing + } + + await redis.setAsync(key, '', 'EX', TTL_CACHE); + return true; + } catch (error) { + logger.error(error); + } + return false; +} + +/* + * check if captcha is needed + * + * @param ip + * @return boolean true if needed + */ +export async function needCaptcha(ip: string) { + if (!CAPTCHA_METHOD) { + return false; + } + + const key = `human:${ip}`; + const ttl: number = await redis.ttlAsync(key); + if (ttl > 0) { + return false; + } + logger.info(`CAPTCHA ${ip} got captcha`); + return true; +} + + +export default verifyCaptcha; diff --git a/src/actions/index.js b/src/actions/index.js index e1f3eb7..2b6a925 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -316,8 +316,12 @@ export function receivePixelReturn( dispatch(pixelWait()); break; case 10: - // captcha - window.grecaptcha.execute(); + // captcha, reCaptcha or hCaptcha + if (typeof window.hcaptcha !== 'undefined') { + window.hcaptcha.execute(); + } else { + window.grecaptcha.execute(); + } break; case 11: errorTitle = 'No Proxies Allowed :('; diff --git a/src/client.js b/src/client.js index 43367da..a34194e 100644 --- a/src/client.js +++ b/src/client.js @@ -140,5 +140,9 @@ window.onCaptcha = async function onCaptcha(token: string) { } = window.pixel; store.dispatch(tryPlacePixel(i, j, offset, color)); - window.grecaptcha.reset(); + if (typeof window.hcaptcha !== 'undefined') { + window.hcaptcha.reset(); + } else { + window.grecaptcha.reset(); + } }; diff --git a/src/components/Converter.jsx b/src/components/Converter.jsx index 3749866..fac2878 100644 --- a/src/components/Converter.jsx +++ b/src/components/Converter.jsx @@ -304,7 +304,7 @@ function Converter({ > Download -

Credit for the Palette of the Moon goes to +

Credit for the Palette of the Moon goes to  starhouse.

Image Converter

diff --git a/src/components/HelpModal.jsx b/src/components/HelpModal.jsx index 21eaf74..bed5197 100644 --- a/src/components/HelpModal.jsx +++ b/src/components/HelpModal.jsx @@ -52,12 +52,22 @@ const HelpModal = () => (

Left Click or tap to place a pixel

Right Click of double tap to remove a pixel

Partners: crazygames.com

-

- This site is protected by reCAPTCHA and the Google - Privacy Policy and - Terms of Service apply. - -

+ { (typeof window.hcaptcha === 'undefined') + ? ( +

+ This site is protected by reCAPTCHA and the Google  + Privacy Policy and  + Terms of Service apply. + +

+ ) : ( +

+ This site is protected by hCAPTCHA and its  + Privacy Policyand  + Terms of Serviceapply. + +

+ )}

); diff --git a/src/components/Html.jsx b/src/components/Html.jsx index a6ffa3a..5adf5cc 100644 --- a/src/components/Html.jsx +++ b/src/components/Html.jsx @@ -11,7 +11,7 @@ /* eslint-disable max-len */ import React from 'react'; -import { CAPTCHA_SITEKEY } from '../core/config'; +import { CAPTCHA_METHOD, CAPTCHA_SITEKEY } from '../core/config'; const Html = ({ title, @@ -26,7 +26,7 @@ const Html = ({ // code as string code, // if recaptcha should get loaded - useRecaptcha, + useCaptcha, }) => ( @@ -48,7 +48,7 @@ const Html = ({ dangerouslySetInnerHTML={{ __html: style.cssText }} /> ))} - {CAPTCHA_SITEKEY && useRecaptcha + {(CAPTCHA_METHOD === 1) && CAPTCHA_SITEKEY && useCaptcha && (
)} - {CAPTCHA_SITEKEY && useRecaptcha &&