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": [
"react",
"react-hooks",
"jsx-a11y",
"import"
],
@ -32,7 +33,9 @@
"no-mixed-operators":"off",
"react/prop-types": "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/no-static-element-interactions":"off",
"no-continue": "off",

3
package-lock.json generated
View File

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

View File

@ -94,6 +94,7 @@
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"generate-package-json-webpack-plugin": "^2.6.0",
"mkdirp": "^1.0.4",
"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 GlobalCaptcha from './GlobalCaptcha';
import BanInfo from './BanInfo';
import { closeAlert } from '../store/actions';
const Alert = () => {
const [render, setRender] = useState(false);
const {
alertOpen,
open,
alertType,
alertTitle,
alertMessage,
alertBtn,
title,
message,
btn,
} = useSelector((state) => state.alert);
const dispatch = useDispatch();
@ -25,20 +26,32 @@ const Alert = () => {
}, [dispatch]);
const onTransitionEnd = () => {
if (!alertOpen) setRender(false);
if (!open) setRender(false);
};
useEffect(() => {
window.setTimeout(() => {
if (alertOpen) setRender(true);
if (open) setRender(true);
}, 10);
}, [alertOpen]);
}, [open]);
let Content = null;
switch (alertType) {
case 'captcha':
Content = GlobalCaptcha;
break;
case 'ban':
Content = BanInfo;
break;
default:
// nothing
}
return (
(render || alertOpen) && (
(render || open) && (
<div>
<div
className={(alertOpen && render)
className={(open && render)
? 'OverlayAlert show'
: 'OverlayAlert'}
onTransitionEnd={onTransitionEnd}
@ -46,23 +59,21 @@ const Alert = () => {
onClick={close}
/>
<div
className={(alertOpen && render) ? 'Alert show' : 'Alert'}
className={(open && render) ? 'Alert show' : 'Alert'}
>
<h2>{alertTitle}</h2>
<h2>{title}</h2>
<p className="modaltext">
{alertMessage}
{message}
</p>
<div>
{(alertType === 'captcha')
? <GlobalCaptcha close={close} />
: (
<button
type="button"
onClick={close}
>
{alertBtn}
</button>
)}
{(Content) ? (
<Content close={close} />
) : (
<button
type="button"
onClick={close}
>{btn}</button>
)}
</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,
ts,
}) {
if (!name) {
return null;
}
const dispatch = useDispatch();
const isDarkMode = useSelector(
(state) => state.gui.style.indexOf('dark') !== -1,

View File

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

View File

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

View File

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

View File

@ -7,6 +7,8 @@ import React, { useState, useEffect } from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import { t } from 'ttag';
import { parseInterval } from '../core/utils';
const keepState = {
tlcoords: '',
brcoords: '',
@ -14,28 +16,6 @@ const keepState = {
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
*/
@ -59,11 +39,12 @@ async function submitWatchAction(
iid,
callback,
) {
const time = parseInterval(interval);
let time = parseInterval(interval);
if (!time) {
callback({ info: t`Interval is invalid` });
return;
}
time = Date.now() - time;
const data = new FormData();
data.append('watchaction', action);
data.append('canvasid', canvas);
@ -147,19 +128,14 @@ function ModWatchtools() {
selectCanvas(sel.options[sel.selectedIndex].value);
}}
>
{
Object.keys(canvases).map((canvas) => ((canvases[canvas].v)
? null
: (
<option
value={canvas}
>
{
canvases[canvas].title
}
</option>
)))
}
{Object.keys(canvases).filter((c) => !canvases[c].v).map((canvas) => (
<option
key={canvas}
value={canvas}
>
{canvases[canvas].title}
</option>
))}
</select>
{` ${t`Interval`}: `}
<input
@ -306,6 +282,7 @@ function ModWatchtools() {
<tr>
{columns.slice(1).map((col, ind) => (
<th
key={col}
style={
(sortBy - 1 === ind)
? { fontWeight: 'normal' }

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import {
import { findIdByNameOrId } from '../data/sql/RegUser';
import ChatMessageBuffer from './ChatMessageBuffer';
import socketEvents from '../socket/SocketEvents';
import cheapDetector from './isProxy';
import checkIPAllowed from './isAllowed';
import { DailyCron } from '../utils/cron';
import { escapeMd } from './utils';
import ttags from './ttag';
@ -380,11 +380,21 @@ export class ChatProvider {
return null;
}
if (await cheapDetector(user.ip)) {
const allowed = await checkIPAllowed(user.ip);
if (!allowed.allowed) {
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) {

View File

@ -12,7 +12,7 @@ import redis from '../data/redis/client';
import { getIPv6Subnet } from '../utils/ip';
import { validateCoorRange } from '../utils/validation';
import CanvasCleaner from './CanvasCleaner';
import { Blacklist, Whitelist, RegUser } from '../data/sql';
import { Whitelist, RegUser } from '../data/sql';
import { getIPofIID } from '../data/sql/IPInfo';
import { forceCaptcha } from '../data/redis/captcha';
// eslint-disable-next-line import/no-unresolved
@ -61,20 +61,6 @@ export async function executeIPAction(action, ips, logger = null) {
if (logger) logger(`${action} ${ip}`);
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':
await Whitelist.findOrCreate({
where: { ip: ipKey },

View File

@ -130,6 +130,7 @@ export async function drawByOffsets(
}
}
if (canvas.req === 'top' && !rankings.prevTop.includes(user.id)) {
// not in top ten
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 redis from '../data/redis/client';
import { getIPv6Subnet } from '../utils/ip';
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 { 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
* @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
*/
async function withoutCache(f, ip) {
if (!ip) return true;
const ipKey = getIPv6Subnet(ip);
let result;
let info;
let allowed;
let status;
let pcInfo;
if (await isWhitelisted(ipKey)) {
result = false;
info = 'wl';
} else if (await isBlacklisted(ipKey)) {
result = true;
info = 'bl';
allowed = false;
pcInfo = 'wl';
status = -1;
} else if (await isIPBanned(ipKey)) {
allowed = true;
pcInfo = 'bl';
status = 2;
} else {
[result, info] = await f(ip);
[allowed, pcInfo] = await f(ip);
status = (allowed) ? 1 : 0;
}
saveIPInfo(ipKey, result, info);
return result;
saveIPInfo(ipKey, allowed, pcInfo);
return {
allowed,
status,
};
}
/*
@ -162,13 +161,17 @@ async function withoutCache(f, ip) {
let lock = 4;
const checking = [];
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
const ipKey = getIPv6Subnet(ip);
const key = `isprox:${ipKey}`;
const cache = await redis.get(key);
const cache = await getCacheAllowed(ipKey);
if (cache) {
return cache === 'y';
return cache;
}
// else make asynchronous ipcheck and assume no proxy in the meantime
@ -179,28 +182,42 @@ async function withCache(f, ip) {
checking.push(ipKey);
try {
const result = await withoutCache(f, ip);
const value = result ? 'y' : 'n';
redis.set(key, value, {
EX: 3 * 24 * 3600,
}); // cache for three days
const pos = checking.indexOf(ipKey);
if (~pos) checking.splice(pos, 1);
cacheAllowed(ip, result);
} catch (error) {
logger.error('Error %s', error.message || error);
} finally {
const pos = checking.indexOf(ipKey);
if (~pos) checking.splice(pos, 1);
} finally {
lock += 1;
}
}
return false;
return {
allowed: true,
status: -2,
};
}
function cheapDetector(ip) {
if (USE_PROXYCHECK) {
return withCache(getProxyCheck, ip);
/*
* check if ip is allowed
* @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(
ms,
smallest = false,
@ -238,6 +242,38 @@ export function durationToString(
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'];
export function numberToString(num) {
if (!num) {
@ -433,3 +469,31 @@ export function getDateTimeString(timestamp) {
}
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 redis from './client';
import client from './client';
import { getIPv6Subnet } from '../../utils/ip';
import {
CAPTCHA_TIME,
@ -79,7 +79,7 @@ export async function setCaptchaSolution(
key += `:${captchaid}`;
}
try {
await redis.set(key, text, {
await client.set(key, text, {
EX: CAPTCHA_TIMEOUT,
});
} catch (error) {
@ -112,7 +112,7 @@ export async function checkCaptchaSolution(
if (captchaid) {
key += `:${captchaid}`;
}
const solution = await redis.get(key);
const solution = await client.get(key);
if (solution) {
if (evaluateResult(solution, text)) {
if (Math.random() < 0.1) {
@ -120,7 +120,7 @@ export async function checkCaptchaSolution(
}
if (!onetime) {
const solvkey = `human:${ipn}`;
await redis.set(solvkey, '', {
await client.set(solvkey, '', {
EX: TTL_CACHE,
});
}
@ -149,7 +149,7 @@ export async function needCaptcha(ip) {
return false;
}
const key = `human:${getIPv6Subnet(ip)}`;
const ttl = await redis.ttl(key);
const ttl = await client.ttl(key);
if (ttl > 0) {
return false;
}
@ -168,6 +168,6 @@ export async function forceCaptcha(ip) {
return null;
}
const key = `human:${getIPv6Subnet(ip)}`;
const ret = await redis.del(key);
const ret = await client.del(key);
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,
});
export async function isIPBanned(ip) {
const count = await Ban
.count({
where: { ip },
});
return count !== 0;
}
export default Ban;

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import getMe from '../../core/me';
import {
USE_PROXYCHECK,
} from '../../core/config';
import cheapDetector from '../../core/isProxy';
import checkIPAllowed from '../../core/isAllowed';
export default async (req, res, next) => {
@ -17,10 +17,10 @@ export default async (req, res, next) => {
user.updateLogInTimestamp();
const { trueIp: ip } = req;
if (USE_PROXYCHECK && ip !== '0.0.0.1') {
// pre-fire cheap Detector to give it time to get a real result
if (USE_PROXYCHECK) {
// pre-fire ip check to give it time to get a real result
// once api_pixel needs it
cheapDetector(ip);
checkIPAllowed(ip);
}
// 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 { drawByOffsets } from '../core/draw';
import { needCaptcha } from '../data/redis/captcha';
import cheapDetector from '../core/isProxy';
import isIPAllowed from '../core/isAllowed';
const ipCounter = new Counter();
@ -95,7 +95,7 @@ class SocketServer {
ws.name = user.getName();
const { ip } = user;
cheapDetector(ip);
isIPAllowed(ip);
ws.send(OnlineCounter.dehydrate(socketEvents.onlineCounter));
@ -493,8 +493,18 @@ class SocketServer {
failureRet = PixelReturn.dehydrate(10, 0, 0);
}
// (re)check for Proxy
if (await cheapDetector(ip)) {
failureRet = PixelReturn.dehydrate(11, 0, 0);
const allowed = await isIPAllowed(ip);
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) {
const now = Date.now();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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