change captcha

remove noauthUser, which was just confusing
remove rest of api/pixel
This commit is contained in:
HF 2020-05-16 10:45:14 +02:00
parent 2b8b5b5888
commit ecb27a1ea0
21 changed files with 216 additions and 486 deletions

View File

@ -19,6 +19,7 @@ import {
receiveChatHistory,
receivePixelReturn,
setMobile,
tryPlacePixel,
} from './actions';
import store from './ui/store';
@ -116,3 +117,28 @@ document.addEventListener('DOMContentLoaded', () => {
}
setInterval(runGC, 300000);
});
// on captcha received
// TODO: this really isn't beautiful
window.onCaptcha = async function onCaptcha(token: string) {
const body = JSON.stringify({
token,
});
await fetch('/api/captcha', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
// https://github.com/github/fetch/issues/349
credentials: 'include',
});
const {
i, j, offset, color,
} = window.pixel;
store.dispatch(tryPlacePixel(i, j, offset, color));
window.grecaptcha.reset();
};

View File

@ -18,7 +18,6 @@ import ChatButton from './ChatButton';
import ChatBox from './ChatBox';
import Menu from './Menu';
import UI from './UI';
import ReCaptcha from './ReCaptcha';
import ExpandMenuButton from './ExpandMenuButton';
import MinecraftTPButton from './MinecraftTPButton';
import ModalRoot from './ModalRoot';
@ -27,7 +26,6 @@ const App = () => (
<div>
<Style />
<div id="outstreamContainer" />
<ReCaptcha />
<IconContext.Provider value={{ style: { verticalAlign: 'middle' } }}>
<CanvasSwitchButton />
<Menu />

View File

@ -11,7 +11,7 @@
/* eslint-disable max-len */
import React from 'react';
import { analytics, RECAPTCHA_SITEKEY } from '../core/config';
import { CAPTCHA_SITEKEY } from '../core/config';
const Html = ({
title,
@ -48,10 +48,16 @@ const Html = ({
dangerouslySetInnerHTML={{ __html: style.cssText }}
/>
))}
{RECAPTCHA_SITEKEY && useRecaptcha
// eslint-disable-next-line react/no-danger
&& <script dangerouslySetInnerHTML={{ __html: `window.sitekey="${RECAPTCHA_SITEKEY}"` }} />}
{RECAPTCHA_SITEKEY && useRecaptcha && <script src="https://www.google.com/recaptcha/api.js" async defer />}
{CAPTCHA_SITEKEY && useRecaptcha
&& (
<div
className="g-recaptcha"
data-sitekey={CAPTCHA_SITEKEY}
data-callback="onCaptcha"
data-size="invisible"
/>
)}
{CAPTCHA_SITEKEY && useRecaptcha && <script src="https://www.google.com/recaptcha/api.js" async defer />}
{code && (
<script
// eslint-disable-next-line react/no-danger
@ -67,19 +73,6 @@ const Html = ({
{body}
</div>
{scripts && scripts.map((script) => <script key={script} src={script} />)}
{analytics.google.trackingId
&& (
<script
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html:
'window.ga=function(){ga.q.push(arguments)};ga.q=[];ga.l=+new Date;'
+ `ga('create','${analytics.google.trackingId}','auto');ga('send','pageview')`,
}}
/>
)}
{analytics.google.trackingId
&& <script src="https://www.google-analytics.com/analytics.js" async defer />}
</body>
</html>
);

View File

@ -1,47 +0,0 @@
/**
*
* @flow
* Implement ReCaptcha
* (the recaptcha sitekey gets received in the Html inline script sent by components/Html)
*/
import React from 'react';
import store from '../ui/store';
import { tryPlacePixel } from '../actions';
async function onCaptcha(token: string) {
const body = JSON.stringify({
token,
});
await fetch('/api/captcha', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
// https://github.com/github/fetch/issues/349
credentials: 'include',
});
const {
i, j, offset, color,
} = window.pixel;
store.dispatch(tryPlacePixel(i, j, offset, color));
window.grecaptcha.reset();
}
// https://stackoverflow.com/questions/41717304/recaptcha-google-data-callback-with-angularjs
window.onCaptcha = onCaptcha;
const ReCaptcha = () => (
<div
className="g-recaptcha"
data-sitekey={window.sitekey}
data-callback="onCaptcha"
data-size="invisible"
/>
);
export default ReCaptcha;

View File

@ -89,9 +89,9 @@ export const ads = {
},
};
export const RECAPTCHA_SECRET = process.env.RECAPTCHA_SECRET || false;
export const RECAPTCHA_SITEKEY = process.env.RECAPTCHA_SITEKEY || false;
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
export const RECAPTCHA_TIME = parseInt(process.env.RECAPTCHA_TIME, 10) || 30;
export const CAPTCHA_TIME = parseInt(process.env.CAPTCHA_TIME, 10) || 30;
export const SESSION_SECRET = process.env.SESSION_SECRET || 'dummy';

View File

@ -9,6 +9,8 @@ import { getIPv6Subnet } from '../utils/ip';
import { Blacklist, Whitelist } from '../data/models';
import { proxyLogger } from './logger';
import { USE_PROXYCHECK } from './config';
const logger = proxyLogger;
@ -204,7 +206,10 @@ async function withCache(f, ip) {
}
export function cheapDetector(ip: string): Promise<boolean> {
return withCache(getProxyCheck, ip);
if (USE_PROXYCHECK) {
return withCache(getProxyCheck, ip);
}
return withCache(dummy, ip);
}
export function strongDetector(ip: string): Promise<boolean> {
@ -214,5 +219,3 @@ export function strongDetector(ip: string): Promise<boolean> {
export function blacklistDetector(ip: string): Promise<boolean> {
return withCache(dummy, ip);
}
// export default cheapDetector;

View File

@ -18,17 +18,15 @@ import { sanitizeName } from '../utils/validation';
import { User, RegUser } from '../data/models';
import { auth } from './config';
import { compareToHash } from '../utils/hash';
import { getIPFromRequest } from '../utils/ip';
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((req, id, done) => {
// req.noauthUser already get populated with id and ip in routes/api/index to allow
// some api requests (pixel) to not require this sql deserlialize
// but still know the id
const user = (req.noauthUser) ? req.noauthUser : new User(id);
passport.deserializeUser(async (req, id, done) => {
const user = new User(id, getIPFromRequest(req));
if (id) {
RegUser.findOne({ where: { id } }).then((reguser) => {
if (reguser) {

View File

@ -13,6 +13,7 @@ import logger from '../../core/logger';
import Model from '../sequelize';
import RegUser from './RegUser';
import { getIPv6Subnet } from '../../utils/ip';
import { ADMIN_IDS } from '../../core/config';
@ -27,6 +28,7 @@ class User {
// id should stay null if unregistered, and user email if registered
this.id = id;
this.ip = ip;
this.ipSub = getIPv6Subnet(ip);
this.wait = null;
this.regUser = null;
}
@ -50,7 +52,7 @@ class User {
if (!coolDown) return false;
this.wait = Date.now() + coolDown;
// PX is milliseconds expire
await redis.setAsync(`cd:${canvasId}:ip:${this.ip}`, '', 'PX', coolDown);
await redis.setAsync(`cd:${canvasId}:ip:${this.ipSub}`, '', 'PX', coolDown);
if (this.id != null) {
await redis.setAsync(`cd:${canvasId}:id:${this.id}`, '', 'PX', coolDown);
}
@ -58,7 +60,7 @@ class User {
}
async getWait(canvasId: number): Promise<?number> {
let ttl: number = await redis.pttlAsync(`cd:${canvasId}:ip:${this.ip}`);
let ttl: number = await redis.pttlAsync(`cd:${canvasId}:ip:${this.ipSub}`);
if (this.id != null && ttl < 0) {
const ttlid: number = await redis.pttlAsync(
`cd:${canvasId}:id:${this.id}`,

View File

@ -65,7 +65,7 @@ router.use(session);
router.use(passport.initialize());
router.use(passport.session());
router.use(async (req, res, next) => {
const ip = await getIPFromRequest(req);
const ip = getIPFromRequest(req);
if (!req.user) {
logger.info(`ADMINTOOLS: ${ip} tried to access admintools without login`);
res.status(403).send('You are not logged in');

View File

@ -62,10 +62,10 @@ export default async (req: Request, res: Response) => {
return;
}
const ip = await getIPFromRequest(req);
const ip = getIPFromRequest(req);
logger.info(`Created new user ${name} ${email} ${ip}`);
const user = req.noauthUser;
const { user } = req;
user.id = newuser.id;
user.regUser = newuser;
const me = await getMe(user);

View File

@ -8,30 +8,11 @@
import type { Request, Response } from 'express';
import logger from '../../core/logger';
import redis from '../../data/redis';
import verifyCaptcha from '../../utils/recaptcha';
import {
RECAPTCHA_SECRET,
RECAPTCHA_TIME,
} from '../../core/config';
const TTL_CACHE = RECAPTCHA_TIME * 60; // seconds
import { verifyCaptcha } from '../../utils/captcha';
import { getIPFromRequest } from '../../utils/ip';
export default async (req: Request, res: Response) => {
if (!RECAPTCHA_SECRET) {
res.status(200)
.json({
errors: [{
msg:
'No need for a captcha here',
}],
});
return;
}
const user = req.user || req.noauthUser;
const { ip } = user;
const ip = getIPFromRequest(req);
try {
const { token } = req.body;
@ -41,20 +22,6 @@ export default async (req: Request, res: Response) => {
return;
}
const key = `human:${ip}`;
const ttl: number = await redis.ttlAsync(key);
if (ttl > 0) {
res.status(400)
.json({
errors: [{
msg:
'Why would you even want to solve a captcha?',
}],
});
return;
}
if (!await verifyCaptcha(token, ip)) {
logger.info(`CAPTCHA ${ip} failed his captcha`);
res.status(422)
@ -67,9 +34,6 @@ export default async (req: Request, res: Response) => {
return;
}
// save to cache
await redis.setAsync(key, 'y', 'EX', TTL_CACHE);
res.status(200)
.json({ success: true });
} catch (error) {

View File

@ -8,12 +8,11 @@ import bodyParser from 'body-parser';
import session from '../../core/session';
import passport from '../../core/passport';
import logger from '../../core/logger';
import { User } from '../../data/models';
import { getIPFromRequest, getIPv6Subnet } from '../../utils/ip';
import User from '../../data/models/User';
import { getIPFromRequest } from '../../utils/ip';
import me from './me';
import mctp from './mctp';
// import pixel from './pixel';
import captcha from './captcha';
import auth from './auth';
import ranking from './ranking';
@ -27,27 +26,6 @@ router.get('/ranking', ranking);
router.get('/history', history);
/*
* get user session
*/
router.use(session);
/*
* create dummy user that has just ip and id
* (cut IPv6 to subnet to prevent abuse)
*/
router.use(async (req, res, next) => {
const { session: sess } = req;
const id = (sess.passport && sess.passport.user)
? sess.passport.user : null;
const ip = await getIPFromRequest(req);
const trueIp = ip || '0.0.0.1';
req.trueIp = trueIp;
const user = new User(id, getIPv6Subnet(trueIp));
req.noauthUser = user;
next();
});
router.use(bodyParser.json());
router.use((err, req, res, next) => {
@ -60,16 +38,21 @@ router.use((err, req, res, next) => {
}
});
/*
* rate limiting should occure outside,
* with nginx or whatever
*/
/* api pixel got deactivated in favor of websocket */
/* keeping it still here to enable it again if needed */
// router.post('/pixel', pixel);
// captcah doesn't need a user
router.post('/captcha', captcha);
/*
* get user session
*/
router.use(session);
/*
* at this point we could use the session id to get
* stuff without having to verify the whole user,
* which would avoid SQL requests and it got used previously
* when we set pixels via api/pixel (new removed)
*/
/*
* passport authenticate
* and deserlialize
@ -79,6 +62,17 @@ router.post('/captcha', captcha);
router.use(passport.initialize());
router.use(passport.session());
/*
* create dummy user with just ip if not
* logged in
*/
router.use((req, res, next) => {
if (!req.user) {
req.user = new User(null, getIPFromRequest(req));
}
next();
});
router.get('/me', me);
router.post('/mctp', mctp);

View File

@ -14,7 +14,7 @@ import { cheapDetector } from '../../core/isProxy';
export default async (req: Request, res: Response) => {
const user = req.user || req.noauthUser;
const { user } = req;
const userdata = await getMe(user);
user.updateLogInTimestamp();

View File

@ -1,224 +0,0 @@
/**
*
* @flow
*/
import type { Request, Response } from 'express';
import { drawSafeByCoords } from '../../core/draw';
import {
blacklistDetector,
cheapDetector,
strongDetector,
} from '../../core/isProxy';
import verifyCaptcha from '../../utils/recaptcha';
import logger, { pixelLogger } from '../../core/logger';
import redis from '../../data/redis';
import {
USE_PROXYCHECK,
RECAPTCHA_SECRET,
RECAPTCHA_TIME,
} from '../../core/config';
async function validate(req: Request, res: Response, next) {
let error = null;
const cn = parseInt(req.body.cn, 10);
const x = parseInt(req.body.x, 10);
const y = parseInt(req.body.y, 10);
let z = null;
if (typeof req.body.z !== 'undefined') {
z = parseInt(req.body.z, 10);
}
const clr = parseInt(req.body.clr, 10);
if (Number.isNaN(cn)) {
error = 'No valid canvas selected';
} else if (Number.isNaN(x)) {
error = 'x is not a valid number';
} else if (Number.isNaN(y)) {
error = 'y is not a valid number';
} else if (Number.isNaN(clr)) {
error = 'No color selected';
} else if (z !== null && Number.isNaN(z)) {
error = 'z is not a valid number';
}
if (error !== null) {
res.status(400).json({ errors: [{ msg: error }] });
return;
}
req.body.cn = cn;
req.body.x = x;
req.body.y = y;
req.body.z = z;
req.body.clr = clr;
/**
* make sure that a user is chosen
* req.noauthUser: user with just ip and id set
* req.user: fully passport authenticated user
* api/pixel just requires ip and id, so noauthUser is enough
* a fully authenticated user would cause more SQL requests
*/
let { user } = req;
if (!req.user) {
req.user = req.noauthUser;
user = req.user;
}
if (!user || !user.ip) {
res.status(400).json({ errors: [{ msg: "Couldn't authenticate" }] });
return;
}
next();
}
const TTL_CACHE = RECAPTCHA_TIME * 60; // seconds
async function checkHuman(req: Request, res: Response, next) {
if (!RECAPTCHA_SECRET) {
next();
return;
}
const { user } = req;
const { ip } = user;
if (user.isAdmin()) {
next();
return;
}
try {
const { token } = req.body;
const key = `human:${ip}`;
const ttl: number = await redis.ttlAsync(key);
if (ttl > 0) {
next();
return;
}
if (!token || !await verifyCaptcha(token, ip)) {
logger.info(`CAPTCHA ${ip} got a captcha`);
res.status(422)
.json({ errors: [{ msg: 'Captcha occured' }] });
return;
}
// save to cache
await redis.setAsync(key, 'y', 'EX', TTL_CACHE);
} catch (error) {
logger.error('checkHuman', error);
}
next();
}
// cheap check whole canvas for proxies, if USE_PROXYCHECK is one
// strongly check selective areas
async function checkProxy(req: Request, res: Response, next) {
const { trueIp: ip } = req;
if (USE_PROXYCHECK && ip !== '0.0.0.1') {
/*
//one area uses stronger detector
const { x, y } = req.body;
if ((x > 970 && x < 2380 && y > -11407 && y < -10597) || //nc
(x > 4220 && x < 6050 && y > -12955 && y < -11230) || //belarius
(x > 14840 && x < 15490 && y > -17380 && y < -16331) || //russian bot
(x > 11189 && x < 12003 && y > 3483 && y < 4170) || //random bot
(x > -13402 && x < -5617 && y > 1640 && y < 5300)){ //brazil
if (!ip || await strongDetector(ip)) {
res.status(403)
.json({ errors: [{ msg: 'You are using a proxy!' }] });
return;
}
} else {
*/
if (!ip || await cheapDetector(ip)) {
res.status(403)
.json({ errors: [{ msg: 'You are using a proxy!' }] });
return;
}
/*
}
*/
} else if (await blacklistDetector(ip)) {
res.status(403)
.json({ errors: [{ msg: 'You are using a proxy or got banned!' }] });
return;
}
next();
}
// strongly check just specific areas for proxies
// do not proxycheck the rest
// eslint-disable-next-line no-unused-vars
async function checkProxySelective(req: Request, res: Response, next) {
const { trueIp: ip } = req;
if (USE_PROXYCHECK) {
const { x, y } = req.body;
if (x > 970 && x < 2380 && y > -11407 && y < -10597) { // nc
if (!ip || await strongDetector(ip)) {
res.status(403)
.json({ errors: [{ msg: 'You are using a proxy!' }] });
return;
}
}
} else if (await blacklistDetector(ip)) {
res.status(403)
.json({ errors: [{ msg: 'You are using a proxy or got banned!' }] });
return;
}
next();
}
// place pixel and return waiting time
async function place(req: Request, res: Response) {
// https://stackoverflow.com/questions/49547/how-to-control-web-page-caching-across-all-browsers
// https://stackoverflow.com/a/7066740
res.set({
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
});
const {
cn, x, y, z, clr,
} = req.body;
const { user, trueIp } = req;
// eslint-disable-next-line max-len
pixelLogger.info(`${trueIp} ${user.id} ${cn} ${x} ${y} ${z} ${clr}`);
const {
errorTitle, error, success, waitSeconds, coolDownSeconds,
} = await drawSafeByCoords(user, cn, clr, x, y, z);
logger.log('debug', success);
if (success) {
res.json({ success, waitSeconds, coolDownSeconds });
} else {
const errors = [];
if (error) {
res.status(403);
errors.push({ msg: error });
}
if (errorTitle) {
res.json({
success, waitSeconds, coolDownSeconds, errorTitle, errors,
});
} else {
res.json({
success, waitSeconds, coolDownSeconds, errors,
});
}
}
}
export default [validate, checkHuman, checkProxy, place];

View File

@ -27,7 +27,7 @@ function heartbeat() {
async function verifyClient(info, done) {
const { req } = info;
const { headers } = req;
const ip = await getIPFromRequest(req);
const ip = getIPFromRequest(req);
if (!headers.authorization
|| headers.authorization !== `Bearer ${APISOCKET_KEY}`) {

View File

@ -38,7 +38,7 @@ class ProtocolClient extends EventEmitter {
this.isConnected = false;
this.ws = null;
this.name = null;
this.canvasId = null;
this.canvasId = '0';
this.msgQueue = [];
}
@ -105,7 +105,11 @@ class ProtocolClient extends EventEmitter {
}
setCanvas(canvasId) {
if (this.canvasId === canvasId || canvasId === null) {
/* canvasId can be string or integer, thanks to
* JSON not allowing integer keys
*/
// eslint-disable-next-line eqeqeq
if (this.canvasId == canvasId || canvasId === null) {
return;
}
console.log('Notify websocket server that we changed canvas');

View File

@ -25,13 +25,8 @@ import authenticateClient from './verifyClient';
import WebSocketEvents from './WebSocketEvents';
import webSockets from './websockets';
import { drawSafeByOffset } from '../core/draw';
import redis from '../data/redis';
import { cheapDetector, blacklistDetector } from '../core/isProxy';
import {
USE_PROXYCHECK,
RECAPTCHA_SECRET,
} from '../core/config';
import { needCaptcha } from '../utils/captcha';
import { cheapDetector } from '../core/isProxy';
const ipCounter: Counter<string> = new Counter();
@ -45,7 +40,7 @@ async function verifyClient(info, done) {
const { headers } = req;
// Limiting socket connections per ip
const ip = await getIPFromRequest(req);
const ip = getIPFromRequest(req);
logger.info(`Got ws request from ${ip} via ${headers.origin}`);
if (ipCounter.get(ip) > 50) {
logger.info(`Client ${ip} has more than 50 connections open.`);
@ -92,7 +87,7 @@ class SocketServer extends WebSocketEvents {
ws.user = user;
ws.name = (user.regUser) ? user.regUser.name : null;
ws.rateLimiter = new RateLimiter(20, 15, true);
SocketServer.checkIfProxy(ws);
cheapDetector(user.ip);
if (ws.name) {
ws.send(`"${ws.name}"`);
@ -102,7 +97,7 @@ class SocketServer extends WebSocketEvents {
online: this.wss.clients.size || 0,
}));
const ip = await getIPFromRequest(req);
const ip = getIPFromRequest(req);
ws.on('error', (e) => {
logger.error(`WebSocket Client Error for ${ws.name}: ${e.message}`);
});
@ -245,16 +240,6 @@ class SocketServer extends WebSocketEvents {
webSockets.broadcastOnlineCounter(online);
}
static async checkIfProxy(ws) {
const { ip } = ws.user;
if (USE_PROXYCHECK && ip && await cheapDetector(ip)) {
return true;
} if (await blacklistDetector(ip)) {
return true;
}
return false;
}
static async onTextMessage(text, ws) {
try {
let message;
@ -288,7 +273,7 @@ class SocketServer extends WebSocketEvents {
return;
}
// check proxy
if (await SocketServer.checkIfProxy(ws)) {
if (await cheapDetector(ws)) {
logger.info(
`${ws.name} / ${user.ip} tried to send chat message with proxy`,
);
@ -346,18 +331,13 @@ class SocketServer extends WebSocketEvents {
}
const { ip } = user;
// check if captcha needed
if (RECAPTCHA_SECRET) {
const key = `human:${ip}`;
const ttl: number = await redis.ttlAsync(key);
if (ttl <= 0) {
// need captcha
logger.info(`CAPTCHA ${ip} / ${ws.name} got captcha`);
ws.send(PixelReturn.dehydrate(10, 0, 0));
break;
}
if (await needCaptcha(ip)) {
// need captcha
ws.send(PixelReturn.dehydrate(10, 0, 0));
break;
}
// (re)check for Proxy
if (await SocketServer.checkIfProxy(ws)) {
if (await cheapDetector(ip)) {
ws.send(PixelReturn.dehydrate(11, 0, 0));
break;
}

View File

@ -6,27 +6,13 @@ import express from 'express';
import session from '../core/session';
import passport from '../core/passport';
import { User } from '../data/models';
import { getIPFromRequest, getIPv6Subnet } from '../utils/ip';
import User from '../data/models/User';
import { getIPFromRequest } from '../utils/ip';
const router = express.Router();
router.use(session);
/*
* create dummy user that has just ip and id
* (cut IPv6 to subnet to prevent abuse)
*/
router.use(async (req, res, next) => {
const ip = await getIPFromRequest(req);
const trueIp = ip || '0.0.0.1';
req.trueIp = trueIp;
const user = new User(null, getIPv6Subnet(trueIp));
req.noauthUser = user;
next();
});
router.use(passport.initialize());
router.use(passport.session());
@ -34,9 +20,10 @@ router.use(passport.session());
function authenticateClient(req) {
return new Promise(
((resolve) => {
router(req, {}, () => {
const user = req.user || req.noauthUser;
router(req, {}, async () => {
const country = req.headers['cf-ipcountry'];
const user = (req.user) ? req.user
: new User(null, getIPFromRequest(req));
user.country = country.toLowerCase();
resolve(user);
});

103
src/utils/captcha.js Normal file
View File

@ -0,0 +1,103 @@
/**
*
* @flow
*/
import fetch from 'isomorphic-fetch';
import logger from '../core/logger';
import redis from '../data/redis';
import {
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}`;
/**
* 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> {
if (!CAPTCHA_SECRET) {
logger.info('Got captcha token but reCaptcha isn\'t configured?!');
return true;
}
const url = `${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;
}
/*
* 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_SECRET) {
return true;
}
const key = `human:${ip}`;
const ttl: number = await redis.ttlAsync(key);
if (ttl > 0) {
return true;
}
if (!await verifyReCaptcha(token, ip)) {
return false;
}
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_SECRET) {
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

@ -25,10 +25,11 @@ export function getHostFromRequest(req): ?string {
return `${proto}://${host}`;
}
export async function getIPFromRequest(req): ?string {
export function getIPFromRequest(req): ?string {
const { socket, connection, headers } = req;
const conip = (connection ? connection.remoteAddress : socket.remoteAddress);
let conip = (connection ? connection.remoteAddress : socket.remoteAddress);
conip = conip || '0.0.0.1';
if (USE_XREALIP) {
const ip = headers['x-real-ip'];
@ -48,9 +49,7 @@ export async function getIPFromRequest(req): ?string {
while (isTrustedProxy(ip) && ipList.length) {
ip = ipList.pop();
}
// logger.info('Proxied Connection allowed', ip, forwardedFor);
return ip;
return ip || conip;
}
export function getIPv6Subnet(ip: string): string {

View File

@ -1,50 +0,0 @@
/**
*
* @flow
*/
import fetch from 'isomorphic-fetch';
import logger from '../core/logger';
import { RECAPTCHA_SECRET } from '../core/config';
const BASE_ENDPOINT = 'https://www.google.com/recaptcha/api/siteverify';
const ENDPOINT = `${BASE_ENDPOINT}?secret=${RECAPTCHA_SECRET}`;
/**
* 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 verifyCaptcha(
token: string,
ip: string,
): Promise<boolean> {
try {
if (!RECAPTCHA_SECRET) {
logger.info('Got captcha token but reCaptcha isn\'t configured?!');
return true;
}
const url = `${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`);
}
} catch (error) {
logger.error(error);
}
return false;
}
export default verifyCaptcha;