add ui for iid banning

removed ip banning
add eslint option for react-key
This commit is contained in:
HF 2022-08-05 16:56:31 +02:00
parent 53b8168f24
commit a41c286372
31 changed files with 491 additions and 220 deletions

View File

@ -13,6 +13,7 @@
}, },
"plugins": [ "plugins": [
"react", "react",
"react-hooks",
"jsx-a11y", "jsx-a11y",
"import" "import"
], ],
@ -32,7 +33,9 @@
"no-mixed-operators":"off", "no-mixed-operators":"off",
"react/prop-types": "off", "react/prop-types": "off",
"react/jsx-one-expression-per-line": "off", "react/jsx-one-expression-per-line": "off",
"react/jsx-closing-tag-location":"off", "react/jsx-closing-tag-location": "off",
"react/jsx-key": "warn",
"react-hooks/rules-of-hooks": "error",
"jsx-a11y/click-events-have-key-events":"off", "jsx-a11y/click-events-have-key-events":"off",
"jsx-a11y/no-static-element-interactions":"off", "jsx-a11y/no-static-element-interactions":"off",
"no-continue": "off", "no-continue": "off",

3
package-lock.json generated
View File

@ -80,6 +80,7 @@
"eslint-plugin-import": "^2.25.3", "eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsx-a11y": "^6.6.0", "eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-react": "^7.30.1", "eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"generate-package-json-webpack-plugin": "^2.6.0", "generate-package-json-webpack-plugin": "^2.6.0",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"ttag-cli": "^1.9.3", "ttag-cli": "^1.9.3",
@ -5064,7 +5065,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@ -15151,7 +15151,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
"dev": true, "dev": true,
"peer": true,
"requires": {} "requires": {}
}, },
"eslint-scope": { "eslint-scope": {

View File

@ -94,6 +94,7 @@
"eslint-plugin-import": "^2.25.3", "eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsx-a11y": "^6.6.0", "eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-react": "^7.30.1", "eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"generate-package-json-webpack-plugin": "^2.6.0", "generate-package-json-webpack-plugin": "^2.6.0",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"ttag-cli": "^1.9.3", "ttag-cli": "^1.9.3",

View File

@ -6,17 +6,18 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import GlobalCaptcha from './GlobalCaptcha'; import GlobalCaptcha from './GlobalCaptcha';
import BanInfo from './BanInfo';
import { closeAlert } from '../store/actions'; import { closeAlert } from '../store/actions';
const Alert = () => { const Alert = () => {
const [render, setRender] = useState(false); const [render, setRender] = useState(false);
const { const {
alertOpen, open,
alertType, alertType,
alertTitle, title,
alertMessage, message,
alertBtn, btn,
} = useSelector((state) => state.alert); } = useSelector((state) => state.alert);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -25,20 +26,32 @@ const Alert = () => {
}, [dispatch]); }, [dispatch]);
const onTransitionEnd = () => { const onTransitionEnd = () => {
if (!alertOpen) setRender(false); if (!open) setRender(false);
}; };
useEffect(() => { useEffect(() => {
window.setTimeout(() => { window.setTimeout(() => {
if (alertOpen) setRender(true); if (open) setRender(true);
}, 10); }, 10);
}, [alertOpen]); }, [open]);
let Content = null;
switch (alertType) {
case 'captcha':
Content = GlobalCaptcha;
break;
case 'ban':
Content = BanInfo;
break;
default:
// nothing
}
return ( return (
(render || alertOpen) && ( (render || open) && (
<div> <div>
<div <div
className={(alertOpen && render) className={(open && render)
? 'OverlayAlert show' ? 'OverlayAlert show'
: 'OverlayAlert'} : 'OverlayAlert'}
onTransitionEnd={onTransitionEnd} onTransitionEnd={onTransitionEnd}
@ -46,23 +59,21 @@ const Alert = () => {
onClick={close} onClick={close}
/> />
<div <div
className={(alertOpen && render) ? 'Alert show' : 'Alert'} className={(open && render) ? 'Alert show' : 'Alert'}
> >
<h2>{alertTitle}</h2> <h2>{title}</h2>
<p className="modaltext"> <p className="modaltext">
{alertMessage} {message}
</p> </p>
<div> <div>
{(alertType === 'captcha') {(Content) ? (
? <GlobalCaptcha close={close} /> <Content close={close} />
: ( ) : (
<button <button
type="button" type="button"
onClick={close} onClick={close}
> >{btn}</button>
{alertBtn} )}
</button>
)}
</div> </div>
</div> </div>
</div> </div>

100
src/components/BanInfo.jsx Normal file
View File

@ -0,0 +1,100 @@
/*
* get information about ban
*/
import React, { useState } from 'react';
import { t } from 'ttag';
import useInterval from './hooks/interval';
import {
largeDurationToString,
} from '../core/utils';
import { requestBanInfo } from '../store/actions/fetch';
const BanInfo = ({ close }) => {
const [errors, setErrors] = useState([]);
const [reason, setReason] = useState('');
const [expireTs, setExpireTs] = useState(0);
const [expire, setExpire] = useState(null);
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (evt) => {
evt.preventDefault();
if (submitting) {
return;
}
setSubmitting(true);
const info = await requestBanInfo();
setSubmitting(false);
if (info.errors) {
setErrors(info.errors);
return;
}
const {
ts,
reason: newReason,
} = info;
setExpireTs(ts);
const tsDate = new Date(ts);
setExpire(tsDate.toLocaleString);
setReason(newReason);
};
useInterval(() => {
console.log('do');
if (expireTs) {
setExpireTs(expireTs - 1);
}
}, 1000);
return (
<div>
{errors.map((error) => (
<p key={error} className="errormessage">
<span>{t`Error`}</span>:&nbsp;{error}
</p>
))}
{(reason) && (
<>
<h3 className="modaltitle">{t`Reason`}:</h3>
<p className="modaltext">{reason}</p>
</>
)}
{(expireTs) && (
<>
<h3 className="modaltitle">{t`Duration`}:</h3>
<p className="modaltext">
{t`Your ban expires at `}
<span style={{ fontWeight: 'bold' }}>{expire}</span>
{t` which is in `}
<span
style={{ fontWeight: 'bold' }}
>
{largeDurationToString(expireTs)}
</span>
</p>
</>
)}
<p>
<button
type="button"
style={{ fontSize: 16 }}
onClick={handleSubmit}
>
{(submitting) ? '...' : t`Why?`}
</button>
&nbsp;
<button
type="submit"
style={{ fontSize: 16 }}
onClick={close}
>
{t`OK`}
</button>
</p>
</div>
);
};
export default React.memo(BanInfo);

View File

@ -19,10 +19,6 @@ function ChatMessage({
msg, msg,
ts, ts,
}) { }) {
if (!name) {
return null;
}
const dispatch = useDispatch(); const dispatch = useDispatch();
const isDarkMode = useSelector( const isDarkMode = useSelector(
(state) => state.gui.style.indexOf('dark') !== -1, (state) => state.gui.style.indexOf('dark') !== -1,

View File

@ -11,10 +11,6 @@ import { t } from 'ttag';
import { MONTH } from '../core/constants'; import { MONTH } from '../core/constants';
function LanguageSelect() { function LanguageSelect() {
if (!navigator.cookieEnabled) {
return null;
}
const { lang, langs } = window.ssv; const { lang, langs } = window.ssv;
const [langSel, setLangSel] = useState(lang); const [langSel, setLangSel] = useState(lang);
@ -30,6 +26,10 @@ function LanguageSelect() {
} }
}, [langSel]); }, [langSel]);
if (!navigator.cookieEnabled) {
return null;
}
return ( return (
<div style={{ textAlign: 'right' }}> <div style={{ textAlign: 'right' }}>
<span> <span>

View File

@ -240,7 +240,7 @@ function ModCanvastools() {
</span> </span>
</div> </div>
)} )}
<p className="modalcotext">Choose Canvas:&nbsp; <p className="modalcotext">{t`Choose Canvas`}:&nbsp;
<select <select
value={selectedCanvas} value={selectedCanvas}
onChange={(e) => { onChange={(e) => {
@ -248,19 +248,14 @@ function ModCanvastools() {
selectCanvas(sel.options[sel.selectedIndex].value); selectCanvas(sel.options[sel.selectedIndex].value);
}} }}
> >
{ {Object.keys(canvases).filter((c) => !canvases[c].v).map((canvas) => (
Object.keys(canvases).map((canvas) => ((canvases[canvas].v) <option
? null key={canvas}
: ( value={canvas}
<option >
value={canvas} {canvases[canvas].title}
> </option>
{ ))}
canvases[canvas].title
}
</option>
)))
}
</select> </select>
</p> </p>
<div className="modaldivider" /> <div className="modaldivider" />

View File

@ -5,13 +5,20 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { t } from 'ttag'; import { t } from 'ttag';
import { parseInterval } from '../core/utils';
async function submitIIDAction( async function submitIIDAction(
action, action,
iid, iid,
reason,
duration,
callback, callback,
) { ) {
const time = Date.now() + parseInterval(duration);
const data = new FormData(); const data = new FormData();
data.append('iidaction', action); data.append('iidaction', action);
data.append('reason', reason);
data.append('time', time);
data.append('iid', iid); data.append('iid', iid);
const resp = await fetch('./api/modtools', { const resp = await fetch('./api/modtools', {
credentials: 'include', credentials: 'include',
@ -24,6 +31,8 @@ async function submitIIDAction(
function ModIIDtools() { function ModIIDtools() {
const [iIDAction, selectIIDAction] = useState('givecaptcha'); const [iIDAction, selectIIDAction] = useState('givecaptcha');
const [iid, selectIid] = useState(''); const [iid, selectIid] = useState('');
const [reason, setReason] = useState('');
const [duration, setDuration] = useState('1d');
const [resp, setResp] = useState(''); const [resp, setResp] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@ -37,15 +46,47 @@ function ModIIDtools() {
selectIIDAction(sel.options[sel.selectedIndex].value); selectIIDAction(sel.options[sel.selectedIndex].value);
}} }}
> >
{['givecaptcha'] {['givecaptcha', 'ban', 'unban', 'whitelist', 'unwhitelist']
.map((opt) => ( .map((opt) => (
<option <option
key={opt}
value={opt} value={opt}
> >
{opt} {opt}
</option> </option>
))} ))}
</select> </select>
{(iIDAction === 'ban') && (
<>
<p>{t`Reason`}</p>
<input
style={{
width: '100%',
}}
value={reason}
placeholder={t`Enter Reason`}
onChange={(evt) => {
setReason(evt.target.value.trim());
}}
/>
<p>
{`${t`Duration`}: `}
<input
style={{
display: 'inline-block',
width: '100%',
maxWidth: '7em',
}}
value={duration}
placeholder="1d"
onChange={(evt) => {
setDuration(evt.target.value.trim());
}}
/>
{t`(0 = infinite)`}
</p>
</>
)}
<p className="modalcotext"> <p className="modalcotext">
{' IID: '} {' IID: '}
<input <input
@ -58,8 +99,7 @@ function ModIIDtools() {
type="text" type="text"
placeholder="xxxx-xxxxx-xxxx" placeholder="xxxx-xxxxx-xxxx"
onChange={(evt) => { onChange={(evt) => {
const newIid = evt.target.value.trim(); selectIid(evt.target.value.trim());
selectIid(newIid);
}} }}
/> />
<button <button
@ -72,6 +112,8 @@ function ModIIDtools() {
submitIIDAction( submitIIDAction(
iIDAction, iIDAction,
iid, iid,
reason,
duration,
(ret) => { (ret) => {
setSubmitting(false); setSubmitting(false);
setResp(ret); setResp(ret);
@ -87,7 +129,6 @@ function ModIIDtools() {
width: '100%', width: '100%',
}} }}
rows={(resp) ? resp.split('\n').length : 10} rows={(resp) ? resp.split('\n').length : 10}
id="iparea"
value={resp} value={resp}
readOnly readOnly
/> />

View File

@ -7,6 +7,8 @@ import React, { useState, useEffect } from 'react';
import { useSelector, shallowEqual } from 'react-redux'; import { useSelector, shallowEqual } from 'react-redux';
import { t } from 'ttag'; import { t } from 'ttag';
import { parseInterval } from '../core/utils';
const keepState = { const keepState = {
tlcoords: '', tlcoords: '',
brcoords: '', brcoords: '',
@ -14,28 +16,6 @@ const keepState = {
iid: '', iid: '',
}; };
/*
* parse interval in s/m/h to timestamp
*/
function parseInterval(interval) {
if (!interval) {
return null;
}
const lastChar = interval.slice(-1).toLowerCase();
const num = parseInt(interval.slice(0, -1), 10);
if (Number.isNaN(num) || num <= 0 || num > 600
|| !['s', 'm', 'h'].includes(lastChar)) {
return null;
}
let factor = 1000;
if (lastChar === 'm') {
factor *= 60;
} else if (lastChar === 'h') {
factor *= 3600;
}
return Date.now() - (num * factor);
}
/* /*
* sorting function for array sort * sorting function for array sort
*/ */
@ -59,11 +39,12 @@ async function submitWatchAction(
iid, iid,
callback, callback,
) { ) {
const time = parseInterval(interval); let time = parseInterval(interval);
if (!time) { if (!time) {
callback({ info: t`Interval is invalid` }); callback({ info: t`Interval is invalid` });
return; return;
} }
time = Date.now() - time;
const data = new FormData(); const data = new FormData();
data.append('watchaction', action); data.append('watchaction', action);
data.append('canvasid', canvas); data.append('canvasid', canvas);
@ -147,19 +128,14 @@ function ModWatchtools() {
selectCanvas(sel.options[sel.selectedIndex].value); selectCanvas(sel.options[sel.selectedIndex].value);
}} }}
> >
{ {Object.keys(canvases).filter((c) => !canvases[c].v).map((canvas) => (
Object.keys(canvases).map((canvas) => ((canvases[canvas].v) <option
? null key={canvas}
: ( value={canvas}
<option >
value={canvas} {canvases[canvas].title}
> </option>
{ ))}
canvases[canvas].title
}
</option>
)))
}
</select> </select>
{` ${t`Interval`}: `} {` ${t`Interval`}: `}
<input <input
@ -306,6 +282,7 @@ function ModWatchtools() {
<tr> <tr>
{columns.slice(1).map((col, ind) => ( {columns.slice(1).map((col, ind) => (
<th <th
key={col}
style={ style={
(sortBy - 1 === ind) (sortBy - 1 === ind)
? { fontWeight: 'normal' } ? { fontWeight: 'normal' }

View File

@ -18,8 +18,8 @@ import ChannelContextMenu from './contextmenus/ChannelContextMenu';
const CONTEXT_MENUS = { const CONTEXT_MENUS = {
USER: <UserContextMenu />, USER: UserContextMenu,
CHANNEL: <ChannelContextMenu />, CHANNEL: ChannelContextMenu,
/* other context menus */ /* other context menus */
}; };
@ -38,24 +38,26 @@ const UI = () => {
state.contextMenu.menuType, state.contextMenu.menuType,
], shallowEqual); ], shallowEqual);
const contextMenu = (menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null; const ContextMenu = menuOpen && menuType && CONTEXT_MENUS[menuType];
if (isHistoricalView) { return (
return [ <>
<HistorySelect />, <Alert />
contextMenu, {(isHistoricalView) ? (
]; <HistorySelect />
} ) : (
return [ <>
<Alert />, <PalselButton />
<PalselButton />, <Palette />
<Palette />, {(!is3D) && <GlobeButton />}
(!is3D) && <GlobeButton />, {(is3D && isOnMobile) && <Mobile3DControls />}
(is3D && isOnMobile) && <Mobile3DControls />, <CoolDownBox />
<CoolDownBox />, </>
<NotifyBox />, )}
contextMenu, <NotifyBox />
]; {ContextMenu && <ContextMenu />}
</>
);
}; };
export default React.memo(UI); export default React.memo(UI);

View File

@ -186,6 +186,7 @@ const ChannelDropDown = ({
const [cid, unreadCh, name] = ch; const [cid, unreadCh, name] = ch;
return ( return (
<div <div
key={cid}
onClick={() => setChatChannel(cid)} onClick={() => setChatChannel(cid)}
className={ className={
`chn${ `chn${

View File

@ -64,6 +64,7 @@ const SettingsItemSelect = ({
{ {
values.map((value) => ( values.map((value) => (
<option <option
key={value}
value={value} value={value}
> >
{value} {value}

View File

@ -11,7 +11,7 @@ import {
import { findIdByNameOrId } from '../data/sql/RegUser'; import { findIdByNameOrId } from '../data/sql/RegUser';
import ChatMessageBuffer from './ChatMessageBuffer'; import ChatMessageBuffer from './ChatMessageBuffer';
import socketEvents from '../socket/SocketEvents'; import socketEvents from '../socket/SocketEvents';
import cheapDetector from './isProxy'; import checkIPAllowed from './isAllowed';
import { DailyCron } from '../utils/cron'; import { DailyCron } from '../utils/cron';
import { escapeMd } from './utils'; import { escapeMd } from './utils';
import ttags from './ttag'; import ttags from './ttag';
@ -380,11 +380,21 @@ export class ChatProvider {
return null; return null;
} }
if (await cheapDetector(user.ip)) { const allowed = await checkIPAllowed(user.ip);
if (!allowed.allowed) {
logger.info( logger.info(
`${name} / ${user.ip} tried to send chat message with proxy`, `${name} / ${user.ip} tried to send chat message but is not allowed`,
); );
return t`You can not send chat messages with proxy`; switch (allowed.status) {
case 1:
return t`You can not send chat messages with proxy`;
case 2:
return t`You are banned`;
case 3:
return t`Your Internet Provider is banned`;
default:
return t`You are not allowed to use chat`;
}
} }
if (message.charAt(0) === '/' && user.userlvl) { if (message.charAt(0) === '/' && user.userlvl) {

View File

@ -12,7 +12,7 @@ import redis from '../data/redis/client';
import { getIPv6Subnet } from '../utils/ip'; import { getIPv6Subnet } from '../utils/ip';
import { validateCoorRange } from '../utils/validation'; import { validateCoorRange } from '../utils/validation';
import CanvasCleaner from './CanvasCleaner'; import CanvasCleaner from './CanvasCleaner';
import { Blacklist, Whitelist, RegUser } from '../data/sql'; import { Whitelist, RegUser } from '../data/sql';
import { getIPofIID } from '../data/sql/IPInfo'; import { getIPofIID } from '../data/sql/IPInfo';
import { forceCaptcha } from '../data/redis/captcha'; import { forceCaptcha } from '../data/redis/captcha';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
@ -61,20 +61,6 @@ export async function executeIPAction(action, ips, logger = null) {
if (logger) logger(`${action} ${ip}`); if (logger) logger(`${action} ${ip}`);
switch (action) { switch (action) {
case 'ban':
await Blacklist.findOrCreate({
where: { ip: ipKey },
});
await redis.set(key, 'y', {
EX: 24 * 3600,
});
break;
case 'unban':
await Blacklist.destroy({
where: { ip: ipKey },
});
await redis.del(key);
break;
case 'whitelist': case 'whitelist':
await Whitelist.findOrCreate({ await Whitelist.findOrCreate({
where: { ip: ipKey }, where: { ip: ipKey },

View File

@ -130,6 +130,7 @@ export async function drawByOffsets(
} }
} }
if (canvas.req === 'top' && !rankings.prevTop.includes(user.id)) { if (canvas.req === 'top' && !rankings.prevTop.includes(user.id)) {
// not in top ten
throw new Error(12); throw new Error(12);
} }
} }

View File

@ -1,9 +1,17 @@
/*
* decide if IP is allowed
* does proxycheck and check bans and whitelists
*/
import fetch from '../utils/proxiedFetch'; import fetch from '../utils/proxiedFetch';
import redis from '../data/redis/client';
import { getIPv6Subnet } from '../utils/ip'; import { getIPv6Subnet } from '../utils/ip';
import whois from '../utils/whois'; import whois from '../utils/whois';
import { Blacklist, Whitelist, IPInfo } from '../data/sql'; import { Whitelist, IPInfo } from '../data/sql';
import { isIPBanned } from '../data/sql/Ban';
import {
cacheAllowed,
getCacheAllowed,
} from '../data/redis/isAllowedCache';
import { proxyLogger as logger } from './logger'; import { proxyLogger as logger } from './logger';
import { USE_PROXYCHECK } from './config'; import { USE_PROXYCHECK } from './config';
@ -78,21 +86,6 @@ async function getProxyCheck(ip) {
]; ];
} }
/*
* check MYSQL Blacklist table
* @param ip IP to check
* @return true if blacklisted
*/
async function isBlacklisted(ip) {
const count = await Blacklist
.count({
where: {
ip,
},
});
return count !== 0;
}
/* /*
* check MYSQL Whitelist table * check MYSQL Whitelist table
* @param ip IP to check * @param ip IP to check
@ -135,21 +128,27 @@ async function saveIPInfo(ip, isProxy, info) {
* @return true if proxy or blacklisted, false if not or whitelisted * @return true if proxy or blacklisted, false if not or whitelisted
*/ */
async function withoutCache(f, ip) { async function withoutCache(f, ip) {
if (!ip) return true;
const ipKey = getIPv6Subnet(ip); const ipKey = getIPv6Subnet(ip);
let result; let allowed;
let info; let status;
let pcInfo;
if (await isWhitelisted(ipKey)) { if (await isWhitelisted(ipKey)) {
result = false; allowed = false;
info = 'wl'; pcInfo = 'wl';
} else if (await isBlacklisted(ipKey)) { status = -1;
result = true; } else if (await isIPBanned(ipKey)) {
info = 'bl'; allowed = true;
pcInfo = 'bl';
status = 2;
} else { } else {
[result, info] = await f(ip); [allowed, pcInfo] = await f(ip);
status = (allowed) ? 1 : 0;
} }
saveIPInfo(ipKey, result, info); saveIPInfo(ipKey, allowed, pcInfo);
return result; return {
allowed,
status,
};
} }
/* /*
@ -162,13 +161,17 @@ async function withoutCache(f, ip) {
let lock = 4; let lock = 4;
const checking = []; const checking = [];
async function withCache(f, ip) { async function withCache(f, ip) {
if (!ip || ip === '0.0.0.1') return true; if (!ip || ip === '0.0.0.1') {
return {
allowed: false,
status: 4,
};
}
// get from cache, if there // get from cache, if there
const ipKey = getIPv6Subnet(ip); const ipKey = getIPv6Subnet(ip);
const key = `isprox:${ipKey}`; const cache = await getCacheAllowed(ipKey);
const cache = await redis.get(key);
if (cache) { if (cache) {
return cache === 'y'; return cache;
} }
// else make asynchronous ipcheck and assume no proxy in the meantime // else make asynchronous ipcheck and assume no proxy in the meantime
@ -179,28 +182,42 @@ async function withCache(f, ip) {
checking.push(ipKey); checking.push(ipKey);
try { try {
const result = await withoutCache(f, ip); const result = await withoutCache(f, ip);
const value = result ? 'y' : 'n'; cacheAllowed(ip, result);
redis.set(key, value, {
EX: 3 * 24 * 3600,
}); // cache for three days
const pos = checking.indexOf(ipKey);
if (~pos) checking.splice(pos, 1);
} catch (error) { } catch (error) {
logger.error('Error %s', error.message || error); logger.error('Error %s', error.message || error);
} finally {
const pos = checking.indexOf(ipKey); const pos = checking.indexOf(ipKey);
if (~pos) checking.splice(pos, 1); if (~pos) checking.splice(pos, 1);
} finally {
lock += 1; lock += 1;
} }
} }
return false; return {
allowed: true,
status: -2,
};
} }
function cheapDetector(ip) { /*
if (USE_PROXYCHECK) { * check if ip is allowed
return withCache(getProxyCheck, ip); * @param ip IP
* @param disableCache if we fetch result from cache
* @return {
* allowed: boolean if allowed to use site
* , status: -2: not yet checked
* -1: whitelisted
* 0: allowed, no proxy
* 1 is proxy
* 2: is banned
* 3: is rangebanned
* 4: invalid ip
* }
*/
function checkIfAllowed(ip, disableCache = false) {
const checker = (USE_PROXYCHECK) ? getProxyCheck : dummy;
if (disableCache) {
return withoutCache(checker, ip);
} }
return withCache(dummy, ip); return withCache(checker, ip);
} }
export default cheapDetector; export default checkIfAllowed;

View File

@ -223,6 +223,10 @@ export function worldToScreen(
]; ];
} }
/*
* parses duration to string
* in xx:xx format with min:sec
*/
export function durationToString( export function durationToString(
ms, ms,
smallest = false, smallest = false,
@ -238,6 +242,38 @@ export function durationToString(
return timestring; return timestring;
} }
/*
* parses a large duration to
* [x]h [y]min [z]sec format
*/
export function largeDurationToString(
ts,
) {
let restA = Math.round(ts / 1000);
let restB = restA % (3600 * 24);
const days = restA - restB;
restA = restB % 3600;
const hours = restB - restA;
restB = restA % 60;
const minutes = restA - restB;
restA = restB % 60;
const seconds = restB - restA;
let out = '';
if (days) {
out += ` ${days}d`;
}
if (hours) {
out += ` ${hours}h`;
}
if (minutes) {
out += ` ${minutes}min`;
}
if (seconds) {
out += ` ${seconds}s`;
}
return out;
}
const postfix = ['k', 'M', 'B']; const postfix = ['k', 'M', 'B'];
export function numberToString(num) { export function numberToString(num) {
if (!num) { if (!num) {
@ -433,3 +469,31 @@ export function getDateTimeString(timestamp) {
} }
return date.toLocaleTimeString(); return date.toLocaleTimeString();
} }
/*
* parse interval in s/m/h form to timestamp
* @param interval string like "2d"
* @return timestamp of now - interval
*/
export function parseInterval(interval) {
if (!interval) {
return 0;
}
const lastChar = interval.slice(-1).toLowerCase();
const num = parseInt(interval.slice(0, -1), 10);
if (Number.isNaN(num) || num <= 0 || num > 600
|| !['s', 'm', 'h', 'd'].includes(lastChar)) {
return 0;
}
let factor = 1000;
if (lastChar === 'm') {
factor *= 60;
} else if (lastChar === 'h') {
factor *= 3600;
} else if (lastChar === 'd') {
factor *= 3600 * 24;
}
return (num * factor);
}

View File

@ -4,7 +4,7 @@
*/ */
import logger from '../../core/logger'; import logger from '../../core/logger';
import redis from './client'; import client from './client';
import { getIPv6Subnet } from '../../utils/ip'; import { getIPv6Subnet } from '../../utils/ip';
import { import {
CAPTCHA_TIME, CAPTCHA_TIME,
@ -79,7 +79,7 @@ export async function setCaptchaSolution(
key += `:${captchaid}`; key += `:${captchaid}`;
} }
try { try {
await redis.set(key, text, { await client.set(key, text, {
EX: CAPTCHA_TIMEOUT, EX: CAPTCHA_TIMEOUT,
}); });
} catch (error) { } catch (error) {
@ -112,7 +112,7 @@ export async function checkCaptchaSolution(
if (captchaid) { if (captchaid) {
key += `:${captchaid}`; key += `:${captchaid}`;
} }
const solution = await redis.get(key); const solution = await client.get(key);
if (solution) { if (solution) {
if (evaluateResult(solution, text)) { if (evaluateResult(solution, text)) {
if (Math.random() < 0.1) { if (Math.random() < 0.1) {
@ -120,7 +120,7 @@ export async function checkCaptchaSolution(
} }
if (!onetime) { if (!onetime) {
const solvkey = `human:${ipn}`; const solvkey = `human:${ipn}`;
await redis.set(solvkey, '', { await client.set(solvkey, '', {
EX: TTL_CACHE, EX: TTL_CACHE,
}); });
} }
@ -149,7 +149,7 @@ export async function needCaptcha(ip) {
return false; return false;
} }
const key = `human:${getIPv6Subnet(ip)}`; const key = `human:${getIPv6Subnet(ip)}`;
const ttl = await redis.ttl(key); const ttl = await client.ttl(key);
if (ttl > 0) { if (ttl > 0) {
return false; return false;
} }
@ -168,6 +168,6 @@ export async function forceCaptcha(ip) {
return null; return null;
} }
const key = `human:${getIPv6Subnet(ip)}`; const key = `human:${getIPv6Subnet(ip)}`;
const ret = await redis.del(key); const ret = await client.del(key);
return (ret > 0); return (ret > 0);
} }

View File

@ -0,0 +1,29 @@
/*
* cache allowed ips
* used for proxychecker and banlist
*/
import client from './client';
const PREFIX = 'isal:';
const CACHE_DURATION = 3 * 24 * 3600;
export function cacheAllowed(ip, allowed) {
const key = `${PREFIX}:${ip}`;
return client.set(key, allowed.status, {
EX: CACHE_DURATION,
});
}
export async function getCacheAllowed(ip) {
const key = `${PREFIX}:${ip}`;
let cache = await client.get(key);
if (!cache) {
return null;
}
cache = parseInt(cache, 10);
return {
allowed: (cache <= 0),
status: cache,
};
}

View File

@ -33,4 +33,12 @@ const Ban = sequelize.define('Blacklist', {
updatedAt: false, updatedAt: false,
}); });
export async function isIPBanned(ip) {
const count = await Ban
.count({
where: { ip },
});
return count !== 0;
}
export default Ban; export default Ban;

View File

@ -35,6 +35,12 @@ const Message = sequelize.define('Message', {
}, },
}, { }, {
updatedAt: false, updatedAt: false,
setterMethods: {
message(value) {
this.setDataValue('message', value.slice(0, 200));
},
},
}); });
Message.belongsTo(Channel, { Message.belongsTo(Channel, {

View File

@ -1,4 +1,3 @@
import Blacklist from './Blacklist';
import Whitelist from './Whitelist'; import Whitelist from './Whitelist';
import RegUser from './RegUser'; import RegUser from './RegUser';
import Channel from './Channel'; import Channel from './Channel';
@ -38,7 +37,6 @@ RegUser.belongsToMany(RegUser, {
export { export {
Whitelist, Whitelist,
Blacklist,
RegUser, RegUser,
Channel, Channel,
UserChannel, UserChannel,

View File

@ -7,7 +7,7 @@ import getMe from '../../core/me';
import { import {
USE_PROXYCHECK, USE_PROXYCHECK,
} from '../../core/config'; } from '../../core/config';
import cheapDetector from '../../core/isProxy'; import checkIPAllowed from '../../core/isAllowed';
export default async (req, res, next) => { export default async (req, res, next) => {
@ -17,10 +17,10 @@ export default async (req, res, next) => {
user.updateLogInTimestamp(); user.updateLogInTimestamp();
const { trueIp: ip } = req; const { trueIp: ip } = req;
if (USE_PROXYCHECK && ip !== '0.0.0.1') { if (USE_PROXYCHECK) {
// pre-fire cheap Detector to give it time to get a real result // pre-fire ip check to give it time to get a real result
// once api_pixel needs it // once api_pixel needs it
cheapDetector(ip); checkIPAllowed(ip);
} }
// https://stackoverflow.com/questions/49547/how-to-control-web-page-caching-across-all-browsers // https://stackoverflow.com/questions/49547/how-to-control-web-page-caching-across-all-browsers

View File

@ -25,7 +25,7 @@ import chatProvider, { ChatProvider } from '../core/ChatProvider';
import authenticateClient from './authenticateClient'; import authenticateClient from './authenticateClient';
import { drawByOffsets } from '../core/draw'; import { drawByOffsets } from '../core/draw';
import { needCaptcha } from '../data/redis/captcha'; import { needCaptcha } from '../data/redis/captcha';
import cheapDetector from '../core/isProxy'; import isIPAllowed from '../core/isAllowed';
const ipCounter = new Counter(); const ipCounter = new Counter();
@ -95,7 +95,7 @@ class SocketServer {
ws.name = user.getName(); ws.name = user.getName();
const { ip } = user; const { ip } = user;
cheapDetector(ip); isIPAllowed(ip);
ws.send(OnlineCounter.dehydrate(socketEvents.onlineCounter)); ws.send(OnlineCounter.dehydrate(socketEvents.onlineCounter));
@ -493,8 +493,18 @@ class SocketServer {
failureRet = PixelReturn.dehydrate(10, 0, 0); failureRet = PixelReturn.dehydrate(10, 0, 0);
} }
// (re)check for Proxy // (re)check for Proxy
if (await cheapDetector(ip)) { const allowed = await isIPAllowed(ip);
failureRet = PixelReturn.dehydrate(11, 0, 0); if (!allowed.allowed) {
// proxy
let failureStatus = 11;
if (allowed.status === 2) {
// banned
failureStatus = 14;
} else if (allowed.status === 3) {
// range banned
failureStatus = 15;
}
failureRet = PixelReturn.dehydrate(failureStatus, 0, 0);
} }
if (failureRet !== null) { if (failureRet !== null) {
const now = Date.now(); const now = Date.now();

View File

@ -7,9 +7,9 @@ export type Action =
{ type: 'LOGGED_OUT' } { type: 'LOGGED_OUT' }
| { type: 'ALERT', | { type: 'ALERT',
title: string, title: string,
text: string, message: string,
icon: string, alertType: string,
confirmButtonText: string, btn: string,
} }
| { type: 'CLOSE_ALERT' } | { type: 'CLOSE_ALERT' }
| { type: 'TOGGLE_GRID' } | { type: 'TOGGLE_GRID' }

View File

@ -282,6 +282,12 @@ export function requestRankings() {
); );
} }
export function requestBanInfo() {
return makeAPIGETRequest(
'api/baninfo',
);
}
export function requestMe() { export function requestMe() {
return makeAPIGETRequest( return makeAPIGETRequest(
'api/me', 'api/me',

View File

@ -9,18 +9,18 @@ import {
requestMe, requestMe,
} from './fetch'; } from './fetch';
export function sweetAlert( export function pAlert(
title, title,
text, message,
icon, alertType,
confirmButtonText, btn = t`OK`,
) { ) {
return { return {
type: 'ALERT', type: 'ALERT',
title, title,
text, message,
icon, alertType,
confirmButtonText, btn,
}; };
} }
@ -871,7 +871,7 @@ export function startDm(windowId, query) {
dispatch(setApiFetching(true)); dispatch(setApiFetching(true));
const res = await requestStartDm(query); const res = await requestStartDm(query);
if (typeof res === 'string') { if (typeof res === 'string') {
dispatch(sweetAlert( dispatch(pAlert(
'Direct Message Error', 'Direct Message Error',
res, res,
'error', 'error',
@ -902,7 +902,7 @@ export function setUserBlock(
dispatch(setApiFetching(true)); dispatch(setApiFetching(true));
const res = await requestBlock(userId, block); const res = await requestBlock(userId, block);
if (res) { if (res) {
dispatch(sweetAlert( dispatch(pAlert(
'User Block Error', 'User Block Error',
res, res,
'error', 'error',
@ -924,7 +924,7 @@ export function setBlockingDm(
dispatch(setApiFetching(true)); dispatch(setApiFetching(true));
const res = await requestBlockDm(block); const res = await requestBlockDm(block);
if (res) { if (res) {
dispatch(sweetAlert( dispatch(pAlert(
'Blocking DMs Error', 'Blocking DMs Error',
res, res,
'error', 'error',
@ -944,7 +944,7 @@ export function setLeaveChannel(
dispatch(setApiFetching(true)); dispatch(setApiFetching(true));
const res = await requestLeaveChan(cid); const res = await requestLeaveChan(cid);
if (res) { if (res) {
dispatch(sweetAlert( dispatch(pAlert(
'Leaving Channel Error', 'Leaving Channel Error',
res, res,
'error', 'error',

View File

@ -1,9 +1,9 @@
const initialState = { const initialState = {
alertOpen: false, open: false,
alertType: null, alertType: null,
alertTitle: null, title: null,
alertMessage: null, message: null,
alertBtn: null, btn: null,
}; };
export default function alert( export default function alert(
@ -13,23 +13,23 @@ export default function alert(
switch (action.type) { switch (action.type) {
case 'ALERT': { case 'ALERT': {
const { const {
title, text, icon, confirmButtonText, title, message, alertType, btn,
} = action; } = action;
return { return {
...state, ...state,
alertOpen: true, open: true,
alertTitle: title, title,
alertMessage: text, message,
alertType: icon, alertType,
alertBtn: confirmButtonText, btn,
}; };
} }
case 'CLOSE_ALERT': { case 'CLOSE_ALERT': {
return { return {
...state, ...state,
alertOpen: false, open: false,
}; };
} }

View File

@ -8,7 +8,7 @@ import { t } from 'ttag';
import { import {
notify, notify,
setRequestingPixel, setRequestingPixel,
sweetAlert, pAlert,
gotCoolDownDelta, gotCoolDownDelta,
pixelFailure, pixelFailure,
setWait, setWait,
@ -48,11 +48,10 @@ export function requestFromQueue(store) {
pixelQueue = []; pixelQueue = [];
pixelTimeout = null; pixelTimeout = null;
store.dispatch(setRequestingPixel(true)); store.dispatch(setRequestingPixel(true));
store.dispatch(sweetAlert( store.dispatch(pAlert(
t`Error :(`, t`Error :(`,
t`Didn't get an answer from pixelplanet. Maybe try to refresh?`, t`Didn't get an answer from pixelplanet. Maybe try to refresh?`,
'error', 'error',
t`OK`,
)); ));
}, 15000); }, 15000);
@ -229,11 +228,10 @@ export function receivePixelReturn(
store.dispatch(pixelWait()); store.dispatch(pixelWait());
break; break;
case 10: case 10:
store.dispatch(sweetAlert( store.dispatch(pAlert(
'Captcha', 'Captcha',
t`Please prove that you are human`, t`Please prove that you are human`,
'captcha', 'captcha',
t`OK`,
)); ));
store.dispatch(setRequestingPixel(true)); store.dispatch(setRequestingPixel(true));
return; return;
@ -250,6 +248,18 @@ export function receivePixelReturn(
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
msg = t`Server got confused by your pixels. Are you playing on multiple devices?`; msg = t`Server got confused by your pixels. Are you playing on multiple devices?`;
break; break;
case 14:
store.dispatch(pAlert(
'Banned',
t`You are banned.`,
'ban',
));
store.dispatch(setRequestingPixel(true));
return;
case 15:
errorTitle = t`Range Banned`;
msg = t`Your Internet Provider is banned from playing this game`;
break;
default: default:
errorTitle = t`Weird`; errorTitle = t`Weird`;
msg = t`Couldn't set Pixel`; msg = t`Couldn't set Pixel`;
@ -257,11 +267,10 @@ export function receivePixelReturn(
if (msg) { if (msg) {
store.dispatch(pixelFailure()); store.dispatch(pixelFailure());
store.dispatch(sweetAlert( store.dispatch(pAlert(
(errorTitle || t`Error ${retCode}`), (errorTitle || t`Error ${retCode}`),
msg, msg,
'error', 'error',
t`OK`,
)); ));
} }

View File

@ -8,7 +8,7 @@
import { t } from 'ttag'; import { t } from 'ttag';
import Renderer2D from './Renderer2D'; import Renderer2D from './Renderer2D';
import { sweetAlert } from '../store/actions'; import { pAlert } from '../store/actions';
import { isWebGL2Available } from '../core/utils'; import { isWebGL2Available } from '../core/utils';
const dummyRenderer = { const dummyRenderer = {
@ -31,7 +31,7 @@ export async function initRenderer(store, is3D) {
renderer.destructor(); renderer.destructor();
if (is3D) { if (is3D) {
if (!isWebGL2Available()) { if (!isWebGL2Available()) {
store.dispatch(sweetAlert( store.dispatch(pAlert(
t`Canvas Error`, t`Canvas Error`,
t`Can't render 3D canvas, do you have WebGL2 disabled?`, t`Can't render 3D canvas, do you have WebGL2 disabled?`,
'error', 'error',