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 && }
+ {(CAPTCHA_METHOD === 1) && CAPTCHA_SITEKEY && useCaptcha
+ && }
+ {(CAPTCHA_METHOD === 2) && CAPTCHA_SITEKEY && useCaptcha
+ && (
+
+ )}
+ {(CAPTCHA_METHOD === 2) && CAPTCHA_SITEKEY && useCaptcha
+ && }
{code && (
,
);
diff --git a/src/core/config.js b/src/core/config.js
index 524894b..67f4071 100644
--- a/src/core/config.js
+++ b/src/core/config.js
@@ -82,13 +82,10 @@ export const auth = {
},
};
-
-export const ads = {
- adsense: {
- id: 'ca-pub-41116611299745444',
- },
-};
-
+// o: none
+// 1: reCaptcha
+// 2: hCaptcha
+export const CAPTCHA_METHOD = Number(process.env.CAPTCHA_METHOD || 0);
export const CAPTCHA_SECRET = process.env.CAPTCHA_SECRET || false;
export const CAPTCHA_SITEKEY = process.env.CAPTCHA_SITEKEY || false;
// time on which to display captcha in minutes
diff --git a/src/socket/ProtocolClient.js b/src/socket/ProtocolClient.js
index 7f9435b..5f20034 100644
--- a/src/socket/ProtocolClient.js
+++ b/src/socket/ProtocolClient.js
@@ -164,6 +164,7 @@ class ProtocolClient extends EventEmitter {
} catch (err) {
console.log(
`An error occured while parsing websocket message ${message}`,
+ err,
);
}
}
diff --git a/src/utils/captcha.js b/src/utils/captcha.js
index 109d737..6d41b73 100644
--- a/src/utils/captcha.js
+++ b/src/utils/captcha.js
@@ -8,13 +8,15 @@ 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
-const BASE_ENDPOINT = 'https://www.google.com/recaptcha/api/siteverify';
-const ENDPOINT = `${BASE_ENDPOINT}?secret=${CAPTCHA_SECRET}`;
+// 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
@@ -27,11 +29,7 @@ async function verifyReCaptcha(
token: string,
ip: string,
): Promise {
- if (!CAPTCHA_SECRET) {
- logger.info('Got captcha token but reCaptcha isn\'t configured?!');
- return true;
- }
- const url = `${ENDPOINT}&response=${token}&remoteip=${ip}`;
+ const url = `${RECAPTCHA_ENDPOINT}&response=${token}&remoteip=${ip}`;
const response = await fetch(url);
if (response.ok) {
const { success } = await response.json();
@@ -46,6 +44,38 @@ async function verifyReCaptcha(
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
*
@@ -58,7 +88,7 @@ export async function verifyCaptcha(
ip: string,
): Promise {
try {
- if (!CAPTCHA_SECRET) {
+ if (!CAPTCHA_METHOD) {
return true;
}
const key = `human:${ip}`;
@@ -67,8 +97,20 @@ export async function verifyCaptcha(
if (ttl > 0) {
return true;
}
- if (!await verifyReCaptcha(token, ip)) {
- return false;
+
+ 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);
@@ -86,7 +128,7 @@ export async function verifyCaptcha(
* @return boolean true if needed
*/
export async function needCaptcha(ip: string) {
- if (!CAPTCHA_SECRET) {
+ if (!CAPTCHA_METHOD) {
return false;
}