Add hCaptcha support

This commit is contained in:
HF 2020-05-16 14:54:14 +02:00
parent ecb27a1ea0
commit 76d2a041b5
11 changed files with 253 additions and 37 deletions

View File

@ -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" |

145
src/:wq Normal file
View File

@ -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.<boolean>}
*/
async function verifyReCaptcha(
token: string,
ip: string,
): Promise<boolean> {
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<boolean> {
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<boolean> {
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;

View File

@ -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 :(';

View File

@ -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();
}
};

View File

@ -304,7 +304,7 @@ function Converter({
>
Download
</button>
<p>Credit for the Palette of the Moon goes to
<p>Credit for the Palette of the Moon goes to&nbsp;
<a href="https://twitter.com/starhousedev">starhouse</a>.</p>
</p>
<h3 className="modaltitle">Image Converter</h3>

View File

@ -52,12 +52,22 @@ const HelpModal = () => (
<p className="modaltext">Left Click or tap to place a pixel</p>
<p className="modaltext">Right Click of double tap to remove a pixel</p>
<p>Partners: <a href="https://www.crazygames.com/c/io" target="_blank" rel="noopener noreferrer">crazygames.com</a></p>
<p className="modaltext">
<small>This site is protected by reCAPTCHA and the Google
<a href="https://policies.google.com/privacy">Privacy Policy</a> and
<a href="https://policies.google.com/terms">Terms of Service</a> apply.
</small>
</p>
{ (typeof window.hcaptcha === 'undefined')
? (
<p className="modaltext">
<small>This site is protected by reCAPTCHA and the Google&nbsp;
<a href="https://policies.google.com/privacy">Privacy Policy</a> and&nbsp;
<a href="https://policies.google.com/terms">Terms of Service</a> apply.
</small>
</p>
) : (
<p className="modaltext">
<small>This site is protected by hCAPTCHA and its&nbsp;
<a href="https://hcaptcha.com/privacy">Privacy Policy</a>and&nbsp;
<a href="https://hcaptcha.com/terms">Terms of Service</a>apply.
</small>
</p>
)}
</p>
);

View File

@ -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,
}) => (
<html className="no-js" lang="en">
<head>
@ -48,7 +48,7 @@ const Html = ({
dangerouslySetInnerHTML={{ __html: style.cssText }}
/>
))}
{CAPTCHA_SITEKEY && useRecaptcha
{(CAPTCHA_METHOD === 1) && CAPTCHA_SITEKEY && useCaptcha
&& (
<div
className="g-recaptcha"
@ -57,7 +57,19 @@ const Html = ({
data-size="invisible"
/>
)}
{CAPTCHA_SITEKEY && useRecaptcha && <script src="https://www.google.com/recaptcha/api.js" async defer />}
{(CAPTCHA_METHOD === 1) && CAPTCHA_SITEKEY && useCaptcha
&& <script src="https://www.google.com/recaptcha/api.js" async defer />}
{(CAPTCHA_METHOD === 2) && CAPTCHA_SITEKEY && useCaptcha
&& (
<div
className="h-captcha"
data-sitekey={CAPTCHA_SITEKEY}
data-callback="onCaptcha"
data-size="invisible"
/>
)}
{(CAPTCHA_METHOD === 2) && CAPTCHA_SITEKEY && useCaptcha
&& <script src="https://hcaptcha.com/1/api.js" async defer />}
{code && (
<script
// eslint-disable-next-line react/no-danger

View File

@ -51,7 +51,7 @@ function generateMainPage(countryCoords: Cell): string {
scripts={scripts}
css={css}
code={`${code}window.coordx=${x};window.coordy=${y};`}
useRecaptcha
useCaptcha
/>,
);

View File

@ -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

View File

@ -164,6 +164,7 @@ class ProtocolClient extends EventEmitter {
} catch (err) {
console.log(
`An error occured while parsing websocket message ${message}`,
err,
);
}
}

View File

@ -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<boolean> {
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<boolean> {
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<boolean> {
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;
}