forked from ppfun/pixelplanet
parent
6eaea5b00c
commit
725e23dbab
|
@ -85,8 +85,8 @@ 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" |
|
||||
| CAPTCHA_URL | URL where captcha is served | "http://localhost:8080" |
|
||||
| CAPTCHA_TIME | time in minutes between captchas | 30 |
|
||||
| | 0: always captcha -1: never captcha | |
|
||||
| SESSION_SECRET | random sting for express sessions | "ayylmao" |
|
||||
| LOG_MYSQL | if sql queries should get logged | 0 |
|
||||
| USE_XREALIP | see ngins / CDN section | 1 |
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
apps:
|
||||
- script : ./captchaserver.js
|
||||
name : 'ppfun-captchas'
|
||||
node_args: --nouse-idle-notification --expose-gc
|
||||
env:
|
||||
PORT: 8080
|
||||
HOST: "localhost"
|
||||
REDIS_URL: 'redis://localhost:6379'
|
||||
CAPTCHA_TIMEOUT: 120
|
|
@ -58,15 +58,12 @@ do
|
|||
cd "$PFOLDER"
|
||||
pm2 stop ppfun-server
|
||||
pm2 stop ppfun-backups
|
||||
pm2 stop ppfun-captchs
|
||||
[ $DO_REINSTALL -eq 0 ] && npm_reinstall
|
||||
pm2 start ecosystem.yml
|
||||
pm2 start ecosystem-backup.yml
|
||||
pm2 start ecosystem-captchas.yml
|
||||
else
|
||||
echo "---UPDATING REPO ON DEV SERVER---"
|
||||
pm2 stop ppfun-server-dev
|
||||
pm2 stop ppfun-captchas-dev
|
||||
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git reset --hard "origin/$branch"
|
||||
COMMITS=`git log --pretty=format:'- %s%b' $newrev ^$oldrev`
|
||||
COMMITS=`echo "$COMMITS" | sed ':a;N;$!ba;s/\n/\\\n/g'`
|
||||
|
@ -88,6 +85,5 @@ do
|
|||
cd "$DEVFOLDER"
|
||||
[ $DO_REINSTALL -eq 0 ] && npm_reinstall
|
||||
pm2 start ecosystem.yml
|
||||
pm2 start ecosystem-captchas.yml
|
||||
fi
|
||||
done
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* serving captchas
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import process from 'process';
|
||||
import http from 'http';
|
||||
import url from 'url';
|
||||
import ppfunCaptcha from 'ppfun-captcha';
|
||||
|
||||
import { connect as connectRedis } from './data/redis/client';
|
||||
import { getIPFromRequest } from './utils/ip';
|
||||
import { setCaptchaSolution } from './utils/captcha';
|
||||
import { getRandomString } from './core/utils';
|
||||
|
||||
const PORT = process.env.PORT || 8080;
|
||||
const HOST = process.env.HOST || 'localhost';
|
||||
|
||||
const font = fs.readdirSync(path.resolve(__dirname, 'captchaFonts'))
|
||||
.filter((e) => e.endsWith('.ttf'))
|
||||
.map((e) => ppfunCaptcha.loadFont(
|
||||
path.resolve(__dirname, 'captchaFonts', e),
|
||||
));
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
console.log(req.url);
|
||||
|
||||
req.on('error', (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
const urlObject = url.parse(req.url, true);
|
||||
|
||||
if (req.method === 'GET' && urlObject.pathname.endsWith('.svg')) {
|
||||
try {
|
||||
const captcha = ppfunCaptcha.create({
|
||||
width: 500,
|
||||
height: 300,
|
||||
fontSize: 180,
|
||||
stroke: 'black',
|
||||
fill: 'none',
|
||||
nodeDeviation: 2.5,
|
||||
connectionPathDeviation: 10.0,
|
||||
style: 'stroke-width: 4;',
|
||||
background: '#EFEFEF',
|
||||
font,
|
||||
});
|
||||
|
||||
const ip = getIPFromRequest(req);
|
||||
const captchaid = getRandomString();
|
||||
|
||||
setCaptchaSolution(captcha.text, ip, captchaid);
|
||||
console.log(`Serving ${captcha.text} to ${ip} / ${captchaid}`);
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Captcha-Id': captchaid,
|
||||
});
|
||||
res.write(captcha.data);
|
||||
res.end();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.writeHead(503, {
|
||||
'Content-Type': 'text/html',
|
||||
'Cache-Control': 'no-cache',
|
||||
});
|
||||
res.end(
|
||||
// eslint-disable-next-line max-len
|
||||
'<html><body><h1>Captchaserver: 503 Server Error</h1>Captchas are accessible via *.svp paths</body></html>',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
res.writeHead(404, {
|
||||
'Content-Type': 'text/html',
|
||||
'Cache-Control': 'no-cache',
|
||||
});
|
||||
res.end(
|
||||
// eslint-disable-next-line max-len
|
||||
'<html><body><h1>Captchaserver: 404 Not Found</h1>Captchas are accessible via *.svp paths</body></html>',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// connect to redis
|
||||
connectRedis()
|
||||
.then(() => {
|
||||
// start http server
|
||||
const startServer = () => {
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log(`Captcha Server listening on port ${PORT}`);
|
||||
});
|
||||
};
|
||||
startServer();
|
||||
// catch errors of server
|
||||
server.on('error', (e) => {
|
||||
console.error(
|
||||
`Captcha Server Error ${e.code} occured, trying again in 5s...`,
|
||||
);
|
||||
setTimeout(() => {
|
||||
server.close();
|
||||
startServer();
|
||||
}, 5000);
|
||||
});
|
||||
});
|
|
@ -12,7 +12,7 @@ import { t } from 'ttag';
|
|||
import { IoReloadCircleSharp } from 'react-icons/io5';
|
||||
|
||||
async function getUrlAndId() {
|
||||
const url = window.ssv.captchaurl;
|
||||
const url = './captcha.svg';
|
||||
const resp = await fetch(url, {
|
||||
cache: 'no-cache',
|
||||
});
|
||||
|
|
67
src/core/captchaserver.js
Normal file
67
src/core/captchaserver.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* creation of captchas
|
||||
*/
|
||||
|
||||
import { Worker } from 'worker_threads';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
const MAX_WAIT = 20 * 1000;
|
||||
|
||||
/*
|
||||
* worker thread
|
||||
*/
|
||||
const worker = new Worker('./workers/captchaloader.js');
|
||||
|
||||
/*
|
||||
* queue of captcha-generation tasks
|
||||
* [[ timestamp, callbackFunction ],...]
|
||||
*/
|
||||
let captchaQueue = [];
|
||||
|
||||
/*
|
||||
* generate a captcha in the worker thread
|
||||
* calls callback with arguments:
|
||||
* (error, captcha.text, captcha.svgdata, captcha.id)
|
||||
*/
|
||||
function requestCaptcha(cb) {
|
||||
worker.postMessage('createCaptcha');
|
||||
captchaQueue.push([
|
||||
Date.now(),
|
||||
cb,
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
* answer of worker thread
|
||||
*/
|
||||
worker.on('message', (msg) => {
|
||||
const task = captchaQueue.shift();
|
||||
task[1](...msg);
|
||||
});
|
||||
|
||||
/*
|
||||
* checks queue of captcha requests for stale
|
||||
* unanswered requests
|
||||
*/
|
||||
function clearOldQueue() {
|
||||
const now = Date.now();
|
||||
captchaQueue = captchaQueue.filter((task) => {
|
||||
if (now - task[0] > MAX_WAIT) {
|
||||
logger.warn(
|
||||
'Captchas: Thread took longer than 30s to generate captcha',
|
||||
);
|
||||
try {
|
||||
task[1]('TIMEOUT');
|
||||
} catch {
|
||||
// nothing
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(clearOldQueue, MAX_WAIT);
|
||||
|
||||
export default requestCaptcha;
|
|
@ -20,8 +20,6 @@ export const TILE_FOLDER = path.join(__dirname, `./${TILE_FOLDER_REL}`);
|
|||
|
||||
export const ASSET_SERVER = process.env.ASSET_SERVER || '.';
|
||||
|
||||
export const CAPTCHA_URL = process.env.CAPTCHA_URL || null;
|
||||
|
||||
export const USE_XREALIP = process.env.USE_XREALIP || false;
|
||||
|
||||
export const BACKUP_URL = process.env.BACKUP_URL || null;
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
* check for captcha requirement
|
||||
*/
|
||||
|
||||
import logger from '../core/logger';
|
||||
import redis from '../data/redis/client';
|
||||
import { getIPv6Subnet } from './ip';
|
||||
import logger from '../../core/logger';
|
||||
import redis from './client';
|
||||
import { getIPv6Subnet } from '../../utils/ip';
|
||||
import {
|
||||
CAPTCHA_URL,
|
||||
CAPTCHA_TIME,
|
||||
CAPTCHA_TIMEOUT,
|
||||
} from '../core/config';
|
||||
} from '../../core/config';
|
||||
|
||||
const TTL_CACHE = CAPTCHA_TIME * 60; // seconds
|
||||
|
||||
|
@ -142,10 +141,9 @@ export async function checkCaptchaSolution(
|
|||
* @return boolean true if needed
|
||||
*/
|
||||
export async function needCaptcha(ip) {
|
||||
if (!CAPTCHA_URL) {
|
||||
if (CAPTCHA_TIME < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = `human:${getIPv6Subnet(ip)}`;
|
||||
const ttl = await redis.ttl(key);
|
||||
if (ttl > 0) {
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '../../../utils/validation';
|
||||
import {
|
||||
checkCaptchaSolution,
|
||||
} from '../../../utils/captcha';
|
||||
} from '../../../data/redis/captcha';
|
||||
|
||||
async function validate(email, name, password, captcha, captchaid, t, gettext) {
|
||||
const errors = [];
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import logger from '../../core/logger';
|
||||
import { checkCaptchaSolution } from '../../utils/captcha';
|
||||
import { checkCaptchaSolution } from '../../data/redis/captcha';
|
||||
import { getIPFromRequest } from '../../utils/ip';
|
||||
|
||||
export default async (req, res) => {
|
||||
|
|
|
@ -26,7 +26,6 @@ export default async (req, res, next) => {
|
|||
// https://stackoverflow.com/questions/49547/how-to-control-web-page-caching-across-all-browsers
|
||||
res.set({
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
});
|
||||
res.json(userdata);
|
||||
|
|
38
src/routes/captcha.js
Normal file
38
src/routes/captcha.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* route providing captcha
|
||||
*/
|
||||
import logger from '../core/logger';
|
||||
import requestCaptcha from '../core/captchaserver';
|
||||
import { getIPFromRequest } from '../utils/ip';
|
||||
import { setCaptchaSolution } from '../data/redis/captcha';
|
||||
|
||||
export default (req, res) => {
|
||||
res.set({
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
});
|
||||
|
||||
requestCaptcha((err, text, data, id) => {
|
||||
if (res.writableEnded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
res.status(503);
|
||||
res.send(
|
||||
// eslint-disable-next-line max-len
|
||||
'<html><body><h1>Captchaserver: 503 Server Error</h1>Captchas are accessible via *.svp paths</body></html>',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const ip = getIPFromRequest(req);
|
||||
setCaptchaSolution(text, ip, id);
|
||||
logger.info(`Captchas: ${ip} got captcha with text: ${text}`);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Captcha-Id': id,
|
||||
});
|
||||
res.end(data);
|
||||
});
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
/**
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
|
@ -11,9 +10,10 @@ import ranking from './ranking';
|
|||
import history from './history';
|
||||
import tiles from './tiles';
|
||||
import chunks from './chunks';
|
||||
import adminapi from './adminapi';
|
||||
import captcha from './captcha';
|
||||
import resetPassword from './reset_password';
|
||||
import api from './api';
|
||||
import adminapi from './adminapi';
|
||||
|
||||
import assets from './assets.json'; // eslint-disable-line import/no-unresolved
|
||||
import { expressTTag } from '../core/ttag';
|
||||
|
@ -47,6 +47,11 @@ router.use('/tiles', tiles);
|
|||
*/
|
||||
router.use('/adminapi', adminapi);
|
||||
|
||||
/*
|
||||
* serve captcha
|
||||
*/
|
||||
router.get('/captcha.svg', captcha);
|
||||
|
||||
/*
|
||||
* public folder
|
||||
* (this should be served with nginx or other webserver)
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
/*
|
||||
* send global ranking
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import rankings from '../core/ranking';
|
||||
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
export default (req, res) => {
|
||||
res.json(rankings.ranks);
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ import socketEvents from './SocketEvents';
|
|||
import chatProvider, { ChatProvider } from '../core/ChatProvider';
|
||||
import authenticateClient from './authenticateClient';
|
||||
import { drawByOffsets } from '../core/draw';
|
||||
import { needCaptcha } from '../utils/captcha';
|
||||
import { needCaptcha } from '../data/redis/captcha';
|
||||
import { cheapDetector } from '../core/isProxy';
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import assets from './assets.json';
|
|||
// eslint-disable-next-line import/no-unresolved
|
||||
import styleassets from './styleassets.json';
|
||||
|
||||
import { CAPTCHA_URL, ASSET_SERVER, BACKUP_URL } from '../core/config';
|
||||
import { ASSET_SERVER, BACKUP_URL } from '../core/config';
|
||||
|
||||
/*
|
||||
* generate language list
|
||||
|
@ -31,7 +31,6 @@ const langs = Object.keys(ttags)
|
|||
*/
|
||||
const ssv = {
|
||||
assetserver: ASSET_SERVER,
|
||||
captchaurl: CAPTCHA_URL,
|
||||
availableStyles: styleassets,
|
||||
langs,
|
||||
};
|
||||
|
|
4
src/workers/README.md
Normal file
4
src/workers/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Worker Threads
|
||||
|
||||
Every single .js file here automatically gets its own webpack entry point and
|
||||
will get build into its own worker/filename file.
|
62
src/workers/captchaloader.js
Normal file
62
src/workers/captchaloader.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* worker thread for creating captchas
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import ppfunCaptcha from 'ppfun-captcha';
|
||||
import { isMainThread, parentPort } from 'worker_threads';
|
||||
|
||||
import { getRandomString } from '../core/utils';
|
||||
|
||||
const FONT_FOLDER = 'captchaFonts';
|
||||
|
||||
if (isMainThread) {
|
||||
throw new Error(
|
||||
'Tilewriter is run as a worker thread, not as own process',
|
||||
);
|
||||
}
|
||||
|
||||
const font = fs.readdirSync(path.resolve(__dirname, '..', FONT_FOLDER))
|
||||
.filter((e) => e.endsWith('.ttf'))
|
||||
.map((e) => ppfunCaptcha.loadFont(
|
||||
path.resolve(__dirname, '..', FONT_FOLDER, e),
|
||||
));
|
||||
|
||||
function createCaptcha() {
|
||||
return ppfunCaptcha.create({
|
||||
width: 500,
|
||||
height: 300,
|
||||
fontSize: 180,
|
||||
stroke: 'black',
|
||||
fill: 'none',
|
||||
nodeDeviation: 2.5,
|
||||
connectionPathDeviation: 10.0,
|
||||
style: 'stroke-width: 4;',
|
||||
background: '#EFEFEF',
|
||||
font,
|
||||
});
|
||||
}
|
||||
|
||||
parentPort.on('message', (msg) => {
|
||||
try {
|
||||
if (msg === 'createCaptcha') {
|
||||
const captcha = createCaptcha();
|
||||
const captchaid = getRandomString();
|
||||
parentPort.postMessage([
|
||||
null,
|
||||
captcha.text,
|
||||
captcha.data,
|
||||
captchaid,
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
// eslint-disable-next-line max-len
|
||||
`Captchas: Error on createCaptcha: ${error.message}`,
|
||||
);
|
||||
parentPort.postMessage(['Failure!']);
|
||||
}
|
||||
});
|
|
@ -36,8 +36,16 @@ connectRedis()
|
|||
createTexture(...args);
|
||||
break;
|
||||
case 'initializeTiles':
|
||||
await initializeTiles(...args);
|
||||
parentPort.postMessage('Done!');
|
||||
try {
|
||||
await initializeTiles(...args);
|
||||
parentPort.postMessage('Done!');
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
// eslint-disable-next-line max-len
|
||||
`Tiling: Error on initializeTiles args ${args}: ${err.message}`,
|
||||
);
|
||||
parentPort.postMessage('Failure!');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn(`Tiling: Main thread requested unknown task ${task}`);
|
||||
|
@ -47,7 +55,6 @@ connectRedis()
|
|||
// eslint-disable-next-line max-len
|
||||
`Tiling: Error on executing task ${task} args ${args}: ${error.message}`,
|
||||
);
|
||||
parentPort.postMessage('Failure!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import AssetsPlugin from 'assets-webpack-plugin';
|
||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import nodeExternals from 'webpack-node-externals';
|
||||
|
@ -34,6 +35,9 @@ const babelPlugins = [
|
|||
export default ({
|
||||
development, extract,
|
||||
}) => {
|
||||
/*
|
||||
* write template files for translations
|
||||
*/
|
||||
if (extract) {
|
||||
ttag.extract = {
|
||||
output: path.resolve(__dirname, 'i18n', 'template-ssr.pot'),
|
||||
|
@ -41,6 +45,20 @@ export default ({
|
|||
ttag.discover = ['t', 'jt'];
|
||||
}
|
||||
|
||||
/*
|
||||
* worker threads need to be their own
|
||||
* entry points
|
||||
*/
|
||||
const workersDir = path.resolve(__dirname, 'src', 'workers');
|
||||
const workerEntries = {};
|
||||
fs.readdirSync(workersDir)
|
||||
.filter((e) => e.endsWith('.js'))
|
||||
.forEach((filename) => {
|
||||
const name = `workers/${filename.slice(0, -3)}`;
|
||||
const fullPath = path.resolve(workersDir, filename);
|
||||
workerEntries[name] = fullPath;
|
||||
});
|
||||
|
||||
return {
|
||||
name: 'server',
|
||||
target: 'node',
|
||||
|
@ -51,8 +69,7 @@ export default ({
|
|||
entry: {
|
||||
server: [path.resolve(__dirname, 'src', 'server.js')],
|
||||
backup: [path.resolve(__dirname, 'src', 'backup.js')],
|
||||
'workers/tilewriter': [path.resolve(__dirname, 'src', 'workers', 'tilewriter.js')],
|
||||
captchaserver: [path.resolve(__dirname, 'src', 'captchaserver.js')],
|
||||
...workerEntries,
|
||||
},
|
||||
|
||||
output: {
|
||||
|
@ -147,14 +164,6 @@ export default ({
|
|||
from: path.resolve(__dirname, 'deployment', 'captchaFonts'),
|
||||
to: path.resolve(__dirname, 'dist', 'captchaFonts'),
|
||||
},
|
||||
{
|
||||
from: path.resolve(
|
||||
__dirname, 'deployment', 'example-ecosystem-captchas.yml'
|
||||
),
|
||||
to: path.resolve(
|
||||
__dirname, 'dist', 'ecosystem-captchas.yml'
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue
Block a user