add ui for iid banning
removed ip banning add eslint option for react-key
This commit is contained in:
parent
53b8168f24
commit
a41c286372
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>: {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>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{ fontSize: 16 }}
|
||||
onClick={close}
|
||||
>
|
||||
{t`OK`}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BanInfo);
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -240,7 +240,7 @@ function ModCanvastools() {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
<p className="modalcotext">Choose Canvas:
|
||||
<p className="modalcotext">{t`Choose Canvas`}:
|
||||
<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" />
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -186,6 +186,7 @@ const ChannelDropDown = ({
|
|||
const [cid, unreadCh, name] = ch;
|
||||
return (
|
||||
<div
|
||||
key={cid}
|
||||
onClick={() => setChatChannel(cid)}
|
||||
className={
|
||||
`chn${
|
||||
|
|
|
@ -64,6 +64,7 @@ const SettingsItemSelect = ({
|
|||
{
|
||||
values.map((value) => (
|
||||
<option
|
||||
key={value}
|
||||
value={value}
|
||||
>
|
||||
{value}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -35,6 +35,12 @@ const Message = sequelize.define('Message', {
|
|||
},
|
||||
}, {
|
||||
updatedAt: false,
|
||||
|
||||
setterMethods: {
|
||||
message(value) {
|
||||
this.setDataValue('message', value.slice(0, 200));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Message.belongsTo(Channel, {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -282,6 +282,12 @@ export function requestRankings() {
|
|||
);
|
||||
}
|
||||
|
||||
export function requestBanInfo() {
|
||||
return makeAPIGETRequest(
|
||||
'api/baninfo',
|
||||
);
|
||||
}
|
||||
|
||||
export function requestMe() {
|
||||
return makeAPIGETRequest(
|
||||
'api/me',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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`,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue