forked from ppfun/pixelplanet
Compare commits
1 Commits
master
...
sqlrefacto
Author | SHA1 | Date | |
---|---|---|---|
00cbddd6c9 |
|
@ -24,7 +24,7 @@ Click or tab: Place Pixel
|
|||
|
||||
## Build
|
||||
### Requirements
|
||||
- [nodejs environment](https://nodejs.org/en/) (>=16)
|
||||
- [nodejs environment](https://nodejs.org/en/) (>=18)
|
||||
- Linux or WSL if you want to be safe (we do not build on Windows and therefor can't guarantee that it will work there)
|
||||
|
||||
### Building
|
||||
|
@ -57,7 +57,7 @@ git config --global url.https://github.com/.insteadOf git://github.com/
|
|||
|
||||
## Run
|
||||
### Requirements
|
||||
- nodejs environment with [npm](https://www.npmjs.com/get-npm) (>=16)
|
||||
- nodejs environment with [npm](https://www.npmjs.com/get-npm) (>=18)
|
||||
- [pm2](https://github.com/Unitech/pm2) (`npm install -g pm2`) as process manager and for logging
|
||||
- [redis](https://redis.io/) as database for storìng the canvas
|
||||
- mysql or mariadb ([setup own user](https://www.digitalocean.com/community/tutorials/how-to-create-a-new-user-and-grant-permissions-in-mysql) and [create database](https://www.w3schools.com/SQl/sql_create_db.asp) for pixelplanet) for storing additional data like IP blacklist
|
||||
|
|
57
SQLMIGRATION-TODO.md
Normal file
57
SQLMIGRATION-TODO.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# SQL Migration
|
||||
|
||||
User Table:
|
||||
|
||||
verified -> if != 0 -> userlvl = 20
|
||||
roles -> if 1 -> userlvl = 100
|
||||
delete verified
|
||||
delete roles
|
||||
|
||||
check threepid associations and all logins
|
||||
discordid -> if != NULL -> ThreePIDs add uid, discordid, provider = 1
|
||||
redditid -> if != NULL -> ThreePIDs add uid, discordid, provider = 2
|
||||
delete discordid
|
||||
delete redditid
|
||||
|
||||
# rwhois
|
||||
|
||||
Error on WHOIS 69.178.112.123 rwhois.gci.net:4321: no rwhois support
|
||||
Error on WHOIS 50.7.93.84 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 2605:a601:a904:cc00:5918:f4cd:1dd1:a9c rwhois.googlefiber.net:8987: getaddrinfo ENOTFOUND rwhois.googlefiber.net:8987
|
||||
Error on WHOIS 198.16.70.52 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 198.16.78.45 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 149.34.217.96 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 38.9.254.98 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 38.54.79.203 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 38.91.101.217 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 38.10.69.98 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 50.7.93.28 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 38.128.66.211 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 198.16.66.125 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 198.16.74.44 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 198.16.70.28 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 198.16.66.197 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 38.54.57.78 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 198.16.66.195 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 198.16.66.101 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 2604:3d09:217e:96e0:e121:2b82:29b7:3fa9 rwhois.shawcable.net:4321: no rwhois support
|
||||
Error on WHOIS 149.57.29.157 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 38.91.100.42 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 149.71.172.36 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 149.36.50.199 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 50.5.243.186 rwhois.fuse.net:4321: no rwhois support
|
||||
Error on WHOIS 65.78.19.109 rwhois.rcn.net:4321: no rwhois support
|
||||
Error on WHOIS 160.2.105.243 rwhois.cableone.net:4321: no rwhois support
|
||||
Error on WHOIS 50.7.142.179 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 198.16.66.140 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 149.100.25.120 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 50.7.93.28 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 38.34.185.140 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 198.16.78.44 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 198.16.66.155 rwhois.fdcservers.net:4321: no rwhois support
|
||||
Error on WHOIS 38.107.255.226 rwhois.cogentco.com:4321: no rwhois support
|
||||
Error on WHOIS 184.155.140.22 rwhois.cableone.net:4321: no rwhois support
|
||||
|
||||
2600:1001:b00c:7158:0000:0000:0000:0000
|
||||
|
||||
181.165.235.69
|
|
@ -16,6 +16,9 @@ Pixelplanet has its own git repository for deployment on the live system, if an
|
|||
## rebuild.sh
|
||||
script to manually trigger rebuilding and restarting pixelplanet on the server
|
||||
|
||||
## banlist.csv
|
||||
CIDR banlist for firewall
|
||||
|
||||
## Some notes:
|
||||
Cloudflare Caching Setting `Broser Cache Expiration` should be set to `Respect Existing Headers` or it would default to 4h, which is unreasonable for chunks.
|
||||
Additinally make sure that cachebreakers get blocked by setting Cloudflare Firewall rules to block empty query strings at least for chunks
|
||||
|
|
103
deployment/banlist.csv
Normal file
103
deployment/banlist.csv
Normal file
|
@ -0,0 +1,103 @@
|
|||
102.129.143.86,list
|
||||
107.175.73.54,list
|
||||
107.179.20.204,list
|
||||
109.175.107.110,list
|
||||
109.95.217.242,list
|
||||
114.232.194.255,list
|
||||
115.226.153.176,list
|
||||
116.202.22.43,list
|
||||
119.74.167.169,list
|
||||
122.252.253.100,list
|
||||
131.153.26.188,list
|
||||
131.153.26.189,list
|
||||
138.3.244.208,list
|
||||
142.44.128.0/17,OVH
|
||||
146.70.52.46,list
|
||||
154.16.192.176,list
|
||||
172.107.240.26,list
|
||||
176.115.102.106,list
|
||||
176.215.80.25,list
|
||||
176.36.75.28,list
|
||||
178.163.117.211,list
|
||||
178.20.142.170,list
|
||||
178.72.81.231,list
|
||||
181.119.30.41,list
|
||||
185.177.124.224,list
|
||||
185.26.63.16,list
|
||||
188.132.139.129,list
|
||||
188.3.187.228,list
|
||||
190.11.138.163,list
|
||||
190.49.124.184,list
|
||||
191.101.31.95,list
|
||||
191.96.150.204,list
|
||||
192.142.16.11,list
|
||||
194.167.0.0/16,frenchy university
|
||||
194.182.67.126,list
|
||||
194.87.239.144,list
|
||||
212.237.228.220,list
|
||||
212.237.228.225,list
|
||||
213.227.154.74,list
|
||||
2400:8901::/64,captcha flood
|
||||
2600:3c03::/64,captcha flood
|
||||
2600:3c04::/64,captcha flood
|
||||
2607:5300:203:14ae::/64,list
|
||||
2a01:4ff:f0:3004::/64,websocket flood
|
||||
2a02:27b0:4d03:c9d0::/64,websocket flood
|
||||
31.210.107.199,list
|
||||
34.245.86.143,list
|
||||
34.66.88.30,list
|
||||
34.71.79.206,list
|
||||
34.82.153.53,list
|
||||
37.188.11.119,list
|
||||
37.212.64.134,list
|
||||
45.189.115.205,list
|
||||
46.246.41.153,list
|
||||
46.246.41.158,list
|
||||
51.161.0.0/17,ovh
|
||||
51.161.128.0/17,ovh
|
||||
51.178.0.0/16,ovh
|
||||
5.142.42.184,list
|
||||
5.173.137.17,list
|
||||
5.173.172.228,list
|
||||
52.143.155.67,list
|
||||
52.205.26.222,list
|
||||
5.227.29.33,list
|
||||
5.227.31.1,list
|
||||
62.244.51.28,list
|
||||
77.47.170.231,list
|
||||
78.132.55.22,list
|
||||
78.36.107.187,list
|
||||
78.86.5.152,list
|
||||
79.45.161.163,list
|
||||
80.169.156.52,list
|
||||
81.92.203.83,list
|
||||
82.193.110.12,list
|
||||
84.124.251.104,list
|
||||
84.17.43.24,list
|
||||
84.17.56.184,list
|
||||
84.239.40.225,list
|
||||
85.76.77.85,list
|
||||
86.138.152.227,list
|
||||
86.186.250.150,list
|
||||
87.7.200.139,list
|
||||
89.108.99.115,list
|
||||
89.40.143.192,list
|
||||
91.121.210.56,list
|
||||
91.228.236.175,list
|
||||
92.32.69.242,list
|
||||
93.115.28.181,list
|
||||
95.181.236.133,list
|
||||
45.9.88.0/22,ddos230612
|
||||
75.127.7.192/27,ddos230612
|
||||
85.98.55.199,sch bot
|
||||
2a01:cb04:60f:2900::/64,sch bot
|
||||
67.255.77.34,sch bot
|
||||
188.23.51.246,sch bot
|
||||
78.174.247.162,sch bot
|
||||
95.76.46.200,sch bot
|
||||
88.231.207.77,sch bot
|
||||
151.135.28.236,sch bot
|
||||
2a0d:6fc2:54c0:7500::/64,ws ddos
|
||||
176.231.133.107,ws ddos
|
||||
80.121.26.216,sch bot
|
||||
88.237.43.71,sch bot
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
# This hook builds pixelplanet after a push, and deploys it, it should be ron post-receive
|
||||
# If it is the master branch, it will deploy it on the life system, and other branch will get deployed to the dev-canvas (a second canvas that is running on the server)
|
||||
# This hook builds pixelplanet after a push, and deploys it, it should be run post-receive
|
||||
# If it is devel or test branch, it will deploy it on the test system.
|
||||
#
|
||||
# To set up a server to use this, you have to go through the building steps manually first.
|
||||
#
|
||||
|
@ -8,8 +8,6 @@
|
|||
BUILDDIR="/home/pixelpla/pixelplanet-build"
|
||||
#folder for dev canvas
|
||||
DEVFOLDER="/home/pixelpla/pixelplanet-dev"
|
||||
#folder for production canvas
|
||||
PFOLDER="/home/pixelpla/pixelplanet"
|
||||
|
||||
should_reinstall () {
|
||||
local TMPFILE="${BUILDDIR}/package.json.${1}.tmp"
|
||||
|
@ -53,28 +51,10 @@ do
|
|||
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git fetch --all
|
||||
cd "$BUILDDIR"
|
||||
branch=$(git rev-parse --symbolic --abbrev-ref $refname)
|
||||
if [ "master" == "$branch" ]; then
|
||||
echo "---UPDATING REPO ON PRODUCTION SERVER---"
|
||||
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git reset --hard "origin/$branch"
|
||||
COMMITS=`git log --pretty=format:'- %s%b' $newrev ^$oldrev`
|
||||
COMMITS=`echo "$COMMITS" | sed ':a;N;$!ba;s/\n/\\\n/g'`
|
||||
echo "---BUILDING pixelplanet---"
|
||||
should_reinstall master
|
||||
DO_REINSTALL=$?
|
||||
[ $DO_REINSTALL -eq 0 ] && npm_reinstall
|
||||
npm run build
|
||||
echo "---RESTARTING CANVAS---"
|
||||
pm2 stop ppfun-server
|
||||
pm2 stop ppfun-backups
|
||||
copy "${PFOLDER}" "${DO_REINSTALL}"
|
||||
cd "$PFOLDER"
|
||||
pm2 start ecosystem-backup.yml
|
||||
else
|
||||
if [ "test" == "$branch" ] || [ "devel" == "$branch" ]; then
|
||||
echo "---UPDATING REPO ON DEV SERVER---"
|
||||
pm2 stop ppfun-server-dev
|
||||
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git reset --hard "origin/$branch"
|
||||
COMMITS=`git log --pretty=format:'- %s%b' $newrev ^$oldrev`
|
||||
COMMITS=`echo "$COMMITS" | sed ':a;N;$!ba;s/\n/\\\n/g'`
|
||||
echo "---BUILDING pixelplanet---"
|
||||
should_reinstall dev
|
||||
DO_REINSTALL=$?
|
||||
|
|
|
@ -9,6 +9,9 @@ geo $allow_ws {
|
|||
195.209.151.0/24 1;
|
||||
213.59.160.0/20 1;
|
||||
62.76.12.0/24 1;
|
||||
195.16.78.61/32 1;
|
||||
# Luhansk
|
||||
176.109.176.0/21 1;
|
||||
}
|
||||
|
||||
geo $deny_ws {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#!/bin/bash
|
||||
# Rebuild from master branch and restart pixelplanet
|
||||
|
||||
#
|
||||
# To set up a server to use this, you have to go through the building steps manually first.
|
||||
#
|
||||
#folder for building the canvas (the git repository will get checkout there and the canvas will get buil thtere)
|
||||
BUILDDIR="/home/pixelpla/pixelplanet-build"
|
||||
#folder for dev canvas
|
||||
DEVFOLDER="/home/pixelpla/pixelplanet-dev"
|
||||
#folder for shards
|
||||
SCBFOLDER="/home/pixelpla/pixelplanet-scb"
|
||||
SCCFOLDER="/home/pixelpla/pixelplanet-scc"
|
||||
|
|
13
deployment/waf-rules.md
Normal file
13
deployment/waf-rules.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
## Cloudflare WAF rules against spam
|
||||
|
||||
95.217.118.106 is a bot pinging for void event
|
||||
|
||||
```
|
||||
((ip.geoip.asnum in {206766 196955 14618 210558 210107 29632 16276 24940 14061 213230}) or (http.user_agent contains "Google-Read-Aloud") or (ip.src in $idiots)) and not http.host in {"matrix.pixelplanet.fun:443" "matrix.pixelplanet.fun" "matrix.pixelplanet.fun:80" "git.pixelplanet.fun" "git.pixelplanet.fun:80" "git.pixelplanet.fun:443"} and not http.request.uri.path contains "/.well-known/" and ip.src ne 95.217.118.106
|
||||
```
|
||||
|
||||
## Google crap
|
||||
|
||||
```
|
||||
(ip.geoip.asnum eq 15169 and http.request.uri ne "/" and http.request.uri ne "" and not http.request.uri contains "/assets")
|
||||
```
|
|
@ -3,7 +3,7 @@
|
|||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=18"
|
||||
},
|
||||
"description": "Unlimited planet canvas for placing pixels",
|
||||
"main": "server.js",
|
||||
|
|
|
@ -9,6 +9,7 @@ import Canvastools from './ModCanvastools';
|
|||
import Admintools from './Admintools';
|
||||
import Watchtools from './ModWatchtools';
|
||||
import IIDTools from './ModIIDtools';
|
||||
import { USERLVL } from '../core/constants';
|
||||
|
||||
|
||||
const CONTENT = {
|
||||
|
@ -26,7 +27,7 @@ function Modtools() {
|
|||
const Content = CONTENT[selectedPart];
|
||||
|
||||
const parts = Object.keys(CONTENT)
|
||||
.filter((part) => part !== 'Admin' || userlvl === 1);
|
||||
.filter((part) => part !== 'Admin' || userlvl >= USERLVL.ADMIN);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
|||
|
||||
import { selectColor } from '../store/actions';
|
||||
import useWindowSize from './hooks/resize';
|
||||
import { USERLVL } from '../core/constants';
|
||||
|
||||
|
||||
/*
|
||||
|
@ -117,7 +118,7 @@ const Palette = () => {
|
|||
if (!paletteOpen) setRender(false);
|
||||
};
|
||||
|
||||
const clrHide = (userlvl === 0) ? clrIgnore : 0;
|
||||
const clrHide = (userlvl >= USERLVL.MOD) ? 0 : clrIgnore;
|
||||
|
||||
const [paletteStyle, spanStyle] = getStylesByWindowSize(
|
||||
(render && paletteOpen),
|
||||
|
|
|
@ -231,8 +231,8 @@ const Rankings = () => {
|
|||
</thead>
|
||||
<tbody>
|
||||
{{
|
||||
total: totalRanking.map((rank) => (
|
||||
<tr key={rank.name}>
|
||||
total: totalRanking.map((rank, ind) => (
|
||||
<tr key={rank.name || ind}>
|
||||
<td>{rank.r}</td>
|
||||
<td><span>{rank.name}</span></td>
|
||||
<td>{rank.t}</td>
|
||||
|
@ -240,8 +240,8 @@ const Rankings = () => {
|
|||
<td>{rank.dt}</td>
|
||||
</tr>
|
||||
)),
|
||||
today: totalDailyRanking.map((rank) => (
|
||||
<tr key={rank.name}>
|
||||
today: totalDailyRanking.map((rank, ind) => (
|
||||
<tr key={rank.name || ind}>
|
||||
<td>{rank.dr}</td>
|
||||
<td><span>{rank.name}</span></td>
|
||||
<td>{rank.dt}</td>
|
||||
|
@ -250,7 +250,7 @@ const Rankings = () => {
|
|||
</tr>
|
||||
)),
|
||||
yesterday: prevTop.map((rank, ind) => (
|
||||
<tr key={rank.name}>
|
||||
<tr key={rank.name || ind}>
|
||||
<td>{ind + 1}</td>
|
||||
<td><span>{rank.name}</span></td>
|
||||
<td>{rank.px}</td>
|
||||
|
|
|
@ -14,6 +14,7 @@ import useInterval from '../hooks/interval';
|
|||
import LogInArea from '../LogInArea';
|
||||
import Tabs from '../Tabs';
|
||||
import UserAreaContent from '../UserAreaContent';
|
||||
import { USERLVL } from '../../core/constants';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const Rankings = React.lazy(() => import(/* webpackChunkName: "stats" */ '../Rankings'));
|
||||
|
@ -66,8 +67,8 @@ const UserArea = () => {
|
|||
<Converter />
|
||||
</Suspense>
|
||||
</div>
|
||||
{userlvl && (
|
||||
<div label={(userlvl === 1) ? t`Modtools` : t`Modtools`}>
|
||||
{(userlvl >= USERLVL.MOD) && (
|
||||
<div label={(userlvl >= USERLVL.ADMIN) ? t`Admintools` : t`Modtools`}>
|
||||
<Suspense fallback={<div>{t`Loading...`}</div>}>
|
||||
<Modtools />
|
||||
</Suspense>
|
||||
|
|
|
@ -3,12 +3,9 @@
|
|||
* it just buffers the most recent 200 messages for each channel
|
||||
*
|
||||
*/
|
||||
import Sequelize from 'sequelize';
|
||||
import logger from './logger';
|
||||
import { storeMessage, getMessagesForChannel } from '../data/sql/Message';
|
||||
|
||||
import { Message, Channel } from '../data/sql';
|
||||
|
||||
const MAX_BUFFER_TIME = 120000;
|
||||
const MAX_BUFFER_TIME = 600000;
|
||||
|
||||
class ChatMessageBuffer {
|
||||
constructor(socketEvents) {
|
||||
|
@ -24,12 +21,12 @@ class ChatMessageBuffer {
|
|||
|
||||
async getMessages(cid, limit = 30) {
|
||||
if (limit > 200) {
|
||||
return ChatMessageBuffer.getMessagesFromDatabase(cid, limit);
|
||||
return getMessagesForChannel(cid, limit);
|
||||
}
|
||||
|
||||
let messages = this.buffer.get(cid);
|
||||
if (!messages) {
|
||||
messages = await ChatMessageBuffer.getMessagesFromDatabase(cid);
|
||||
messages = await getMessagesForChannel(cid, limit);
|
||||
this.buffer.set(cid, messages);
|
||||
}
|
||||
this.timestamps.set(cid, Date.now());
|
||||
|
@ -39,18 +36,15 @@ class ChatMessageBuffer {
|
|||
cleanBuffer() {
|
||||
const curTime = Date.now();
|
||||
const toDelete = [];
|
||||
this.timestamps.forEach((cid, timestamp) => {
|
||||
this.timestamps.forEach((timestamp, cid) => {
|
||||
if (curTime > timestamp + MAX_BUFFER_TIME) {
|
||||
toDelete.push(cid);
|
||||
}
|
||||
});
|
||||
toDelete.forEach((cid) => {
|
||||
this.buffer.delete(cid);
|
||||
this.timestamps.delete(cid);
|
||||
this.buffer.delete(cid);
|
||||
});
|
||||
logger.info(
|
||||
`Cleaned ${toDelete.length} channels from chat message buffer`,
|
||||
);
|
||||
}
|
||||
|
||||
async broadcastChatMessage(
|
||||
|
@ -64,20 +58,7 @@ class ChatMessageBuffer {
|
|||
if (message.length > 200) {
|
||||
return;
|
||||
}
|
||||
Message.create({
|
||||
name,
|
||||
flag,
|
||||
message,
|
||||
cid,
|
||||
uid,
|
||||
});
|
||||
Channel.update({
|
||||
lastMessage: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
}, {
|
||||
where: {
|
||||
id: cid,
|
||||
},
|
||||
});
|
||||
storeMessage(flag, message, cid, uid);
|
||||
/*
|
||||
* goes through socket events and then comes
|
||||
* back at addMessage
|
||||
|
@ -110,45 +91,6 @@ class ChatMessageBuffer {
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
static async getMessagesFromDatabase(cid, limit = 200) {
|
||||
const messagesModel = await Message.findAll({
|
||||
attributes: [
|
||||
'message',
|
||||
'uid',
|
||||
'name',
|
||||
'flag',
|
||||
[
|
||||
Sequelize.fn('UNIX_TIMESTAMP', Sequelize.col('createdAt')),
|
||||
'ts',
|
||||
],
|
||||
],
|
||||
where: { cid },
|
||||
limit,
|
||||
order: [['createdAt', 'DESC']],
|
||||
raw: true,
|
||||
});
|
||||
const messages = [];
|
||||
let i = messagesModel.length;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const {
|
||||
message,
|
||||
uid,
|
||||
name,
|
||||
flag,
|
||||
ts,
|
||||
} = messagesModel[i];
|
||||
messages.push([
|
||||
name,
|
||||
message,
|
||||
flag,
|
||||
uid,
|
||||
ts,
|
||||
]);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatMessageBuffer;
|
||||
|
|
|
@ -5,12 +5,13 @@ import { Op } from 'sequelize';
|
|||
import logger from './logger';
|
||||
import RateLimiter from '../utils/RateLimiter';
|
||||
import {
|
||||
Channel, RegUser, UserChannel, Message,
|
||||
Channel, RegUser, UserChannel, Message, USERLVL,
|
||||
} from '../data/sql';
|
||||
import { findIdByNameOrId } from '../data/sql/RegUser';
|
||||
import { banIP } from '../data/sql/Ban';
|
||||
import ChatMessageBuffer from './ChatMessageBuffer';
|
||||
import socketEvents from '../socket/socketEvents';
|
||||
import isIPAllowed from './isAllowed';
|
||||
import isIPAllowed from './ipUserIntel';
|
||||
import {
|
||||
mutec, unmutec,
|
||||
unmutecAll, listMutec,
|
||||
|
@ -18,7 +19,6 @@ import {
|
|||
unmute,
|
||||
allowedChat,
|
||||
} from '../data/redis/chat';
|
||||
import { banIP } from '../data/sql/Ban';
|
||||
import { DailyCron } from '../utils/cron';
|
||||
import { escapeMd } from './utils';
|
||||
import ttags from './ttag';
|
||||
|
@ -82,7 +82,7 @@ export class ChatProvider {
|
|||
}
|
||||
|
||||
async clearOldMessages() {
|
||||
if (!socketEvents.amIImportant()) {
|
||||
if (!socketEvents.important) {
|
||||
return;
|
||||
}
|
||||
const ids = Object.keys(this.defaultChannels);
|
||||
|
@ -152,7 +152,7 @@ export class ChatProvider {
|
|||
where: { name },
|
||||
defaults: {
|
||||
name,
|
||||
verified: 3,
|
||||
userlvl: USERLVL.VERIFIED,
|
||||
email: 'info@example.com',
|
||||
},
|
||||
raw: true,
|
||||
|
@ -166,7 +166,7 @@ export class ChatProvider {
|
|||
where: { name },
|
||||
defaults: {
|
||||
name,
|
||||
verified: 3,
|
||||
userlvl: USERLVL.VERIFIED,
|
||||
email: 'event@example.com',
|
||||
},
|
||||
raw: true,
|
||||
|
@ -180,7 +180,7 @@ export class ChatProvider {
|
|||
where: { name },
|
||||
defaults: {
|
||||
name,
|
||||
verified: 3,
|
||||
userlvl: USERLVL.VERIFIED,
|
||||
email: 'event@example.com',
|
||||
},
|
||||
raw: true,
|
||||
|
@ -419,7 +419,7 @@ export class ChatProvider {
|
|||
return 'nope';
|
||||
}
|
||||
|
||||
if (!user.userlvl) {
|
||||
if (user.userlvl < USERLVL.MOD) {
|
||||
const [allowed, needProxycheck] = await allowedChat(
|
||||
channelId,
|
||||
id,
|
||||
|
@ -432,7 +432,7 @@ export class ChatProvider {
|
|||
);
|
||||
if (allowed === 1) {
|
||||
return t`You can not send chat messages with proxy`;
|
||||
} if (allowed === 100 && user.userlvl === 0) {
|
||||
} if (allowed === 100) {
|
||||
return t`Your country is temporary muted from this chat channel`;
|
||||
} if (allowed === 101) {
|
||||
// eslint-disable-next-line max-len
|
||||
|
@ -472,7 +472,7 @@ export class ChatProvider {
|
|||
}
|
||||
|
||||
let displayCountry = country;
|
||||
if (user.userlvl !== 0) {
|
||||
if (user.userlvl >= USERLVL.MOD) {
|
||||
displayCountry = 'zz';
|
||||
/*
|
||||
* meme names disabled for now
|
||||
|
@ -490,7 +490,7 @@ export class ChatProvider {
|
|||
displayCountry = 'bt';
|
||||
}
|
||||
|
||||
if (USE_MAILER && !user.regUser.verified) {
|
||||
if (USE_MAILER && user.userlvl < USERLVL.VERIFIED) {
|
||||
return t`Your mail has to be verified in order to chat`;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,7 @@ import { getTTag } from './ttag';
|
|||
import { codeExists, checkCode, setCode } from '../data/redis/mailCodes';
|
||||
import socketEvents from '../socket/socketEvents';
|
||||
import { USE_MAILER, MAIL_ADDRESS } from './config';
|
||||
|
||||
import { RegUser } from '../data/sql';
|
||||
import { RegUser, USERLVL } from '../data/sql';
|
||||
|
||||
export class MailProvider {
|
||||
constructor() {
|
||||
|
@ -130,16 +129,6 @@ export class MailProvider {
|
|||
return t`Couldn't find this mail in our database`;
|
||||
}
|
||||
|
||||
/*
|
||||
* not sure if this is needed yet
|
||||
* does it matter if spamming password reset mails or verifications mails?
|
||||
*
|
||||
if(!reguser.verified) {
|
||||
logger.info(`Password reset mail for ${to} requested by ${ip} - mail not verified`);
|
||||
return "Can't reset password of unverified account.";
|
||||
}
|
||||
*/
|
||||
|
||||
const code = setCode(to);
|
||||
if (this.enabled) {
|
||||
this.postPasswdResetMail(to, ip, host, lang, code);
|
||||
|
@ -160,7 +149,7 @@ export class MailProvider {
|
|||
return false;
|
||||
}
|
||||
await reguser.update({
|
||||
mailVerified: true,
|
||||
userlvl: USERLVL.VERIFIED,
|
||||
verificationReqAt: null,
|
||||
});
|
||||
return reguser.name;
|
||||
|
@ -176,7 +165,7 @@ export class MailProvider {
|
|||
[Sequelize.Op.lt]:
|
||||
Sequelize.literal('CURRENT_TIMESTAMP - INTERVAL 4 DAY'),
|
||||
},
|
||||
verified: 0,
|
||||
userlvl: USERLVL.REGISTERED,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* timers and cron for account related actions
|
||||
*/
|
||||
|
||||
import { populateRanking } from '../data/sql/RegUser';
|
||||
import { populateIdObj } from '../data/sql/RegUser';
|
||||
import {
|
||||
getRanks,
|
||||
resetDailyRanks,
|
||||
|
@ -23,8 +23,10 @@ import { MINUTE } from './constants';
|
|||
import { DailyCron, HourlyCron } from '../utils/cron';
|
||||
|
||||
class Ranks {
|
||||
#ranks;
|
||||
|
||||
constructor() {
|
||||
this.ranks = {
|
||||
this.#ranks = {
|
||||
// ranking today of users by pixels
|
||||
dailyRanking: [],
|
||||
// ranking of users by pixels
|
||||
|
@ -48,14 +50,18 @@ class Ranks {
|
|||
* we go through socketEvents for sharding
|
||||
*/
|
||||
socketEvents.on('rankingListUpdate', (rankings) => {
|
||||
this.mergeIntoRanks(rankings);
|
||||
this.#mergeIntoRanks(rankings);
|
||||
});
|
||||
}
|
||||
|
||||
get ranks() {
|
||||
return this.#ranks;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
this.mergeIntoRanks(await Ranks.dailyUpdateRanking());
|
||||
this.mergeIntoRanks(await Ranks.hourlyUpdateRanking());
|
||||
this.#mergeIntoRanks(await Ranks.dailyUpdateRanking());
|
||||
this.#mergeIntoRanks(await Ranks.hourlyUpdateRanking());
|
||||
await Ranks.updateRanking();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -66,28 +72,39 @@ class Ranks {
|
|||
DailyCron.hook(Ranks.setDailyRanking);
|
||||
}
|
||||
|
||||
mergeIntoRanks(newRanks) {
|
||||
#mergeIntoRanks(newRanks) {
|
||||
if (!newRanks) {
|
||||
return;
|
||||
}
|
||||
this.ranks = {
|
||||
...this.ranks,
|
||||
this.#ranks = {
|
||||
...this.#ranks,
|
||||
...newRanks,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* populate ranking list with userdata inplace, censor private users
|
||||
* @param ranks Array of rank objects with userIds
|
||||
* @return ranks Array
|
||||
*/
|
||||
static async #populateRanking(ranks) {
|
||||
const popRanks = await populateIdObj(ranks);
|
||||
// remove data of private users
|
||||
return popRanks.map((rank) => (rank.name ? rank : {}));
|
||||
}
|
||||
|
||||
static async updateRanking() {
|
||||
// only main shard does it
|
||||
if (!socketEvents.amIImportant()) {
|
||||
if (!socketEvents.important) {
|
||||
return null;
|
||||
}
|
||||
const ranking = await populateRanking(
|
||||
const ranking = await Ranks.#populateRanking(
|
||||
await getRanks(
|
||||
false,
|
||||
1,
|
||||
100,
|
||||
));
|
||||
const dailyRanking = await populateRanking(
|
||||
const dailyRanking = await Ranks.#populateRanking(
|
||||
await getRanks(
|
||||
true,
|
||||
1,
|
||||
|
@ -112,7 +129,7 @@ class Ranks {
|
|||
cHistStats,
|
||||
pHourlyStats,
|
||||
};
|
||||
if (socketEvents.amIImportant()) {
|
||||
if (socketEvents.important) {
|
||||
// only main shard sends to others
|
||||
socketEvents.rankingListUpdate(ret);
|
||||
}
|
||||
|
@ -120,12 +137,13 @@ class Ranks {
|
|||
}
|
||||
|
||||
static async dailyUpdateRanking() {
|
||||
const prevTop = await populateRanking(
|
||||
const prevTop = await Ranks.#populateRanking(
|
||||
await getPrevTop(),
|
||||
);
|
||||
const pDailyStats = await getDailyPixelStats();
|
||||
const histStats = await getTopDailyHistory();
|
||||
histStats.users = await populateRanking(histStats.users);
|
||||
const hisUsers = await Ranks.#populateRanking(histStats.users);
|
||||
histStats.users = hisUsers.filter((r) => r.name);
|
||||
histStats.stats = histStats.stats.map((day) => day.filter(
|
||||
(r) => histStats.users.some((u) => u.id === r.id),
|
||||
));
|
||||
|
@ -134,7 +152,7 @@ class Ranks {
|
|||
pDailyStats,
|
||||
histStats,
|
||||
};
|
||||
if (socketEvents.amIImportant()) {
|
||||
if (socketEvents.important) {
|
||||
// only main shard sends to others
|
||||
socketEvents.rankingListUpdate(ret);
|
||||
}
|
||||
|
@ -142,7 +160,7 @@ class Ranks {
|
|||
}
|
||||
|
||||
static async setHourlyRanking() {
|
||||
if (!socketEvents.amIImportant()) {
|
||||
if (!socketEvents.important) {
|
||||
return;
|
||||
}
|
||||
const amount = socketEvents.onlineCounter.total;
|
||||
|
@ -155,7 +173,7 @@ class Ranks {
|
|||
* reset daily rankings, store previous rankings
|
||||
*/
|
||||
static async setDailyRanking() {
|
||||
if (!socketEvents.amIImportant()) {
|
||||
if (!socketEvents.important) {
|
||||
return;
|
||||
}
|
||||
logger.info('Resetting Daily Ranking');
|
||||
|
@ -165,5 +183,4 @@ class Ranks {
|
|||
}
|
||||
|
||||
|
||||
const rankings = new Ranks();
|
||||
export default rankings;
|
||||
export default new Ranks();
|
||||
|
|
|
@ -112,7 +112,7 @@ class RpgEvent {
|
|||
const success = await getSuccess();
|
||||
this.success = success;
|
||||
RpgEvent.setCoolDownFactorFromSuccess(success);
|
||||
if (socketEvents.amIImportant()) {
|
||||
if (socketEvents.important) {
|
||||
let eventTimestamp = await nextEvent();
|
||||
if (!eventTimestamp) {
|
||||
eventTimestamp = await RpgEvent.setNextEvent();
|
||||
|
@ -196,7 +196,7 @@ class RpgEvent {
|
|||
* if we aren't the main shard, we just wait and regularly check,
|
||||
* re-initializing if we become it
|
||||
*/
|
||||
if (!socketEvents.amIImportant()) {
|
||||
if (!socketEvents.important) {
|
||||
this.iAmNotImportant = true;
|
||||
if (this.void) {
|
||||
this.void.cancel();
|
||||
|
|
|
@ -507,7 +507,7 @@ export async function createTexture(
|
|||
// dont create textures larger than 4096
|
||||
const targetSize = Math.min(canvasSize, 4096);
|
||||
const amount = targetSize / TILE_SIZE;
|
||||
const zoom = Math.log2(amount) * 2 / TILE_ZOOM_LEVEL;
|
||||
const zoom = Math.log2(amount) * Math.log2(TILE_ZOOM_LEVEL);
|
||||
const textureBuffer = new Uint8Array(targetSize * targetSize * 3);
|
||||
const startTime = Date.now();
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
import sharp from 'sharp';
|
||||
import Sequelize from 'sequelize';
|
||||
|
||||
import isIPAllowed from './isAllowed';
|
||||
import isIPAllowed from './ipUserIntel';
|
||||
import { validateCoorRange } from '../utils/validation';
|
||||
import CanvasCleaner from './CanvasCleaner';
|
||||
import socketEvents from '../socket/socketEvents';
|
||||
import { RegUser } from '../data/sql';
|
||||
import { RegUser, USERLVL } from '../data/sql';
|
||||
import {
|
||||
cleanCacheForIP,
|
||||
} from '../data/redis/isAllowedCache';
|
||||
|
@ -100,7 +100,7 @@ export async function executeIIDAction(
|
|||
|
||||
switch (action) {
|
||||
case 'status': {
|
||||
const allowed = await isIPAllowed(ip, true);
|
||||
const allowed = await isIPAllowed(ip, { disableCache: true });
|
||||
let out = `Allowed to place: ${allowed.allowed}\n`;
|
||||
const info = await getInfoToIp(ip);
|
||||
out += `Country: ${info.country}\n`
|
||||
|
@ -590,7 +590,7 @@ export async function removeMod(userId) {
|
|||
}
|
||||
try {
|
||||
await user.update({
|
||||
isMod: false,
|
||||
userlvl: USERLVL.VERIFIED,
|
||||
});
|
||||
return `Moderation rights removed from user ${userId}`;
|
||||
} catch {
|
||||
|
@ -617,7 +617,7 @@ export async function makeMod(name) {
|
|||
}
|
||||
try {
|
||||
await user.update({
|
||||
isMod: true,
|
||||
userlvl: USERLVL.MOD,
|
||||
});
|
||||
return [user.id, user.name];
|
||||
} catch {
|
||||
|
|
64
src/core/cachedWhois.js
Normal file
64
src/core/cachedWhois.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* get whois cached in SQL
|
||||
*/
|
||||
|
||||
import logger from './logger';
|
||||
import whoisIp from '../utils/whois';
|
||||
import { rangeToString } from '../utils/ip';
|
||||
import { setWhoisIdToIp } from '../data/sql/IPInfo';
|
||||
import { getWhoisRangeOfIp, saveWhoisRange } from '../data/sql/WhoisRange';
|
||||
import { saveWhoisReferral } from '../data/sql/WhoisReferral';
|
||||
|
||||
/*
|
||||
* treat whois data always for whole ip ranges,
|
||||
* save them in the database and use them from there
|
||||
* if possible
|
||||
*/
|
||||
export default async function cachedWhoisIp(ip) {
|
||||
const rangeq = await getWhoisRangeOfIp(ip);
|
||||
console.log('WHOIS RANGEQ:', rangeq);
|
||||
// TODO: i don't think that it's returned like this
|
||||
// TODO: make sure data is purged after a month with IPInfo associations
|
||||
if (rangeq?.cidr) {
|
||||
return rangeq;
|
||||
}
|
||||
let whoisData;
|
||||
if (rangeq?.host) {
|
||||
const { host } = rangeq;
|
||||
logger.info(`WHOIS for ${ip} through ${host}`)
|
||||
whoisData = await whoisIp(ip, {host});
|
||||
} else {
|
||||
logger.info(`WHOIS for ${ip}`);
|
||||
whoisData = await whoisIp(ip)
|
||||
}
|
||||
const {
|
||||
referralHost,
|
||||
range,
|
||||
org,
|
||||
descr,
|
||||
asn,
|
||||
} = whoisData;
|
||||
const country = whoisData.country || 'xx';
|
||||
if (referralHost && rangeq?.host !== referralHost) {
|
||||
/*
|
||||
* the last used host for whois is cached
|
||||
* to improve time and to avoid getting rate limited
|
||||
*/
|
||||
saveWhoisReferral(whoisData.referralRange, referralHost)
|
||||
}
|
||||
if (!range) {
|
||||
return null;
|
||||
}
|
||||
const wid = await saveWhoisRange(whoisData);
|
||||
if (!wid) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
wid,
|
||||
cidr: rangeToString(range),
|
||||
country,
|
||||
org,
|
||||
descr,
|
||||
asn,
|
||||
};
|
||||
}
|
|
@ -11,7 +11,7 @@ if (process.env.BROWSER) {
|
|||
}
|
||||
|
||||
export const PORT = process.env.PORT || 8080;
|
||||
export const HOST = process.env.HOST || 'localhost';
|
||||
export const HOST = process.env.HOST || '127.0.0.1';
|
||||
|
||||
export const USE_MAILER = parseInt(process.env.USE_MAILER, 10) || false;
|
||||
export const MAIL_ADDRESS = process.env.MAIL_ADDRESS
|
||||
|
|
|
@ -9,6 +9,9 @@ export const MAX_SCALE = 40; // 52 in log2
|
|||
// export const DEFAULT_SCALE = 0.25; //-20 in log2
|
||||
export const DEFAULT_SCALE = 3;
|
||||
|
||||
// background color behind 2D canvses
|
||||
export const BACKGROUND_CLR_HEX = '#C4C4C4';
|
||||
|
||||
// default canvas that is first assumed, before real canvas data
|
||||
// gets fetched via api/me
|
||||
export const DEFAULT_CANVAS_ID = '0';
|
||||
|
@ -101,3 +104,19 @@ export const APISOCKET_USER_NAME = 'apisocket';
|
|||
export const MAX_LOADED_CHUNKS = 2000;
|
||||
export const MAX_CHUNK_AGE = 300000;
|
||||
export const GC_INTERVAL = 300000;
|
||||
|
||||
export const USERLVL = {
|
||||
ANONYM: 0,
|
||||
REGISTERED: 10,
|
||||
VERIFIED: 20,
|
||||
MOD: 100,
|
||||
ADMIN: 200,
|
||||
};
|
||||
|
||||
export const THREEP = {
|
||||
DISCORD: 1,
|
||||
REDDIT: 2,
|
||||
FACEBOOK: 3,
|
||||
GOOGLE: 4,
|
||||
VK: 5,
|
||||
};
|
||||
|
|
|
@ -7,9 +7,10 @@ import {
|
|||
} from './utils';
|
||||
import logger, { pixelLogger } from './logger';
|
||||
import allowPlace from '../data/redis/cooldown';
|
||||
import { USERLVL } from '../data/sql';
|
||||
import socketEvents from '../socket/socketEvents';
|
||||
import { setPixelByOffset } from './setPixel';
|
||||
import isIPAllowed from './isAllowed';
|
||||
import isIPAllowed from './ipUserIntel';
|
||||
import canvases from './canvases';
|
||||
|
||||
import { THREE_CANVAS_HEIGHT, THREE_TILE_SIZE, TILE_SIZE } from './constants';
|
||||
|
@ -109,16 +110,11 @@ export default async function drawByOffsets(
|
|||
throw new Error(3);
|
||||
}
|
||||
|
||||
/*
|
||||
* userlvl:
|
||||
* 0: nothing
|
||||
* 1: admin
|
||||
* 2: mod
|
||||
*/
|
||||
const isAdmin = (user.userlvl === 1);
|
||||
const isAdmin = (user.userlvl >= USERLVL.ADMIN);
|
||||
const req = (isAdmin) ? null : canvas.req;
|
||||
const clrIgnore = canvas.cli || 0;
|
||||
let factor = (isAdmin || (user.userlvl > 0 && pixels[0][1] < clrIgnore))
|
||||
const factor = (isAdmin
|
||||
|| (user.userlvl >= USERLVL.MOD && pixels[0][1] < clrIgnore))
|
||||
? 0.0 : coolDownFactor;
|
||||
|
||||
if (user.country === 'tr') {
|
||||
|
@ -154,7 +150,7 @@ export default async function drawByOffsets(
|
|||
// admins and mods can place unset pixels
|
||||
if (color >= canvas.colors.length
|
||||
|| (color < clrIgnore
|
||||
&& user.userlvl === 0
|
||||
&& user.userlvl < USERLVL.MOD
|
||||
&& !(canvas.v && color === 0))
|
||||
) {
|
||||
// color out of bounds
|
||||
|
@ -200,7 +196,7 @@ export default async function drawByOffsets(
|
|||
);
|
||||
|
||||
if (needProxycheck) {
|
||||
const pc = await isIPAllowed(ip, true);
|
||||
const pc = await isIPAllowed(ip, { disableCache: true });
|
||||
if (pc.status > 0) {
|
||||
pxlCnt = 0;
|
||||
switch (pc.status) {
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
/*
|
||||
* decide if IP is allowed
|
||||
* Get various data of IP and User and check if it is allowed to use site
|
||||
* or check if Email is dispoable
|
||||
* does proxycheck and check bans and whitelists
|
||||
* write IPInfo and UserIP to database
|
||||
*/
|
||||
import { getIPv6Subnet } from '../utils/ip';
|
||||
import whois from '../utils/whois';
|
||||
import ProxyCheck from '../utils/ProxyCheck';
|
||||
import { IPInfo } from '../data/sql';
|
||||
import { updateLastIp } from '../data/sql/UserIP';
|
||||
import { isIPBanned } from '../data/sql/Ban';
|
||||
import { isWhitelisted } from '../data/sql/Whitelist';
|
||||
import socketEvents from '../socket/socketEvents';
|
||||
import {
|
||||
cacheAllowed,
|
||||
getCacheAllowed,
|
||||
cacheMailProviderDisposable,
|
||||
getCacheMailProviderDisposable,
|
||||
} from '../data/redis/isAllowedCache';
|
||||
import { proxyLogger as logger } from './logger';
|
||||
|
||||
|
@ -27,10 +33,29 @@ if (USE_PROXYCHECK && PROXYCHECK_KEY) {
|
|||
mailChecker = pc.checkEmail;
|
||||
}
|
||||
|
||||
/*
|
||||
* notify user update
|
||||
*/
|
||||
async function userIpUpdate(userIpData) {
|
||||
const {
|
||||
userId,
|
||||
ip,
|
||||
userAgent,
|
||||
} = userIpData;
|
||||
try {
|
||||
await updateLastIp(userId, ip, userAgent);
|
||||
socketEvents.gotUserIpInfo(userIpData);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error on saving UserIP for ${ip} / ${userId}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* save information of ip into database
|
||||
*/
|
||||
async function saveIPInfo(ip, whoisRet, allowed, info) {
|
||||
async function saveIPInfo(ip, whoisRet, allowed, info, options) {
|
||||
try {
|
||||
await IPInfo.upsert({
|
||||
...whoisRet,
|
||||
|
@ -38,16 +63,27 @@ async function saveIPInfo(ip, whoisRet, allowed, info) {
|
|||
proxy: allowed,
|
||||
pcheck: info,
|
||||
});
|
||||
|
||||
const { userId } = options;
|
||||
if (userId) {
|
||||
const userIpData = {
|
||||
...whoisRet,
|
||||
ip,
|
||||
userId,
|
||||
userAgent: options.userAgent || null,
|
||||
};
|
||||
userIpUpdate(userIpData);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error whois for ${ip}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* execute proxycheck and blacklist whitelist check
|
||||
* @param f proxycheck function
|
||||
* @param ip full ip
|
||||
* @param ipKey
|
||||
* @param ipKey IP cleared of IPv6Subnets
|
||||
* @return [ allowed, status, pcheck capromise]
|
||||
*/
|
||||
async function checkPCAndLists(f, ip, ipKey) {
|
||||
|
@ -78,13 +114,15 @@ async function checkPCAndLists(f, ip, ipKey) {
|
|||
return [allowed, status, pcheck, caPromise];
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* execute proxycheck and whois and save result into cache
|
||||
* @param f function for checking if proxy
|
||||
* @param ip IP to check
|
||||
* @param ipKey IP cleared of IPv6Subnets
|
||||
* @param options see checkIfAllowed
|
||||
* @return checkifAllowed return
|
||||
*/
|
||||
async function withoutCache(f, ip, ipKey) {
|
||||
async function withoutCache(f, ip, ipKey, options) {
|
||||
const [
|
||||
[allowed, status, pcheck, caPromise],
|
||||
whoisRet,
|
||||
|
@ -95,7 +133,7 @@ async function withoutCache(f, ip, ipKey) {
|
|||
|
||||
await Promise.all([
|
||||
caPromise,
|
||||
saveIPInfo(ipKey, whoisRet, status, pcheck),
|
||||
saveIPInfo(ipKey, whoisRet, status, pcheck, options),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
@ -113,18 +151,20 @@ async function withoutCache(f, ip, ipKey) {
|
|||
* ]
|
||||
*/
|
||||
const checking = [];
|
||||
/*
|
||||
/**
|
||||
* Execute proxycheck and whois and save result into cache
|
||||
* If IP is already getting checked, reuse its request
|
||||
* @param ip ip to check
|
||||
* @param ipKey IP cleared of IPv6Subnets
|
||||
* @param options see checkIfAllowed
|
||||
* @return checkIfAllowed return
|
||||
*/
|
||||
async function withoutCacheButReUse(f, ip, ipKey) {
|
||||
async function withoutCacheButReUse(f, ip, ipKey, options) {
|
||||
const runReq = checking.find((q) => q[0] === ipKey);
|
||||
if (runReq) {
|
||||
return runReq[1];
|
||||
}
|
||||
const promise = withoutCache(f, ip, ipKey);
|
||||
const promise = withoutCache(f, ip, ipKey, options);
|
||||
checking.push([ipKey, promise]);
|
||||
|
||||
const result = await promise;
|
||||
|
@ -135,15 +175,17 @@ async function withoutCacheButReUse(f, ip, ipKey) {
|
|||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* execute proxycheck, don't wait, return cache if exists or
|
||||
* status -2 if currently checking
|
||||
* @param f function for checking if proxy
|
||||
* @param ip IP to check
|
||||
* @param ipKey IP cleared of IPv6Subnets
|
||||
* @param options see checkIfAllowed
|
||||
* @return Object as in checkIfAllowed
|
||||
* @return true if proxy or blacklisted, false if not or whitelisted
|
||||
*/
|
||||
async function withCache(f, ip, ipKey) {
|
||||
async function withCache(f, ip, ipKey, options) {
|
||||
const runReq = checking.find((q) => q[0] === ipKey);
|
||||
|
||||
if (!runReq) {
|
||||
|
@ -151,7 +193,7 @@ async function withCache(f, ip, ipKey) {
|
|||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
withoutCacheButReUse(f, ip, ipKey);
|
||||
withoutCacheButReUse(f, ip, ipKey, options);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -160,10 +202,14 @@ async function withCache(f, ip, ipKey) {
|
|||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* check if ip is allowed
|
||||
/**
|
||||
* check if ip is allowed, get IP informations and store them
|
||||
* @param ip IP
|
||||
* @param disableCache if we fetch result from cache
|
||||
* @param options {
|
||||
* userId: id of user (if given, connect IP to user via UserIP table),
|
||||
* userAgent: useragent string (for UserIP table ),
|
||||
* disableCache: if we fetch result from cache,
|
||||
* }
|
||||
* @return Promise {
|
||||
* allowed: boolean if allowed to use site
|
||||
* , status: -2: not yet checked
|
||||
|
@ -175,8 +221,8 @@ async function withCache(f, ip, ipKey) {
|
|||
* 4: invalid ip
|
||||
* }
|
||||
*/
|
||||
export default function checkIfAllowed(ip, disableCache = false) {
|
||||
if (!ip || ip === '0.0.0.1') {
|
||||
export default function getIpUserIntel(ip, options = {}) {
|
||||
if (!ip) {
|
||||
return {
|
||||
allowed: false,
|
||||
status: 4,
|
||||
|
@ -184,20 +230,32 @@ export default function checkIfAllowed(ip, disableCache = false) {
|
|||
}
|
||||
const ipKey = getIPv6Subnet(ip);
|
||||
|
||||
if (disableCache) {
|
||||
return withoutCacheButReUse(checker, ip, ipKey);
|
||||
if (options.disableCache) {
|
||||
return withoutCacheButReUse(checker, ip, ipKey, options);
|
||||
}
|
||||
return withCache(checker, ip, ipKey);
|
||||
return withCache(checker, ip, ipKey, options);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* check if email is disposable
|
||||
* @param email
|
||||
* @param disableCache if we fetch result from cache
|
||||
* @return Promise
|
||||
* null: some error occurred
|
||||
* false: legit provider
|
||||
* true: disposable
|
||||
*/
|
||||
export function checkIfMailDisposable(email) {
|
||||
return mailChecker(email);
|
||||
export async function checkIfMailDisposable(email, options = {}) {
|
||||
const mailProvider = email.slice(email.indexOf('@') + 1);
|
||||
if (!options.disableCache) {
|
||||
const cache = await getCacheMailProviderDisposable(mailProvider);
|
||||
if (cache !== null) {
|
||||
return cache;
|
||||
}
|
||||
}
|
||||
const isDisposable = await mailChecker(email);
|
||||
if (isDisposable !== null) {
|
||||
cacheMailProviderDisposable(mailProvider, isDisposable);
|
||||
}
|
||||
return isDisposable;
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
*
|
||||
*/
|
||||
import { getLocalizedCanvases } from '../canvasesDesc';
|
||||
import { USERLVL } from '../data/sql';
|
||||
import { USE_MAILER } from './config';
|
||||
import chatProvider from './ChatProvider';
|
||||
|
||||
|
@ -13,17 +14,18 @@ export default async function getMe(user, lang = 'default') {
|
|||
const userdata = await user.getUserData();
|
||||
// sanitize data
|
||||
const {
|
||||
name, mailVerified,
|
||||
name, userlvl,
|
||||
} = userdata;
|
||||
if (!name) userdata.name = null;
|
||||
const messages = [];
|
||||
if (USE_MAILER && name && !mailVerified) {
|
||||
if (USE_MAILER
|
||||
&& userlvl >= USERLVL.REGISTERED && userlvl < USERLVL.VERIFIED
|
||||
) {
|
||||
messages.push('not_verified');
|
||||
}
|
||||
if (messages.length > 0) {
|
||||
userdata.messages = messages;
|
||||
}
|
||||
delete userdata.mailVerified;
|
||||
|
||||
userdata.canvases = getLocalizedCanvases(lang);
|
||||
userdata.channels = {
|
||||
|
|
|
@ -13,20 +13,21 @@ import VkontakteStrategy from 'passport-vkontakte/lib/strategy';
|
|||
|
||||
import { sanitizeName } from '../utils/validation';
|
||||
import logger from './logger';
|
||||
import { RegUser } from '../data/sql';
|
||||
import User, { regUserQueryInclude as include } from '../data/User';
|
||||
import {
|
||||
RegUser, ThreePID, USERLVL, THREEP, regUserQueryInclude as include,
|
||||
} from '../data/sql';
|
||||
import User from '../data/User';
|
||||
import { auth } from './config';
|
||||
import { compareToHash } from '../utils/hash';
|
||||
import { getIPFromRequest } from '../utils/ip';
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser(async (req, id, done) => {
|
||||
const user = new User();
|
||||
const user = new User(req);
|
||||
try {
|
||||
await user.initialize(id, getIPFromRequest(req));
|
||||
await user.initialize({ id });
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
done(err, user);
|
||||
|
@ -61,28 +62,63 @@ passport.use(new JsonStrategy({
|
|||
return;
|
||||
}
|
||||
const user = new User();
|
||||
await user.initialize(reguser.id, null, reguser);
|
||||
user.updateLogInTimestamp();
|
||||
await user.initialize({ regUser: reguser });
|
||||
user.touch();
|
||||
done(null, user);
|
||||
}));
|
||||
|
||||
/*
|
||||
* OAuth SignIns, mail based
|
||||
/**
|
||||
* OAuth SignIns, either mail or tpid has to be given
|
||||
* @param provider one out of the possible THREEP enums
|
||||
* @param name name of thid party account
|
||||
* @param email email
|
||||
* @param tpid id of third party account
|
||||
*
|
||||
*/
|
||||
async function oauthLogin(provider, email, name, discordid = null) {
|
||||
if (!email) {
|
||||
throw new Error('You don\'t have a mail set in your account.');
|
||||
}
|
||||
async function oauthLogin(providerString, name, email = null, tpid = null) {
|
||||
name = sanitizeName(name);
|
||||
let reguser = await RegUser.findOne({
|
||||
include,
|
||||
where: { email },
|
||||
});
|
||||
if (!reguser) {
|
||||
const provider = THREEP[providerString];
|
||||
if (!provider) {
|
||||
throw new Error(`Can not login with ${providerString}`);
|
||||
}
|
||||
if (!email && !tpid) {
|
||||
throw new Error(
|
||||
// eslint-disable-next-line max-len
|
||||
`${provider} didn't give us enoguh information to log you in, maybe you don't have an email set in their account?`,
|
||||
);
|
||||
}
|
||||
let reguser;
|
||||
if (tpid) {
|
||||
await RegUser.findOne({
|
||||
include: [
|
||||
...include,
|
||||
{
|
||||
association: 'tp',
|
||||
where: { provider, tpid },
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (!reguser && email) {
|
||||
reguser = await RegUser.findOne({
|
||||
include,
|
||||
where: { email },
|
||||
});
|
||||
if (reguser && tpid) {
|
||||
await ThreePID.create({
|
||||
uid: reguser.id,
|
||||
provider,
|
||||
tpid,
|
||||
}, {
|
||||
raw: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!reguser) {
|
||||
reguser = await RegUser.findOne({
|
||||
where: { name },
|
||||
raw: true,
|
||||
});
|
||||
while (reguser) {
|
||||
// name is taken by someone else
|
||||
|
@ -90,24 +126,35 @@ async function oauthLogin(provider, email, name, discordid = null) {
|
|||
name = `${name.substring(0, 15)}-${Math.random().toString(36).substring(2, 10)}`;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
reguser = await RegUser.findOne({
|
||||
include,
|
||||
where: { name },
|
||||
raw: true,
|
||||
});
|
||||
}
|
||||
logger.info(
|
||||
// eslint-disable-next-line max-len
|
||||
`Create new user from ${providerString} oauth login ${email} / ${name} / ${tpid}`,
|
||||
);
|
||||
if (tpid) {
|
||||
reguser = await RegUser.create({
|
||||
email,
|
||||
name,
|
||||
userlvl: USERLVL.VERIFIED,
|
||||
tp: [{ provider, tpid }],
|
||||
}, {
|
||||
include: [{
|
||||
association: 'tp',
|
||||
}],
|
||||
});
|
||||
} else {
|
||||
reguser = await RegUser.create({
|
||||
email,
|
||||
name,
|
||||
userlvl: USERLVL.VERIFIED,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line max-len
|
||||
logger.info(`Create new user from ${provider} oauth login ${email} / ${name}`);
|
||||
reguser = await RegUser.create({
|
||||
email,
|
||||
name,
|
||||
verified: 1,
|
||||
discordid,
|
||||
});
|
||||
}
|
||||
if (!reguser.discordid && discordid) {
|
||||
reguser.update({ discordid });
|
||||
}
|
||||
const user = new User();
|
||||
await user.initialize(reguser.id, null, reguser);
|
||||
await user.initialize({ regUser: reguser });
|
||||
return user;
|
||||
}
|
||||
|
||||
|
@ -123,7 +170,7 @@ passport.use(new FacebookStrategy({
|
|||
try {
|
||||
const { displayName: name, emails } = profile;
|
||||
const email = emails[0].value;
|
||||
const user = await oauthLogin('facebook', email, name);
|
||||
const user = await oauthLogin('FACEBOOK', name, email);
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
|
@ -140,13 +187,7 @@ passport.use(new DiscordStrategy({
|
|||
}, async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
const { id, email, username: name } = profile;
|
||||
if (!email) {
|
||||
throw new Error(
|
||||
// eslint-disable-next-line max-len
|
||||
'Sorry, you can not use discord login with an discord account that does not have email set.',
|
||||
);
|
||||
}
|
||||
const user = await oauthLogin('discord', email, name, id);
|
||||
const user = await oauthLogin('DISCORD', name, email, id);
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
|
@ -164,7 +205,7 @@ passport.use(new GoogleStrategy({
|
|||
try {
|
||||
const { displayName: name, emails } = profile;
|
||||
const email = emails[0].value;
|
||||
const user = await oauthLogin('google', email, name);
|
||||
const user = await oauthLogin('GOOGLE', name, email);
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
|
@ -180,39 +221,10 @@ passport.use(new RedditStrategy({
|
|||
proxy: true,
|
||||
}, async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
const redditid = profile.id;
|
||||
let name = sanitizeName(profile.name);
|
||||
// reddit needs an own login strategy based on its id,
|
||||
// because we can not access it's mail
|
||||
let reguser = await RegUser.findOne({
|
||||
include,
|
||||
where: { redditid },
|
||||
});
|
||||
if (!reguser) {
|
||||
reguser = await RegUser.findOne({
|
||||
include,
|
||||
where: { name },
|
||||
});
|
||||
while (reguser) {
|
||||
// name is taken by someone else
|
||||
// eslint-disable-next-line max-len
|
||||
name = `${name.substring(0, 15)}-${Math.random().toString(36).substring(2, 10)}`;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
reguser = await RegUser.findOne({
|
||||
include,
|
||||
where: { name },
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line max-len
|
||||
logger.info(`Create new user from reddit oauth login ${name} / ${redditid}`);
|
||||
reguser = await RegUser.create({
|
||||
name,
|
||||
verified: 1,
|
||||
redditid,
|
||||
});
|
||||
}
|
||||
const user = new User();
|
||||
await user.initialize(reguser.id, null, reguser);
|
||||
const { id } = profile;
|
||||
const name = sanitizeName(profile.name);
|
||||
// reddit does not give us access to email
|
||||
const user = await oauthLogin('REDDIT', name, null, id);
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
|
@ -238,7 +250,7 @@ passport.use(new VkontakteStrategy({
|
|||
'Sorry, you can not use vk login with an account that does not have a verified email set.',
|
||||
);
|
||||
}
|
||||
const user = await oauthLogin('vkontakte', email, name);
|
||||
const user = await oauthLogin('VK', name, email);
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
|
|
|
@ -102,7 +102,7 @@ export function getTileOfPixel(
|
|||
|
||||
export function getMaxTiledZoom(canvasSize) {
|
||||
if (!canvasSize) return 0;
|
||||
return Math.log2(canvasSize / TILE_SIZE) / TILE_ZOOM_LEVEL * 2;
|
||||
return Math.log2(canvasSize / TILE_SIZE) / Math.log2(TILE_ZOOM_LEVEL);
|
||||
}
|
||||
|
||||
export function getHistoricalCanvasSize(
|
||||
|
|
280
src/data/User.js
280
src/data/User.js
|
@ -6,117 +6,89 @@
|
|||
*
|
||||
* */
|
||||
|
||||
import { QueryTypes, Utils } from 'sequelize';
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import sequelize from './sql/sequelize';
|
||||
import { RegUser, Channel, UserBlock } from './sql';
|
||||
import {
|
||||
RegUser, IPInfo, USERLVL, regUserQueryInclude,
|
||||
} from './sql';
|
||||
import { touch as touchUserIP } from './sql/UserIP';
|
||||
import { setCoolDown, getCoolDown } from './redis/cooldown';
|
||||
import { getUserRanks } from './redis/ranks';
|
||||
import { getIPv6Subnet } from '../utils/ip';
|
||||
import {
|
||||
getIPFromRequest,
|
||||
getIPv6Subnet,
|
||||
} from '../utils/ip';
|
||||
import isIPUserAllowed from '../core/ipUserIntel';
|
||||
import { ADMIN_IDS } from '../core/config';
|
||||
|
||||
|
||||
export const regUserQueryInclude = [{
|
||||
model: Channel,
|
||||
as: 'channel',
|
||||
include: [{
|
||||
model: RegUser,
|
||||
as: 'dmu1',
|
||||
foreignKey: 'dmu1id',
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
],
|
||||
}, {
|
||||
model: RegUser,
|
||||
as: 'dmu2',
|
||||
foreignKey: 'dmu2id',
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
],
|
||||
}],
|
||||
}, {
|
||||
model: RegUser,
|
||||
through: UserBlock,
|
||||
as: 'blocked',
|
||||
foreignKey: 'uid',
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
],
|
||||
}];
|
||||
|
||||
class User {
|
||||
id; // string
|
||||
ip; // string
|
||||
regUser; // Object
|
||||
channels; // Object
|
||||
blocked; // Array
|
||||
/*
|
||||
* 0: nothing
|
||||
* 1: Admin
|
||||
* 2: Mod
|
||||
#regu;
|
||||
#ipin;
|
||||
|
||||
/**
|
||||
* @param req Optional request object, or object containing ip
|
||||
*/
|
||||
userlvl; // number
|
||||
|
||||
constructor() {
|
||||
this.resetRegUser();
|
||||
this.ip = '127.0.0.1';
|
||||
this.ipSub = this.ip;
|
||||
}
|
||||
|
||||
async initialize(id, ip = null, regUser = null) {
|
||||
if (ip) {
|
||||
this.ip = ip;
|
||||
this.ipSub = getIPv6Subnet(ip);
|
||||
}
|
||||
if (regUser) {
|
||||
this.id = regUser.id;
|
||||
this.setRegUser(regUser);
|
||||
}
|
||||
if (id && !regUser) {
|
||||
const reguser = await RegUser.findByPk(id, {
|
||||
include: regUserQueryInclude,
|
||||
});
|
||||
if (reguser) {
|
||||
this.setRegUser(reguser);
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetRegUser() {
|
||||
constructor(req) {
|
||||
// if id = 0 -> unregistered
|
||||
this.id = 0;
|
||||
this.regUser = null;
|
||||
this.channels = {};
|
||||
this.blocked = [];
|
||||
this.userlvl = 0;
|
||||
this.#ipin = null;
|
||||
|
||||
if (req) {
|
||||
this.ip = getIPFromRequest(req);
|
||||
this.ua = (req.headers) ? req.headers['user-agent'] : null;
|
||||
this.ipSub = getIPv6Subnet(this.ip);
|
||||
this.country = req.cc;
|
||||
} else {
|
||||
this.ip = '127.0.0.1';
|
||||
this.ipSub = this.ip;
|
||||
this.ua = null;
|
||||
this.country = 'xx';
|
||||
}
|
||||
}
|
||||
|
||||
setRegUser(reguser) {
|
||||
this.regUser = reguser;
|
||||
this.id = reguser.id;
|
||||
get name() {
|
||||
return (this.regUser) ? this.regUser.name : null;
|
||||
}
|
||||
|
||||
get isRegistered() {
|
||||
return !!this.id;
|
||||
}
|
||||
|
||||
get regUser() {
|
||||
return this.#regu;
|
||||
}
|
||||
|
||||
set regUser(regu) {
|
||||
if (!regu) {
|
||||
this.id = 0;
|
||||
this.#regu = null;
|
||||
this.channels = {};
|
||||
this.blocked = [];
|
||||
this.userlvl = USERLVL.ANONYM;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#regu = regu;
|
||||
this.id = regu.id;
|
||||
this.channels = {};
|
||||
this.blocked = [];
|
||||
this.blocked.length = 0;
|
||||
|
||||
if (this.regUser.isMod) {
|
||||
this.userlvl = 2;
|
||||
}
|
||||
if (ADMIN_IDS.includes(this.id)) {
|
||||
this.userlvl = 1;
|
||||
if (ADMIN_IDS.includes(regu.id)) {
|
||||
this.userlvl = 200;
|
||||
} else {
|
||||
this.userlvl = regu.userlvl;
|
||||
}
|
||||
|
||||
if (reguser.channel) {
|
||||
for (let i = 0; i < reguser.channel.length; i += 1) {
|
||||
if (regu.channel) {
|
||||
for (let i = 0; i < regu.channel.length; i += 1) {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
lastTs,
|
||||
dmu1,
|
||||
dmu2,
|
||||
} = reguser.channel[i];
|
||||
} = regu.channel[i];
|
||||
if (type === 1) {
|
||||
/* in DMs:
|
||||
* the name is the name of the other user
|
||||
|
@ -136,7 +108,7 @@ class User {
|
|||
dmu,
|
||||
]);
|
||||
} else {
|
||||
const { name } = reguser.channel[i];
|
||||
const { name } = regu.channel[i];
|
||||
this.addChannel(id, [
|
||||
name,
|
||||
type,
|
||||
|
@ -145,35 +117,72 @@ class User {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (reguser.blocked) {
|
||||
for (let i = 0; i < reguser.blocked.length; i += 1) {
|
||||
if (regu.blocked) {
|
||||
for (let i = 0; i < regu.blocked.length; i += 1) {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
} = reguser.blocked[i];
|
||||
} = regu.blocked[i];
|
||||
this.blocked.push([id, name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get ipInfo() {
|
||||
return this.#ipin;
|
||||
}
|
||||
|
||||
set ipInfo(ipin) {
|
||||
this.country = ipin.country;
|
||||
this.#ipin = ipin;
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize registered user
|
||||
* @param values object with one or more:
|
||||
* id userId as number
|
||||
* regUser as sequelize instance
|
||||
* ipSub as string to initialize IPInfoj
|
||||
* ipInfo as sequelize instance
|
||||
* @return promise
|
||||
*/
|
||||
initialize(values) {
|
||||
const promises = [];
|
||||
if (values.regUser) {
|
||||
this.regUser = values.regUser;
|
||||
} else if (values.id) {
|
||||
promises.push(RegUser.findByPk(values.id, {
|
||||
include: regUserQueryInclude,
|
||||
}).then((regUser) => {
|
||||
if (regUser) {
|
||||
this.regUser = regUser;
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (values.ipInfo) {
|
||||
this.ipInfo = values.ipInfo;
|
||||
} else if (values.ipSub) {
|
||||
promises.push(IPInfo.findByPk(values.ipSub, {
|
||||
raw: true,
|
||||
}).then((ipInfo) => {
|
||||
if (ipInfo) {
|
||||
this.ipInfo = ipInfo;
|
||||
}
|
||||
}).catch(() => {}));
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
async reload() {
|
||||
if (!this.regUser) return;
|
||||
if (!this.#regu) return;
|
||||
try {
|
||||
await this.regUser.reload();
|
||||
await this.#regu.reload();
|
||||
} catch (e) {
|
||||
// user got deleted
|
||||
this.resetRegUser();
|
||||
this.regUser = null;
|
||||
return;
|
||||
}
|
||||
this.setRegUser(this.regUser);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return (this.regUser) ? this.regUser.name : null;
|
||||
}
|
||||
|
||||
get isRegistered() {
|
||||
return !!this.id;
|
||||
this.regUser = this.#regu;
|
||||
}
|
||||
|
||||
addChannel(cid, channelArray) {
|
||||
|
@ -192,49 +201,22 @@ class User {
|
|||
return getCoolDown(this.ipSub, this.id, canvasId);
|
||||
}
|
||||
|
||||
async getTotalPixels() {
|
||||
const { id } = this;
|
||||
if (!id) return 0;
|
||||
if (this.userlvl === 1) return 100000;
|
||||
if (this.regUser) {
|
||||
return this.regUser.totalPixels;
|
||||
}
|
||||
try {
|
||||
// TODO does not work anymore
|
||||
const userq = await sequelize.query(
|
||||
'SELECT totalPixels FROM Users WHERE id = $1',
|
||||
{
|
||||
bind: [id],
|
||||
type: QueryTypes.SELECT,
|
||||
raw: true,
|
||||
plain: true,
|
||||
},
|
||||
);
|
||||
return userq.totalPixels;
|
||||
} catch (err) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async setCountry(country) {
|
||||
this.country = country;
|
||||
if (this.regUser && this.regUser.flag !== country) {
|
||||
this.regUser.update({
|
||||
flag: country,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async updateLogInTimestamp() {
|
||||
if (!this.regUser) return false;
|
||||
try {
|
||||
await this.regUser.update({
|
||||
lastLogIn: new Utils.Literal('CURRENT_TIMESTAMP'),
|
||||
});
|
||||
} catch (err) {
|
||||
/*
|
||||
* update lastSeen timestamp and userAgent
|
||||
*/
|
||||
async touch() {
|
||||
if (!this.id || this.ip === '127.0.0.1') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return touchUserIP(this.id, this.ip, this.ua);
|
||||
}
|
||||
|
||||
isAllowed(disableCache = false) {
|
||||
return isIPUserAllowed(this.ip, {
|
||||
disableCache,
|
||||
userId: this.id,
|
||||
userAgent: this.ua,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserData() {
|
||||
|
@ -250,27 +232,25 @@ class User {
|
|||
channels,
|
||||
blocked,
|
||||
};
|
||||
if (this.regUser == null) {
|
||||
if (!this.#regu) {
|
||||
return {
|
||||
...data,
|
||||
name: null,
|
||||
mailVerified: false,
|
||||
blockDm: false,
|
||||
priv: false,
|
||||
mailreg: false,
|
||||
};
|
||||
}
|
||||
const { regUser } = this;
|
||||
const [
|
||||
totalPixels,
|
||||
dailyTotalPixels,
|
||||
ranking,
|
||||
dailyRanking,
|
||||
] = await getUserRanks(id);
|
||||
const regUser = this.#regu;
|
||||
return {
|
||||
...data,
|
||||
name: regUser.name,
|
||||
mailVerified: regUser.mailVerified,
|
||||
blockDm: regUser.blockDm,
|
||||
priv: regUser.priv,
|
||||
totalPixels,
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
import client from './client';
|
||||
|
||||
export const PREFIX = 'isal';
|
||||
export const MAIL_PREFIX = 'ised';
|
||||
const CACHE_DURATION = 14 * 24 * 3600;
|
||||
const MAIL_CACHE_DURATION = 7 * 24 * 3600;
|
||||
|
||||
export function cacheAllowed(ip, status) {
|
||||
const key = `${PREFIX}:${ip}`;
|
||||
|
@ -33,3 +35,20 @@ export function cleanCacheForIP(ip) {
|
|||
const key = `${PREFIX}:${ip}`;
|
||||
return client.del(key);
|
||||
}
|
||||
|
||||
export function cacheMailProviderDisposable(mailProvider, isDisposable) {
|
||||
const key = `${MAIL_PREFIX}:${mailProvider}`;
|
||||
const value = (isDisposable) ? '1' : '';
|
||||
return client.set(key, value, {
|
||||
EX: MAIL_CACHE_DURATION,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCacheMailProviderDisposable(mailProvider) {
|
||||
const key = `${MAIL_PREFIX}:${mailProvider}`;
|
||||
const cache = await client.get(key);
|
||||
if (!cache) {
|
||||
return null;
|
||||
}
|
||||
return cache === '1';
|
||||
}
|
||||
|
|
|
@ -60,6 +60,9 @@ async function cleanBans() {
|
|||
},
|
||||
raw: true,
|
||||
});
|
||||
if (!expiredIPs.length) {
|
||||
return;
|
||||
}
|
||||
const ips = [];
|
||||
for (let i = 0; i < expiredIPs.length; i += 1) {
|
||||
ips.push(expiredIPs[i].ip);
|
||||
|
|
|
@ -1,49 +1,32 @@
|
|||
import { DataTypes } from 'sequelize';
|
||||
import Sequelize, { DataTypes } from 'sequelize';
|
||||
import sequelize from './sequelize';
|
||||
|
||||
|
||||
const IPInfo = sequelize.define('IPInfo', {
|
||||
/*
|
||||
* Store both 32bit IPv4 and first half of 128bit IPv6
|
||||
* (only the first 64bit of a v6 is usually assigned
|
||||
* to customers by ISPs, the second half is assigned by devices)
|
||||
* NOTE:
|
||||
* IPv6 addresses in the ::/32 subnet would map to IPv4, which
|
||||
* should be no issues, because ::/8 is reserved by IETF
|
||||
*/
|
||||
ip: {
|
||||
type: DataTypes.CHAR(39),
|
||||
allowNull: false,
|
||||
type: 'VARBINARY(8)',
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
uuid: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
country: {
|
||||
type: DataTypes.CHAR(2),
|
||||
defaultValue: 'xx',
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
cidr: {
|
||||
type: DataTypes.CHAR(43),
|
||||
defaultValue: 'N/A',
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
org: {
|
||||
type: `${DataTypes.CHAR(60)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
},
|
||||
|
||||
descr: {
|
||||
type: `${DataTypes.CHAR(60)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
},
|
||||
|
||||
asn: {
|
||||
type: DataTypes.CHAR(12),
|
||||
defaultValue: 'N/A',
|
||||
type: 'BINARY(16)',
|
||||
defaultValue: Sequelize.literal('UUID_TO_BIN(UUID())'),
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
/*
|
||||
* 0: no proxy
|
||||
* 1: proxy
|
||||
*/
|
||||
proxy: {
|
||||
type: DataTypes.TINYINT,
|
||||
defaultValue: 0,
|
||||
},
|
||||
|
||||
/*
|
||||
|
@ -51,77 +34,78 @@ const IPInfo = sequelize.define('IPInfo', {
|
|||
* proxycheck
|
||||
*/
|
||||
pcheck: {
|
||||
type: `${DataTypes.CHAR(60)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
},
|
||||
}, {
|
||||
getterMethods: {
|
||||
isProxy() {
|
||||
return (this.proxy === 1);
|
||||
},
|
||||
},
|
||||
|
||||
setterMethods: {
|
||||
isProxy(proxy) {
|
||||
const num = (proxy) ? 1 : 0;
|
||||
this.setDataValue('proxy', num);
|
||||
},
|
||||
|
||||
asn(value) {
|
||||
const asn = value.split(',')[0];
|
||||
this.setDataValue('asn', asn);
|
||||
},
|
||||
|
||||
org(value) {
|
||||
this.setDataValue('org', value.slice(0, 60));
|
||||
},
|
||||
|
||||
descr(value) {
|
||||
this.setDataValue('descr', value.slice(0, 60));
|
||||
},
|
||||
|
||||
pcheck(value) {
|
||||
type: `${DataTypes.STRING(60)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
set(value) {
|
||||
if (value) {
|
||||
this.setDataValue('pcheck', value.slice(0, 60));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
* time of last proxycheck
|
||||
*/
|
||||
checkedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
|
||||
country(value) {
|
||||
this.setDataValue('country', value.slice(0, 2).toLowerCase());
|
||||
/*
|
||||
* virtual field to get a boolean for proxy,
|
||||
* is not set in database
|
||||
*/
|
||||
isProxy: {
|
||||
type: DataTypes.VIRTUAL,
|
||||
get() {
|
||||
return (this.proxy === 1);
|
||||
},
|
||||
set() {
|
||||
throw new Error(
|
||||
'Do not try to set the `isProxy` value! Set proxy instead',
|
||||
);
|
||||
},
|
||||
},
|
||||
}, {
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
export async function getIPofIID(uuid) {
|
||||
if (!uuid) {
|
||||
return null;
|
||||
}
|
||||
let result = null;
|
||||
try {
|
||||
result = await IPInfo.findOne({
|
||||
attributes: ['ip'],
|
||||
where: { uuid },
|
||||
const result = await IPInfo.findOne({
|
||||
attributes: [
|
||||
[Sequelize.fn('BIN_TO_IP', Sequelize.col('ip')), 'ip'],
|
||||
],
|
||||
where: {
|
||||
uuid: Sequelize.fn('UUID_TO_BIN', uuid),
|
||||
},
|
||||
raw: true,
|
||||
});
|
||||
} catch {
|
||||
// nothing
|
||||
return result.ip;
|
||||
} catch (err) {
|
||||
console.error(`SQL Error on getIPofIID: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
return result && result.ip;
|
||||
}
|
||||
|
||||
export async function getIIDofIP(ip) {
|
||||
if (!ip) {
|
||||
return null;
|
||||
}
|
||||
let result = null;
|
||||
try {
|
||||
result = await IPInfo.findByPk(ip, {
|
||||
attributes: ['uuid'],
|
||||
const result = await IPInfo.findOne({
|
||||
attributes: [
|
||||
[Sequelize.fn('BIN_TO_UUID', Sequelize.col('uuid')), 'uuid'],
|
||||
],
|
||||
where: {
|
||||
ip: Sequelize.fn('IP_TO_BIN', ip),
|
||||
},
|
||||
raw: true,
|
||||
});
|
||||
} catch {
|
||||
// nothing
|
||||
return result?.uuid;
|
||||
} catch (err) {
|
||||
console.error(`SQL Error on getIIDofIP: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
return result && result.uuid;
|
||||
}
|
||||
|
||||
export async function getIdsToIps(ips) {
|
||||
|
@ -131,21 +115,24 @@ export async function getIdsToIps(ips) {
|
|||
}
|
||||
try {
|
||||
const result = await IPInfo.findAll({
|
||||
attributes: ['ip', 'uuid'],
|
||||
where: { ip: ips },
|
||||
attributes: [
|
||||
[Sequelize.fn('BIN_TO_IP', Sequelize.col('ip')), 'ip'],
|
||||
[Sequelize.fn('BIN_TO_UUID', Sequelize.col('uuid')), 'uuid'],
|
||||
],
|
||||
where: { ip: ips.map((ip) => Sequelize.fn('IP_TO_BIN', ip)) },
|
||||
raw: true,
|
||||
});
|
||||
result.forEach((obj) => {
|
||||
ipToIdMap.set(obj.ip, obj.uuid);
|
||||
});
|
||||
} catch {
|
||||
// nothing
|
||||
} catch (err) {
|
||||
console.error(`SQL Error on getIdsToIps: ${err.message}`);
|
||||
}
|
||||
return ipToIdMap;
|
||||
}
|
||||
|
||||
export async function getInfoToIp(ip) {
|
||||
return IPInfo.findByPk(ip);
|
||||
return IPInfo.findByPk(Sequelize.fn('IP_TO_BIN', ip));
|
||||
}
|
||||
|
||||
export async function getInfoToIps(ips) {
|
||||
|
@ -155,8 +142,15 @@ export async function getInfoToIps(ips) {
|
|||
}
|
||||
try {
|
||||
const result = await IPInfo.findAll({
|
||||
attributes: ['ip', 'uuid', 'country', 'cidr', 'org', 'pcheck'],
|
||||
where: { ip: ips },
|
||||
attributes: [
|
||||
[Sequelize.fn('BIN_TO_IP', Sequelize.col('ip')), 'ip'],
|
||||
[Sequelize.fn('BIN_TO_UUID', Sequelize.col('uuid')), 'uuid'],
|
||||
'country',
|
||||
'cidr',
|
||||
'org',
|
||||
'pcheck',
|
||||
],
|
||||
where: { ip: ips.map((ip) => Sequelize.fn('IP_TO_BIN', ip)) },
|
||||
raw: true,
|
||||
});
|
||||
result.forEach((obj) => {
|
||||
|
|
|
@ -4,25 +4,18 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { DataTypes } from 'sequelize';
|
||||
import Sequelize, { DataTypes } from 'sequelize';
|
||||
import sequelize from './sequelize';
|
||||
import Channel from './Channel';
|
||||
import RegUser from './RegUser';
|
||||
|
||||
const Message = sequelize.define('Message', {
|
||||
// Message ID
|
||||
id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
name: {
|
||||
type: `${DataTypes.CHAR(32)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
defaultValue: 'mx',
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
flag: {
|
||||
type: DataTypes.CHAR(2),
|
||||
defaultValue: 'xx',
|
||||
|
@ -43,16 +36,57 @@ const Message = sequelize.define('Message', {
|
|||
},
|
||||
});
|
||||
|
||||
Message.belongsTo(Channel, {
|
||||
as: 'channel',
|
||||
foreignKey: 'cid',
|
||||
onDelete: 'cascade',
|
||||
});
|
||||
export async function storeMessage(
|
||||
flag,
|
||||
message,
|
||||
cid,
|
||||
uid,
|
||||
) {
|
||||
await Channel.update({
|
||||
lastMessage: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
}, {
|
||||
where: {
|
||||
id: cid,
|
||||
},
|
||||
});
|
||||
return Message.create({
|
||||
flag,
|
||||
message,
|
||||
cid,
|
||||
uid,
|
||||
});
|
||||
}
|
||||
|
||||
Message.belongsTo(RegUser, {
|
||||
as: 'user',
|
||||
foreignKey: 'uid',
|
||||
onDelete: 'cascade',
|
||||
});
|
||||
export async function getMessagesForChannel(cid, limit) {
|
||||
const models = await Message.findAll({
|
||||
attributes: [
|
||||
'message',
|
||||
'uid',
|
||||
'flag',
|
||||
[
|
||||
Sequelize.fn('UNIX_TIMESTAMP', Sequelize.col('createdAt')),
|
||||
'ts',
|
||||
],
|
||||
],
|
||||
include: {
|
||||
association: 'user',
|
||||
attributes: ['name'],
|
||||
},
|
||||
where: { cid },
|
||||
limit,
|
||||
order: [['createdAt', 'DESC']],
|
||||
raw: true,
|
||||
});
|
||||
return models.map((model) => {
|
||||
const {
|
||||
[user.name]: name,
|
||||
message,
|
||||
flag,
|
||||
uid,
|
||||
ts,
|
||||
} = model;
|
||||
return [name, message, flag, uid, ts];
|
||||
});
|
||||
}
|
||||
|
||||
export default Message;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
/**
|
||||
* Created by HF
|
||||
*
|
||||
* This is the database of the data for registered Users
|
||||
*
|
||||
|
@ -9,6 +8,9 @@ import Sequelize, { DataTypes, QueryTypes } from 'sequelize';
|
|||
|
||||
import sequelize from './sequelize';
|
||||
import { generateHash } from '../../utils/hash';
|
||||
import { USERLVL } from '../../core/constants';
|
||||
|
||||
export { USERLVL } from '../../core/constants';
|
||||
|
||||
|
||||
const RegUser = sequelize.define('User', {
|
||||
|
@ -21,6 +23,7 @@ const RegUser = sequelize.define('User', {
|
|||
email: {
|
||||
type: DataTypes.CHAR(40),
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
},
|
||||
|
||||
name: {
|
||||
|
@ -38,22 +41,28 @@ const RegUser = sequelize.define('User', {
|
|||
defaultValue: false,
|
||||
},
|
||||
|
||||
// null if external oauth authentication
|
||||
// null if only ever used external oauth
|
||||
password: {
|
||||
type: DataTypes.CHAR(60),
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
// currently just moderator
|
||||
userlvl: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
defaultValue: USERLVL.REGISTERED,
|
||||
},
|
||||
|
||||
// currently just moderator TODO Delete
|
||||
roles: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
|
||||
// mail and Minecraft verified
|
||||
// if account is mail verified TODO Delete
|
||||
verified: {
|
||||
type: DataTypes.TINYINT,
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
|
@ -65,11 +74,13 @@ const RegUser = sequelize.define('User', {
|
|||
defaultValue: 0,
|
||||
},
|
||||
|
||||
// TODO Delete, replaced by ThreePID table
|
||||
discordid: {
|
||||
type: DataTypes.CHAR(20),
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
// TODO Delete, replaced by ThreePID table
|
||||
redditid: {
|
||||
type: DataTypes.CHAR(10),
|
||||
allowNull: true,
|
||||
|
@ -81,52 +92,22 @@ const RegUser = sequelize.define('User', {
|
|||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
// flag == country code
|
||||
flag: {
|
||||
type: DataTypes.CHAR(2),
|
||||
defaultValue: 'xx',
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
lastLogIn: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
timestamps: true,
|
||||
updatedAt: false,
|
||||
|
||||
getterMethods: {
|
||||
mailVerified() {
|
||||
return this.verified & 0x01;
|
||||
},
|
||||
|
||||
blockDm() {
|
||||
return this.blocks & 0x01;
|
||||
},
|
||||
|
||||
isMod() {
|
||||
return this.roles & 0x01;
|
||||
},
|
||||
},
|
||||
|
||||
setterMethods: {
|
||||
mailVerified(num) {
|
||||
const val = (num) ? (this.verified | 0x01) : (this.verified & ~0x01);
|
||||
this.setDataValue('verified', val);
|
||||
},
|
||||
|
||||
blockDm(num) {
|
||||
const val = (num) ? (this.blocks | 0x01) : (this.blocks & ~0x01);
|
||||
this.setDataValue('blocks', val);
|
||||
},
|
||||
|
||||
isMod(num) {
|
||||
const val = (num) ? (this.roles | 0x01) : (this.roles & ~0x01);
|
||||
this.setDataValue('roles', val);
|
||||
},
|
||||
|
||||
password(value) {
|
||||
if (value) this.setDataValue('password', generateHash(value));
|
||||
},
|
||||
|
@ -192,10 +173,11 @@ export async function getNamesToIds(ids) {
|
|||
}
|
||||
|
||||
/*
|
||||
* take array of {id: useId, ...} object and resolve
|
||||
* user information
|
||||
* take array of objects that include user ids and add
|
||||
* user informations if user is not private
|
||||
* @param rawRanks array of {id: userId, ...} objects
|
||||
*/
|
||||
export async function populateRanking(rawRanks) {
|
||||
export async function populateIdObj(rawRanks) {
|
||||
if (!rawRanks.length) {
|
||||
return rawRanks;
|
||||
}
|
||||
|
@ -219,15 +201,14 @@ export async function populateRanking(rawRanks) {
|
|||
},
|
||||
raw: true,
|
||||
});
|
||||
for (let i = 0; i < userData.length; i += 1) {
|
||||
const { id, name, age } = userData[i];
|
||||
for (const { id, name, age } of userData) {
|
||||
const dat = rawRanks.find((r) => r.id === id);
|
||||
if (dat) {
|
||||
dat.name = name;
|
||||
dat.age = age;
|
||||
}
|
||||
}
|
||||
return rawRanks.filter((r) => r.name);
|
||||
return rawRanks;
|
||||
}
|
||||
|
||||
export default RegUser;
|
||||
|
|
30
src/data/sql/ThreePID.js
Normal file
30
src/data/sql/ThreePID.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
*
|
||||
* Storing third party IDs for oauth login
|
||||
*/
|
||||
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
import sequelize from './sequelize';
|
||||
|
||||
export { THREEP } from '../../core/constants';
|
||||
|
||||
const ThreePID = sequelize.define('ThreePID', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
provider: {
|
||||
type: DataTypes.TINYINT,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
tpid: {
|
||||
type: DataTypes.CHAR(20),
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default ThreePID;
|
66
src/data/sql/UserIP.js
Normal file
66
src/data/sql/UserIP.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
*
|
||||
* Last IP and useragent a user connected with
|
||||
*
|
||||
*/
|
||||
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from './sequelize';
|
||||
|
||||
const UserIP = sequelize.define('UserIP', {
|
||||
ua: {
|
||||
type: DataTypes.CHAR(200),
|
||||
},
|
||||
|
||||
lastSeen: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
timestamps: false,
|
||||
|
||||
setterMethods: {
|
||||
ua(value) {
|
||||
const URIencode = encodeURIComponent(value);
|
||||
this.setDataValue('ua', URIencode.slice(0, 200));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* save lastIp of user
|
||||
* @param uid userId
|
||||
* @param ip IP
|
||||
* @param ua UserAgent
|
||||
*/
|
||||
export function updateLastIp(uid, ip, ua) {
|
||||
return UserIP.upsert({
|
||||
uid,
|
||||
ip,
|
||||
ua,
|
||||
lastSeen: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* update lastSeen
|
||||
*/
|
||||
export async function touch(uid, ip, ua) {
|
||||
try {
|
||||
const data = {
|
||||
lastSeen: new Date().toISOString(),
|
||||
};
|
||||
if (ua) {
|
||||
data.ua = ua;
|
||||
}
|
||||
await UserIP.update(data, {
|
||||
where: { uid, ip },
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default UserIP;
|
126
src/data/sql/WhoisRange.js
Normal file
126
src/data/sql/WhoisRange.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
import Sequelize, { DataTypes, QueryTypes } from 'sequelize';
|
||||
import sequelize from './sequelize';
|
||||
|
||||
/*
|
||||
* Information of IP Ranges from whois,
|
||||
* min and max are the upper and lower bound of IPs within the range,
|
||||
* stored in the same 64bit format as IP in IPInfo.js
|
||||
*
|
||||
* Will be kept indefinitelly, updated regularly
|
||||
*/
|
||||
const WhoisRange = sequelize.define('WhoisRange', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
min: {
|
||||
type: 'VARBINARY(8)',
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
max: {
|
||||
type: 'VARBINARY(8)',
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
mask: {
|
||||
type: DataTypes.TINYINT.UNSIGNED,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
country: {
|
||||
type: DataTypes.CHAR(2),
|
||||
defaultValue: 'xx',
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
org: {
|
||||
type: `${DataTypes.STRING(60)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
set(value) {
|
||||
this.setDataValue('org', value.slice(0, 60));
|
||||
},
|
||||
},
|
||||
|
||||
descr: {
|
||||
type: `${DataTypes.STRING(60)} CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci`,
|
||||
set(value) {
|
||||
this.setDataValue('descr', value.slice(0, 60));
|
||||
},
|
||||
},
|
||||
|
||||
asn: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
},
|
||||
|
||||
checkedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks if range that includes ip exists,
|
||||
* If it exists, updates the IPInfos table to associate the IP to this ranges
|
||||
* Procedure got declared in ./sequelize.js
|
||||
* @param ip as string
|
||||
* @return {
|
||||
* wid as number,
|
||||
* cidr as string,
|
||||
* asn as number,
|
||||
* cuntry as two letter lowercase code,
|
||||
* org as string,
|
||||
* descr as string,
|
||||
* } if exists or {
|
||||
* host as string (whois host to query)
|
||||
* } if whois host is known or null if not
|
||||
*/
|
||||
export async function getWhoisRangeOfIp(ip) {
|
||||
try {
|
||||
// return cidr, country, org, descr, asn, checkedAt
|
||||
const rangeq = sequelize.query(
|
||||
'CALL RANGE_OF_IP($1)',
|
||||
{
|
||||
bind: [ip],
|
||||
type: QueryTypes.SELECT,
|
||||
raw: true,
|
||||
plain: true,
|
||||
}
|
||||
);
|
||||
return rangeq;
|
||||
} catch (err) {
|
||||
console.error(`SQL Error on getRangeOfIp: ${err.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save whois data to range
|
||||
* @param whoisData
|
||||
* @return id if successful, null if not
|
||||
*/
|
||||
export async function saveWhoisRange(whoisData) {
|
||||
const { range, country, org, descr, asn } = whoisData;
|
||||
try {
|
||||
const [rangeq] = await WhoisRange.upsert({
|
||||
min: Sequelize.fn('UNHEX', range[0]),
|
||||
max: Sequelize.fn('UNHEX', range[1]),
|
||||
mask: range[2],
|
||||
country,
|
||||
org,
|
||||
descr,
|
||||
asn,
|
||||
checkedAt: new Date(),
|
||||
});
|
||||
return rangeq.id;
|
||||
} catch (err) {
|
||||
console.error(`SQL Error on saveIpRange: ${err.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
82
src/data/sql/WhoisReferral.js
Normal file
82
src/data/sql/WhoisReferral.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import Sequelize, { DataTypes, QueryTypes } from 'sequelize';
|
||||
import sequelize from './sequelize';
|
||||
|
||||
/*
|
||||
* Information of whois hosts responsible for IPRanges,
|
||||
* min and max are the upper and lower bound of IPs within the range,
|
||||
* stored in the same 64bit format as IP in IPInfo.js
|
||||
*
|
||||
* Will be kept indefinitelly, updated regularly
|
||||
*/
|
||||
const WhoisReferral = sequelize.define('WhoisReferral', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
min: {
|
||||
type: 'VARBINARY(8)',
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
max: {
|
||||
type: 'VARBINARY(8)',
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
mask: {
|
||||
type: DataTypes.TINYINT.UNSIGNED,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
host: {
|
||||
type: DataTypes.STRING(60),
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
checkedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
export async function getWhoisReferraloOfIp(ip) {
|
||||
try {
|
||||
// return cidr, country, org, descr, asn, checkedAt
|
||||
const rangeq = sequelize.query(
|
||||
'CALL WHOIS_REFERRAL_OF_IP($1)',
|
||||
{
|
||||
bind: [ip],
|
||||
type: QueryTypes.SELECT,
|
||||
raw: true,
|
||||
plain: true,
|
||||
}
|
||||
);
|
||||
return rangeq?.host;
|
||||
} catch (err) {
|
||||
console.error(`SQL Error on getRangeOfIp: ${err.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function saveWhoisReferral(range, host) {
|
||||
try {
|
||||
const [rangeq] = await WhoisReferral.upsert({
|
||||
min: Sequelize.fn('UNHEX', range[0]),
|
||||
max: Sequelize.fn('UNHEX', range[1]),
|
||||
mask: range[2],
|
||||
host,
|
||||
checkedAt: new Date(),
|
||||
});
|
||||
return rangeq.id;
|
||||
} catch (err) {
|
||||
console.error(`SQL Error on saveIpRange: ${err.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
import sequelize from './sequelize';
|
||||
import Whitelist from './Whitelist';
|
||||
import RegUser from './RegUser';
|
||||
import RegUser, { USERLVL } from './RegUser';
|
||||
import Channel from './Channel';
|
||||
import UserChannel from './UserChannel';
|
||||
import Message from './Message';
|
||||
import UserBlock from './UserBlock';
|
||||
import IPInfo from './IPInfo';
|
||||
import WhoisRange from './WhoisRange';
|
||||
import UserIP from './UserIP';
|
||||
import ThreePID, { THREEP } from './ThreePID';
|
||||
|
||||
/*
|
||||
* User Channel access
|
||||
|
@ -18,6 +22,54 @@ Channel.belongsToMany(RegUser, {
|
|||
through: UserChannel,
|
||||
});
|
||||
|
||||
/*
|
||||
* ip informations of user
|
||||
*/
|
||||
IPInfo.belongsToMany(RegUser, {
|
||||
through: UserIP,
|
||||
foreignKey: 'ip',
|
||||
});
|
||||
RegUser.belongsToMany(IPInfo, {
|
||||
through: UserIP,
|
||||
foreignKey: 'uid',
|
||||
});
|
||||
|
||||
/*
|
||||
* third party ids for oauth login
|
||||
*/
|
||||
RegUser.hasMany(ThreePID, {
|
||||
as: 'tp',
|
||||
foreignKey: {
|
||||
name: 'uid',
|
||||
allowNull: false,
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
});
|
||||
ThreePID.belongsTo(RegUser);
|
||||
|
||||
/*
|
||||
* whois range for ip
|
||||
*/
|
||||
WhoisRange.hasMany(IPInfo, {
|
||||
as: 'whois',
|
||||
foreignKey: 'wid',
|
||||
});
|
||||
IPInfo.belongsTo(WhoisRange);
|
||||
|
||||
/*
|
||||
* chat messages
|
||||
*/
|
||||
Message.belongsTo(Channel, {
|
||||
as: 'channel',
|
||||
foreignKey: 'cid',
|
||||
onDelete: 'CASCADE',
|
||||
});
|
||||
Message.belongsTo(RegUser, {
|
||||
as: 'user',
|
||||
foreignKey: 'uid',
|
||||
onDelete: 'CASCADE',
|
||||
});
|
||||
|
||||
/*
|
||||
* User blocks of other user
|
||||
*
|
||||
|
@ -35,12 +87,41 @@ RegUser.belongsToMany(RegUser, {
|
|||
foreignKey: 'buid',
|
||||
});
|
||||
|
||||
/*
|
||||
* includes for RegUsert
|
||||
* that should be available on ordinary
|
||||
* login
|
||||
*/
|
||||
const regUserQueryInclude = [{
|
||||
model: Channel,
|
||||
as: 'channel',
|
||||
include: [{
|
||||
model: RegUser,
|
||||
as: 'dmu1',
|
||||
foreignKey: 'dmu1id',
|
||||
attributes: ['id', 'name'],
|
||||
}, {
|
||||
model: RegUser,
|
||||
as: 'dmu2',
|
||||
foreignKey: 'dmu2id',
|
||||
attributes: ['id', 'name'],
|
||||
}],
|
||||
}, {
|
||||
association: 'blocked',
|
||||
attributes: ['id', 'name'],
|
||||
}];
|
||||
|
||||
export {
|
||||
regUserQueryInclude,
|
||||
Whitelist,
|
||||
RegUser,
|
||||
Channel,
|
||||
UserChannel,
|
||||
Message,
|
||||
UserBlock,
|
||||
WhoisRange,
|
||||
IPInfo,
|
||||
ThreePID,
|
||||
USERLVL,
|
||||
THREEP,
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import Sequelize from 'sequelize';
|
||||
|
||||
import logger from '../../core/logger';
|
||||
import {
|
||||
MYSQL_HOST, MYSQL_DATABASE, MYSQL_USER, MYSQL_PW, LOG_MYSQL,
|
||||
} from '../../core/config';
|
||||
|
@ -18,11 +17,81 @@ const sequelize = new Sequelize(MYSQL_DATABASE, MYSQL_USER, MYSQL_PW, {
|
|||
idle: 10000,
|
||||
acquire: 10000,
|
||||
},
|
||||
logging: (LOG_MYSQL) ? (...msg) => logger.info(msg) : false,
|
||||
// eslint-disable-next-line no-console
|
||||
logging: (LOG_MYSQL) ? (...msg) => console.info(msg) : false,
|
||||
dialectOptions: {
|
||||
connectTimeout: 10000,
|
||||
multipleStatements: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const sync = async () => {
|
||||
await sequelize.sync({ alter: { drop: true } });
|
||||
|
||||
/*
|
||||
* custom functions (for IP_BIN explenation, look into IP_Info comments)
|
||||
*/
|
||||
const functions = {
|
||||
IP_TO_BIN: `CREATE FUNCTION IF NOT EXISTS IP_TO_BIN(ip VARCHAR(39)) returns VARBINARY(8) DETERMINISTIC CONTAINS SQL
|
||||
BEGIN
|
||||
DECLARE longBin VARBINARY(16);
|
||||
SET longBin = INET6_ATON(ip);
|
||||
IF LENGTH(longBin) > 4
|
||||
THEN
|
||||
RETURN (cast(longBin as binary(8)));
|
||||
ELSE
|
||||
RETURN (longBin);
|
||||
END IF;
|
||||
END`,
|
||||
BIN_TO_IP: `CREATE FUNCTION IF NOT EXISTS BIN_TO_IP(bin VARBINARY(8)) returns VARCHAR(21) DETERMINISTIC CONTAINS SQL
|
||||
BEGIN
|
||||
RETURN (INET6_NTOA(IF(LENGTH(bin) > 4, CAST(bin as BINARY(16)), bin)));
|
||||
END`,
|
||||
RANGE_OF_IP: `CREATE PROCEDURE IF NOT EXISTS RANGE_OF_IP(ip VARCHAR(39)) READS SQL DATA
|
||||
BEGIN
|
||||
DECLARE binIp VARBINARY(8);
|
||||
SET binIp = IP_TO_BIN(ip);
|
||||
SELECT id as wid, CONCAT(BIN_TO_IP(min), '/', mask) AS cidr, country, org, descr, asn FROM WhoisRanges WHERE min <= binIp AND max >= binIp AND LENGTH(binIP) = LENGTH(min) AND checkedAt > (NOW() - INTERVAL 1 MONTH) LIMIT 1;
|
||||
IF FOUND_ROWS() = 0
|
||||
THEN
|
||||
SELECT host FROM WhoisReferrals WHERE min <= binIp AND max >= binIp AND LENGTH(binIp) = LENGTH(min) AND checkedAt > (NOW() - INTERVAL 1 MONTH) LIMIT 1;
|
||||
END IF;
|
||||
END`,
|
||||
RANGE_OF_IP_OI: `CREATE PROCEDURE IF NOT EXISTS RANGE_OF_IP(ip VARCHAR(39)) READS SQL DATA
|
||||
BEGIN
|
||||
DECLARE binIp VARBINARY(8);
|
||||
DECLARE q_id INTEGER UNSIGNED;
|
||||
DECLARE q_cidr VARCHAR(22);
|
||||
DECLARE q_country CHAR(2);
|
||||
DECLARE q_org VARCHAR(60);
|
||||
DECLARE q_descr VARCHAR(60);
|
||||
DECLARE q_asn VARCHAR(12);
|
||||
SET binIp = IP_TO_BIN(ip);
|
||||
SELECT id, CONCAT(BIN_TO_IP(min), '/', mask), country, org, descr, asn FROM WhoisRanges WHERE min <= binIp AND max >= binIp AND LENGTH(binIP) = LENGTH(min) AND checkedAt > (NOW() - INTERVAL 1 MONTH) LIMIT 1 INTO q_id, q_cidr, q_country, q_org, q_descr, q_asn;
|
||||
IF q_id IS NULL
|
||||
THEN
|
||||
SELECT host FROM WhoisReferrals WHERE min <= binIp AND max >= binIp AND LENGTH(binIp) = LENGTH(min) AND checkedAt > (NOW() - INTERVAL 1 MONTH) LIMIT 1;
|
||||
ELSE
|
||||
INSERT INTO IPInfos (ip, uuid, wid) VALUES (binIP, UUID_TO_BIN(UUID()), q_id) ON DUPLICATE KEY UPDATE wid = q_id;
|
||||
SELECT q_cidr AS cidr, q_country AS country, q_org AS org, q_descr AS descr, q_asn AS asn;
|
||||
END IF;
|
||||
END`,
|
||||
WHOIS_REFERRAL_OF_IP: `CREATE PROCEDURE IF NOT EXISTS WHOIS_REFERRAL_OF_IP(ip VARCHAR(39)) READS SQL DATA
|
||||
BEGIN
|
||||
DECLARE binIp VARBINARY(8);
|
||||
SET binIp = IP_TO_BIN(ip);
|
||||
SELECT host FROM WhoisReferrals WHERE min <= binIp AND max >= binIp AND LENGTH(binIP) = LENGTH(min);
|
||||
END`,
|
||||
};
|
||||
|
||||
for (const name of Object.keys(functions)) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sequelize.query(functions[name], { raw: true });
|
||||
} catch (err) {
|
||||
console.error(`Error on creating SQL Function ${name}: ${err.message}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default sequelize;
|
||||
|
|
|
@ -2,6 +2,7 @@ import express from 'express';
|
|||
|
||||
import logger from '../core/logger';
|
||||
import { RegUser } from '../data/sql';
|
||||
import { USERLVL } from '../core/constants';
|
||||
import { getIPFromRequest } from '../utils/ip';
|
||||
import { compareToHash } from '../utils/hash';
|
||||
import { APISOCKET_KEY } from '../core/config';
|
||||
|
@ -48,7 +49,7 @@ router.post('/checklogin', async (req, res) => {
|
|||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'verified',
|
||||
'userlvl',
|
||||
],
|
||||
};
|
||||
let userString;
|
||||
|
@ -94,14 +95,14 @@ router.post('/checklogin', async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
logger.info(`ADMINAPI: User ${reguser.name} / ${reguser.id} got loged in`);
|
||||
logger.info(`ADMINAPI: User ${reguser.name} / ${reguser.id} got logged in`);
|
||||
res.json({
|
||||
success: true,
|
||||
userdata: {
|
||||
id: reguser.id,
|
||||
name: reguser.name,
|
||||
email: reguser.email,
|
||||
verified: !!reguser.verified,
|
||||
verified: reguser.userlvl >= USERLVL.VERIFIED,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { getHostFromRequest } from '../../../utils/ip';
|
|||
import { compareToHash } from '../../../utils/hash';
|
||||
import { checkIfMuted } from '../../../data/redis/chat';
|
||||
import { checkIfMailDisposable } from '../../../core/isAllowed';
|
||||
import { USERLVL } from '../../../core/constants';
|
||||
|
||||
async function validate(email, password, t, gettext) {
|
||||
const errors = [];
|
||||
|
@ -64,13 +65,18 @@ export default async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
await user.regUser.update({
|
||||
const { regUser } = user;
|
||||
let { userlvl } = regUser;
|
||||
if (userlvl <= USERLVL.VERIFIED && userlvl > USERLVL.REGISTERED) {
|
||||
userlvl = USERLVL.REGISTERED;
|
||||
}
|
||||
await regUser.update({
|
||||
email,
|
||||
mailVerified: false,
|
||||
userlvl,
|
||||
});
|
||||
|
||||
const host = getHostFromRequest(req);
|
||||
mailProvider.sendVerifyMail(email, user.regUser.name, host, lang);
|
||||
mailProvider.sendVerifyMail(email, regUser.name, host, lang);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
|
|
@ -5,7 +5,7 @@ import { RegUser } from '../../../data/sql';
|
|||
import mailProvider from '../../../core/MailProvider';
|
||||
import getMe from '../../../core/me';
|
||||
import { getIPFromRequest, getHostFromRequest } from '../../../utils/ip';
|
||||
import { checkIfMailDisposable } from '../../../core/isAllowed';
|
||||
import { checkIfMailDisposable } from '../../../core/ipUserIntel';
|
||||
import {
|
||||
validateEMail,
|
||||
validateName,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import mailProvider from '../../../core/MailProvider';
|
||||
import { getHostFromRequest } from '../../../utils/ip';
|
||||
import { USERLVL } from '../../../data/sql';
|
||||
|
||||
export default async (req, res) => {
|
||||
const { user, lang } = req;
|
||||
|
@ -15,8 +16,8 @@ export default async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const { name, email, mailVerified } = user.regUser;
|
||||
if (mailVerified) {
|
||||
const { name, email, userlvl } = user.regUser;
|
||||
if (userlvl >= USERLVL.VERIFIED) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
errors: ['You are already verified.'],
|
||||
|
|
|
@ -16,7 +16,7 @@ async function validate(email, gettext) {
|
|||
}
|
||||
|
||||
export default async (req, res) => {
|
||||
const ip = req.trueIp;
|
||||
const { ip } = req.user;
|
||||
const { email } = req.body;
|
||||
const { gettext } = req.ttag;
|
||||
|
||||
|
|
|
@ -31,9 +31,6 @@ async function block(req, res) {
|
|||
if (!userName && !userId) {
|
||||
errors.push('No userId or userName defined');
|
||||
}
|
||||
if (!user || !user.regUser) {
|
||||
errors.push('You are not logged in');
|
||||
}
|
||||
if (user && userId && user.id === userId) {
|
||||
errors.push('You can not block yourself.');
|
||||
}
|
||||
|
|
|
@ -10,17 +10,9 @@ async function blockdm(req, res) {
|
|||
const { block } = req.body;
|
||||
const { user } = req;
|
||||
|
||||
const errors = [];
|
||||
if (typeof block !== 'boolean') {
|
||||
errors.push('Not defined if blocking or unblocking');
|
||||
}
|
||||
if (!user || !user.regUser) {
|
||||
errors.push('You are not logged in');
|
||||
}
|
||||
if (errors.length) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
errors,
|
||||
res.status(400).json({
|
||||
errors: ['Not defined if blocking or unblocking'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
*
|
||||
* tell user his own IID
|
||||
*/
|
||||
import {
|
||||
getIPFromRequest,
|
||||
|
@ -16,7 +16,6 @@ async function getiid(req, res, next) {
|
|||
);
|
||||
|
||||
const iid = await getIIDofIP(ip);
|
||||
|
||||
if (!iid) {
|
||||
throw new Error('Could not get IID');
|
||||
}
|
||||
|
|
|
@ -2,9 +2,7 @@ import express from 'express';
|
|||
|
||||
import session from '../../core/session';
|
||||
import passport from '../../core/passport';
|
||||
import logger from '../../core/logger';
|
||||
import User from '../../data/User';
|
||||
import { getIPFromRequest } from '../../utils/ip';
|
||||
|
||||
import me from './me';
|
||||
import auth from './auth';
|
||||
|
@ -35,7 +33,6 @@ router.use(express.json());
|
|||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
router.use((err, req, res, next) => {
|
||||
logger.warn(`Got invalid json from ${req.trueIp} on ${req.originalUrl}`);
|
||||
res.status(400).json({
|
||||
errors: [{ msg: 'Invalid Request' }],
|
||||
});
|
||||
|
@ -54,8 +51,7 @@ router.use(session);
|
|||
/*
|
||||
* at this point we could use the session id to get
|
||||
* stuff without having to verify the whole user,
|
||||
* which would avoid SQL requests and it got used previously
|
||||
* when we set pixels via api/pixel (new removed)
|
||||
* which would avoid SQL requests
|
||||
*/
|
||||
|
||||
/*
|
||||
|
@ -69,22 +65,41 @@ router.use(passport.session());
|
|||
|
||||
/*
|
||||
* modtools
|
||||
* (does not json bodies, but urlencoded)
|
||||
* (does not take urlencoded bodies)
|
||||
*/
|
||||
router.use('/modtools', modtools);
|
||||
|
||||
/*
|
||||
* create dummy user with just ip if not
|
||||
* logged in
|
||||
* create unregistered user by request if
|
||||
* not logged in
|
||||
*/
|
||||
router.use(async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.user = new User();
|
||||
await req.user.initialize(null, getIPFromRequest(req));
|
||||
req.user = new User(req);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/chathistory', chatHistory);
|
||||
|
||||
router.get('/me', me);
|
||||
|
||||
router.use('/auth', auth);
|
||||
|
||||
router.post('/banme', banme);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
if (!req.user.isRegistered) {
|
||||
next(new Error('You are not logged in'));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
/*
|
||||
* require registered user after this point
|
||||
*/
|
||||
|
||||
router.post('/startdm', startDm);
|
||||
|
||||
router.post('/leavechan', leaveChan);
|
||||
|
@ -95,13 +110,9 @@ router.post('/blockdm', blockdm);
|
|||
|
||||
router.post('/privatize', privatize);
|
||||
|
||||
router.get('/chathistory', chatHistory);
|
||||
|
||||
router.get('/me', me);
|
||||
|
||||
router.post('/banme', banme);
|
||||
|
||||
router.use('/auth', auth);
|
||||
/*
|
||||
* error handling
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
router.use((err, req, res, next) => {
|
||||
|
|
|
@ -11,17 +11,9 @@ async function leaveChan(req, res) {
|
|||
const channelId = parseInt(req.body.channelId, 10);
|
||||
const { user } = req;
|
||||
|
||||
const errors = [];
|
||||
if (channelId && Number.isNaN(channelId)) {
|
||||
errors.push('Invalid channelId');
|
||||
}
|
||||
if (!user || !user.regUser) {
|
||||
errors.push('You are not logged in');
|
||||
}
|
||||
if (errors.length) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
errors,
|
||||
res.status(400).json({
|
||||
errors: ['Invalid channelId'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -35,8 +27,7 @@ async function leaveChan(req, res) {
|
|||
}
|
||||
}
|
||||
if (!channel) {
|
||||
res.status(401);
|
||||
res.json({
|
||||
res.status(401).json({
|
||||
errors: ['You are not in this channel'],
|
||||
});
|
||||
return;
|
||||
|
@ -49,8 +40,7 @@ async function leaveChan(req, res) {
|
|||
* Faction and Default channels should be impossible to leave
|
||||
*/
|
||||
if (channel.type !== 1) {
|
||||
res.status(401);
|
||||
res.json({
|
||||
res.status(401).json({
|
||||
errors: ['Can not leave this channel'],
|
||||
});
|
||||
return;
|
||||
|
|
|
@ -9,7 +9,7 @@ export default async (req, res, next) => {
|
|||
try {
|
||||
const { user, lang } = req;
|
||||
const userdata = await getMe(user, lang);
|
||||
user.updateLogInTimestamp();
|
||||
user.touch();
|
||||
res.json(userdata);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
|
|
@ -9,7 +9,6 @@ import multer from 'multer';
|
|||
|
||||
import CanvasCleaner from '../../core/CanvasCleaner';
|
||||
import chatProvider from '../../core/ChatProvider';
|
||||
import { getIPFromRequest } from '../../utils/ip';
|
||||
import { escapeMd } from '../../core/utils';
|
||||
import logger, { modtoolsLogger } from '../../core/logger';
|
||||
import {
|
||||
|
@ -24,6 +23,7 @@ import {
|
|||
removeMod,
|
||||
makeMod,
|
||||
} from '../../core/adminfunctions';
|
||||
import { USERLVL } from '../../data/sql';
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
@ -41,31 +41,22 @@ const upload = multer({
|
|||
|
||||
|
||||
/*
|
||||
* make sure User is logged in and mod or mod
|
||||
* make sure User is logged in and at least Mod
|
||||
*/
|
||||
router.use(async (req, res, next) => {
|
||||
const ip = getIPFromRequest(req);
|
||||
if (!req.user) {
|
||||
logger.warn(
|
||||
`MODTOOLS: ${ip} tried to access modtools without login`,
|
||||
);
|
||||
const { t } = req.ttag;
|
||||
res.status(403).send(t`You are not logged in`);
|
||||
next(new Error(t`You are not logged in`));
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* 1 = Admin
|
||||
* 2 = Mod
|
||||
*/
|
||||
if (!req.user.userlvl) {
|
||||
if (req.user.userlvl < USERLVL.MOD) {
|
||||
logger.warn(
|
||||
`MODTOOLS: ${ip} / ${req.user.id} tried to access modtools`,
|
||||
`MODTOOLS: ${req.user.ip} / ${req.user.id} tried to access modtools`,
|
||||
);
|
||||
const { t } = req.ttag;
|
||||
res.status(403).send(t`You are not allowed to access this page`);
|
||||
next(new Error(t`You are not allowed to access this page`));
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
|
@ -77,7 +68,7 @@ router.post('/', upload.single('image'), async (req, res, next) => {
|
|||
const aLogger = (text) => {
|
||||
const timeString = new Date().toLocaleTimeString();
|
||||
// eslint-disable-next-line max-len
|
||||
const logText = `@[${escapeMd(req.user.regUser.name)}](${req.user.id}) ${text}`;
|
||||
const logText = `@[${escapeMd(req.user.name)}](${req.user.id}) ${text}`;
|
||||
modtoolsLogger.info(
|
||||
`${timeString} | MODTOOLS> ${logText}`,
|
||||
);
|
||||
|
@ -90,7 +81,7 @@ router.post('/', upload.single('image'), async (req, res, next) => {
|
|||
};
|
||||
|
||||
const bLogger = (text) => {
|
||||
logger.info(`IID> ${req.user.regUser.name}[${req.user.id}]> ${text}`);
|
||||
logger.info(`IID> ${req.user.name}[${req.user.id}]> ${text}`);
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -186,7 +177,7 @@ router.post('/', upload.single('image'), async (req, res, next) => {
|
|||
brcoor,
|
||||
canvasid,
|
||||
aLogger,
|
||||
(req.user.userlvl === 1),
|
||||
(req.user.userlvl >= USERLVL.ADMIN),
|
||||
);
|
||||
res.status(ret).send(msg);
|
||||
return;
|
||||
|
@ -202,7 +193,7 @@ router.post('/', upload.single('image'), async (req, res, next) => {
|
|||
* just admins past here, no Mods
|
||||
*/
|
||||
router.use(async (req, res, next) => {
|
||||
if (req.user.userlvl !== 1) {
|
||||
if (req.user.userlvl < USERLVL.ADMIN) {
|
||||
const { t } = req.ttag;
|
||||
res.status(403).send(t`Just admins can do that`);
|
||||
return;
|
||||
|
@ -215,7 +206,7 @@ router.use(async (req, res, next) => {
|
|||
*/
|
||||
router.post('/', async (req, res, next) => {
|
||||
const aLogger = (text) => {
|
||||
logger.info(`ADMIN> ${req.user.regUser.name}[${req.user.id}]> ${text}`);
|
||||
logger.info(`ADMIN> ${req.user.name}[${req.user.id}]> ${text}`);
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -251,8 +242,8 @@ router.post('/', async (req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
router.use(async (req, res) => {
|
||||
res.status(400).send('Invalid request');
|
||||
router.use(async (req, res, next) => {
|
||||
next(new Error('Invalid request'));
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
|
|
@ -9,17 +9,9 @@ async function privatize(req, res) {
|
|||
const { priv } = req.body;
|
||||
const { user } = req;
|
||||
|
||||
const errors = [];
|
||||
if (typeof priv !== 'boolean') {
|
||||
errors.push('Not defined if setting or unsetting private');
|
||||
}
|
||||
if (!user || !user.regUser) {
|
||||
errors.push('You are not logged in');
|
||||
}
|
||||
if (errors.length) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
errors,
|
||||
res.status(400).json({
|
||||
errors: ['Not defined if setting or unsetting private'],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -27,10 +27,7 @@ async function startDm(req, res) {
|
|||
if (!userName && !userId) {
|
||||
errors.push('No userId or userName defined');
|
||||
}
|
||||
if (!user || !user.regUser) {
|
||||
errors.push('You are not logged in');
|
||||
}
|
||||
if (user && userId && user.id === userId) {
|
||||
if (userId && user.id === userId) {
|
||||
errors.push('You can not DM yourself.');
|
||||
}
|
||||
if (errors.length) {
|
||||
|
@ -73,7 +70,7 @@ async function startDm(req, res) {
|
|||
}
|
||||
|
||||
logger.info(
|
||||
`Creating DM Channel between ${user.regUser.name} and ${userName}`,
|
||||
`Creating DM Channel between ${user.name} and ${userName}`,
|
||||
);
|
||||
/*
|
||||
* start DM session
|
||||
|
|
|
@ -10,7 +10,7 @@ import http from 'http';
|
|||
import forceGC from './core/forceGC';
|
||||
import logger from './core/logger';
|
||||
import rankings from './core/Ranks';
|
||||
import sequelize from './data/sql/sequelize';
|
||||
import { sync as syncSql } from './data/sql/sequelize';
|
||||
import { connect as connectRedis } from './data/redis/client';
|
||||
import routes from './routes';
|
||||
import chatProvider from './core/ChatProvider';
|
||||
|
@ -84,7 +84,7 @@ app.use(routes);
|
|||
// ip config
|
||||
// -----------------------------------------------------------------------------
|
||||
// sync sql models
|
||||
sequelize.sync({ alter: { drop: false } })
|
||||
syncSql()
|
||||
// connect to redis
|
||||
.then(connectRedis)
|
||||
.then(async () => {
|
||||
|
@ -122,7 +122,7 @@ sequelize.sync({ alter: { drop: false } })
|
|||
* initializers that rely on the cluster being fully established
|
||||
* i.e. to know if it is the shard that runs the event
|
||||
*/
|
||||
if (socketEvents.isCluster && socketEvents.amIImportant()) {
|
||||
if (socketEvents.isCluster && socketEvents.important) {
|
||||
logger.info('I am the main shard');
|
||||
}
|
||||
rankings.initialize();
|
||||
|
|
|
@ -41,10 +41,11 @@ const LISTEN_PREFIX = 'l';
|
|||
|
||||
|
||||
class MessageBroker extends SocketEvents {
|
||||
isCluster = true;
|
||||
thisShard = SHARD_NAME;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.isCluster = true;
|
||||
this.thisShard = SHARD_NAME;
|
||||
/*
|
||||
* currently running cross-shard requests,
|
||||
* are tracked in order to only send them to receiving
|
||||
|
@ -75,6 +76,28 @@ class MessageBroker extends SocketEvents {
|
|||
setInterval(this.checkHealth, 10000);
|
||||
}
|
||||
|
||||
get important() {
|
||||
/*
|
||||
* important main shard does tasks like running RpgEvent
|
||||
* or updating rankings
|
||||
*/
|
||||
return !this.shardOnlineCounters[0]
|
||||
|| this.shardOnlineCounters[0][0] === this.thisShard;
|
||||
}
|
||||
|
||||
get lowestActiveShard() {
|
||||
let lowest = 0;
|
||||
let lShard = null;
|
||||
this.shardOnlineCounters.forEach((shardData) => {
|
||||
const [shard, cnt] = shardData;
|
||||
if (cnt.total < lowest || !lShard) {
|
||||
lShard = shard;
|
||||
lowest = cnt.total;
|
||||
}
|
||||
});
|
||||
return lShard || this.thisShard;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.publisher = pubsub.publisher;
|
||||
this.subscriber = pubsub.subscriber;
|
||||
|
@ -96,7 +119,7 @@ class MessageBroker extends SocketEvents {
|
|||
console.log('CLUSTER: Initialized message broker');
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* messages on shared broadcast channels that every shard is listening to
|
||||
*/
|
||||
async onShardBCMessage(message) {
|
||||
|
@ -129,7 +152,7 @@ class MessageBroker extends SocketEvents {
|
|||
super.emit(key, ...val);
|
||||
return;
|
||||
}
|
||||
/*
|
||||
/**
|
||||
* other messages are shard names that announce the existence
|
||||
* of a shard
|
||||
*/
|
||||
|
@ -151,7 +174,7 @@ class MessageBroker extends SocketEvents {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* messages on shard specific listener channel
|
||||
* messages in form `type,JSONArrayData`
|
||||
* straight emitted as socket event
|
||||
|
@ -168,29 +191,7 @@ class MessageBroker extends SocketEvents {
|
|||
}
|
||||
}
|
||||
|
||||
getLowestActiveShard() {
|
||||
let lowest = 0;
|
||||
let lShard = null;
|
||||
this.shardOnlineCounters.forEach((shardData) => {
|
||||
const [shard, cnt] = shardData;
|
||||
if (cnt.total < lowest || !lShard) {
|
||||
lShard = shard;
|
||||
lowest = cnt.total;
|
||||
}
|
||||
});
|
||||
return lShard || this.thisShard;
|
||||
}
|
||||
|
||||
amIImportant() {
|
||||
/*
|
||||
* important main shard does tasks like running RpgEvent
|
||||
* or updating rankings
|
||||
*/
|
||||
return !this.shardOnlineCounters[0]
|
||||
|| this.shardOnlineCounters[0][0] === this.thisShard;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* requests that go over all shards and combine responses from all
|
||||
*/
|
||||
req(type, ...args) {
|
||||
|
@ -256,7 +257,7 @@ class MessageBroker extends SocketEvents {
|
|||
this.sumOnlineCounters();
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* messages on binary shard channels, where specific shards send from
|
||||
*/
|
||||
onShardBinaryMessage(buffer, shard) {
|
||||
|
@ -314,7 +315,7 @@ class MessageBroker extends SocketEvents {
|
|||
this.publisher.publish(BROADCAST_CHAN, msg);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* broadcast pixel message via websocket
|
||||
* @param canvasId number ident of canvas
|
||||
* @param chunkid number id consisting of i,j chunk coordinates
|
||||
|
@ -337,7 +338,7 @@ class MessageBroker extends SocketEvents {
|
|||
}
|
||||
|
||||
setCoolDownFactor(fac) {
|
||||
if (this.amIImportant()) {
|
||||
if (this.important) {
|
||||
this.emit('setCoolDownFactor', fac);
|
||||
} else {
|
||||
super.emit('setCoolDownFactor', fac);
|
||||
|
|
|
@ -8,6 +8,11 @@ import {
|
|||
} from './packets/server';
|
||||
|
||||
class SocketEvents extends EventEmitter {
|
||||
isCluster = false;
|
||||
// object with amount of online users
|
||||
// in total and per canvas
|
||||
onlineCounter;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
/*
|
||||
|
@ -23,18 +28,18 @@ class SocketEvents extends EventEmitter {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async initialize() {
|
||||
// nothing, only for child classes
|
||||
get important() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getLowestActiveShard() {
|
||||
get lowestActiveShard() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
amIImportant() {
|
||||
return true;
|
||||
async initialize() {
|
||||
// nothing, only for child classes
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -48,7 +53,7 @@ class SocketEvents extends EventEmitter {
|
|||
});
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* requests that expect a response
|
||||
* req(type, args) can be awaited
|
||||
* it will return a response from whatever listens on onReq(type, cb(args))
|
||||
|
@ -84,7 +89,7 @@ class SocketEvents extends EventEmitter {
|
|||
});
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* broadcast pixel message via websocket
|
||||
* @param canvasId number ident of canvas
|
||||
* @param chunkid number id consisting of i,j chunk coordinates
|
||||
|
@ -102,7 +107,7 @@ class SocketEvents extends EventEmitter {
|
|||
this.emit('chunkUpdate', canvasId, [i, j]);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* chunk updates from event, image upload, etc.
|
||||
* everything that's not a pixelUpdate and changes chunks
|
||||
* @param canvasId
|
||||
|
@ -115,7 +120,26 @@ class SocketEvents extends EventEmitter {
|
|||
this.emit('chunkUpdate', canvasId, chunk);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* broadcast fetched IpInfo of user,
|
||||
* used to spread flag informations of users
|
||||
* to shards
|
||||
* @param userIpInfo object {
|
||||
* userId, // 0 if not logged in
|
||||
* status, // proxycheck and ban status (see core/isAllowed)
|
||||
* ip,
|
||||
* cidr,
|
||||
* org,
|
||||
* country,
|
||||
* asn,
|
||||
* descr,
|
||||
* }
|
||||
*/
|
||||
gotUserIpInfo(userIpInfo) {
|
||||
this.emit('useripinfo', userIpInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* ask other shards to send email for us,
|
||||
* only used when USE_MAILER is false
|
||||
* @param type type of mail to send
|
||||
|
@ -125,7 +149,7 @@ class SocketEvents extends EventEmitter {
|
|||
this.emit('mail', ...args);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* received Chat message on own websocket
|
||||
* @param user User Instance that sent the message
|
||||
* @param message text message
|
||||
|
@ -139,7 +163,7 @@ class SocketEvents extends EventEmitter {
|
|||
this.emit('recvChatMessage', user, message, channelId);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* set cooldownfactor
|
||||
* (used by RpgEvent)
|
||||
* @param fac factor by which cooldown changes globally
|
||||
|
@ -148,7 +172,7 @@ class SocketEvents extends EventEmitter {
|
|||
this.emit('setCoolDownFactor', fac);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* broadcast chat message to all users in channel
|
||||
* @param name chatname
|
||||
* @param message Message to send
|
||||
|
@ -174,7 +198,7 @@ class SocketEvents extends EventEmitter {
|
|||
);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* send chat message to a single user in channel
|
||||
*/
|
||||
broadcastSUChatMessage(
|
||||
|
@ -196,7 +220,7 @@ class SocketEvents extends EventEmitter {
|
|||
);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* broadcast Assigning chat channel to user
|
||||
* @param userId numerical id of user
|
||||
* @param channelId numerical id of chat channel
|
||||
|
@ -215,7 +239,7 @@ class SocketEvents extends EventEmitter {
|
|||
);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* broadcast Removing chat channel from user
|
||||
* @param userId numerical id of user
|
||||
* @param channelId numerical id of chat channel
|
||||
|
@ -228,7 +252,7 @@ class SocketEvents extends EventEmitter {
|
|||
this.emit('remChatChannel', userId, channelId);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* trigger rate limit of ip
|
||||
* @param ip
|
||||
* @param blockTime in ms
|
||||
|
@ -237,7 +261,7 @@ class SocketEvents extends EventEmitter {
|
|||
this.emit('rateLimitTrigger', ip, blockTime);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* broadcast ranking list updates
|
||||
* @param {
|
||||
* dailyRanking?: daily pixel raking top 100,
|
||||
|
@ -249,14 +273,14 @@ class SocketEvents extends EventEmitter {
|
|||
this.emit('rankingListUpdate', rankings);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* reload user on websocket to get changes
|
||||
*/
|
||||
reloadUser(name) {
|
||||
this.emit('reloadUser', name);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* broadcast online counter
|
||||
* @param online Object of total and canvas online users
|
||||
* (see this.onlineCounter)
|
||||
|
|
|
@ -33,8 +33,8 @@ import socketEvents from './socketEvents';
|
|||
import chatProvider, { ChatProvider } from '../core/ChatProvider';
|
||||
import authenticateClient from './authenticateClient';
|
||||
import drawByOffsets from '../core/draw';
|
||||
import isIPAllowed from '../core/isAllowed';
|
||||
import { HOUR } from '../core/constants';
|
||||
import isIPAllowed from '../core/ipUserIntel';
|
||||
import { checkCaptchaSolution } from '../data/redis/captcha';
|
||||
|
||||
|
||||
|
@ -106,6 +106,10 @@ class SocketServer {
|
|||
});
|
||||
});
|
||||
|
||||
socketEvents.on('useripinfo', (userIpInfo) => {
|
||||
|
||||
});
|
||||
|
||||
socketEvents.on('onlineCounter', (online) => {
|
||||
const onlineBuffer = dehydrateOnlineCounter(online);
|
||||
this.broadcast(onlineBuffer);
|
||||
|
@ -188,7 +192,7 @@ class SocketServer {
|
|||
async handleUpgrade(request, socket, head) {
|
||||
const { headers } = request;
|
||||
const ip = getIPFromRequest(request);
|
||||
// trigger proxycheck
|
||||
// trigger proxycheck TODO DO WE REMOVE THIS????
|
||||
isIPAllowed(ip);
|
||||
/*
|
||||
* rate limit
|
||||
|
@ -235,6 +239,7 @@ class SocketServer {
|
|||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
user.isAllowed();
|
||||
request.user = user;
|
||||
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
this.wss.emit('connection', ws, request);
|
||||
|
@ -329,8 +334,9 @@ class SocketServer {
|
|||
|
||||
killAllWsByUerIp(ip) {
|
||||
let amount = ipCounter.get(ip);
|
||||
if (!amount) return 0;
|
||||
|
||||
if (!amount) {
|
||||
return 0;
|
||||
}
|
||||
for (const [chunkid, clients] of this.CHUNK_CLIENTS.entries()) {
|
||||
const newClients = clients.filter((ws) => ws.user.ip !== ip);
|
||||
if (clients.length !== newClients.length) {
|
||||
|
@ -630,18 +636,23 @@ class SocketServer {
|
|||
if (!clients) {
|
||||
clients = [];
|
||||
this.CHUNK_CLIENTS.set(chunkid, clients);
|
||||
} else if (clients.includes(ws)) {
|
||||
return;
|
||||
}
|
||||
const pos = clients.indexOf(ws);
|
||||
if (~pos) return;
|
||||
clients.push(ws);
|
||||
}
|
||||
|
||||
deleteChunk(chunkid, ws) {
|
||||
ws.chunkCnt -= 1;
|
||||
if (!this.CHUNK_CLIENTS.has(chunkid)) return;
|
||||
const clients = this.CHUNK_CLIENTS.get(chunkid);
|
||||
if (!clients) {
|
||||
return;
|
||||
}
|
||||
const pos = clients.indexOf(ws);
|
||||
if (~pos) clients.splice(pos, 1);
|
||||
if (pos === -1) {
|
||||
return;
|
||||
}
|
||||
ws.chunkCnt -= 1;
|
||||
clients.splice(pos, 1);
|
||||
}
|
||||
|
||||
deleteAllChunks(ws) {
|
||||
|
@ -650,10 +661,12 @@ class SocketServer {
|
|||
}
|
||||
for (const client of this.CHUNK_CLIENTS.values()) {
|
||||
const pos = client.indexOf(ws);
|
||||
if (~pos) {
|
||||
if (pos !== -1) {
|
||||
client.splice(pos, 1);
|
||||
ws.chunkCnt -= 1;
|
||||
if (!ws.chunkCnt) return;
|
||||
if (!ws.chunkCnt) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import session from '../core/session';
|
|||
import passport from '../core/passport';
|
||||
import User from '../data/User';
|
||||
import { expressTTag } from '../core/ttag';
|
||||
import { getIPFromRequest } from '../utils/ip';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
@ -24,16 +23,12 @@ function authenticateClient(req) {
|
|||
return new Promise(
|
||||
((resolve) => {
|
||||
router(req, {}, async () => {
|
||||
const country = req.headers['cf-ipcountry'] || 'xx';
|
||||
const countryCode = country.toLowerCase();
|
||||
let user;
|
||||
if (req.user) {
|
||||
user = req.user;
|
||||
} else {
|
||||
user = new User();
|
||||
await user.initialize(null, getIPFromRequest(req));
|
||||
user = new User(req);
|
||||
}
|
||||
user.setCountry(countryCode);
|
||||
user.ttag = req.ttag;
|
||||
user.lang = req.lang;
|
||||
resolve(user);
|
||||
|
|
|
@ -45,7 +45,7 @@ function generateMainPage(req) {
|
|||
const ssvR = {
|
||||
...ssv,
|
||||
shard: (host.startsWith(`${socketEvents.thisShard}.`))
|
||||
? null : socketEvents.getLowestActiveShard(),
|
||||
? null : socketEvents.lowestActiveShard,
|
||||
lang: lang === 'default' ? 'en' : lang,
|
||||
};
|
||||
const scripts = (assets[`client-${lang}`])
|
||||
|
|
|
@ -41,7 +41,7 @@ function generatePopUpPage(req) {
|
|||
const ssvR = {
|
||||
...ssv,
|
||||
shard: (host.startsWith(`${socketEvents.thisShard}.`))
|
||||
? null : socketEvents.getLowestActiveShard(),
|
||||
? null : socketEvents.lowestActiveShard,
|
||||
lang: lang === 'default' ? 'en' : lang,
|
||||
};
|
||||
const script = (assets[`popup-${lang}`])
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { USERLVL } from '../../core/constants';
|
||||
|
||||
const initialState = {
|
||||
id: null,
|
||||
name: null,
|
||||
wait: null,
|
||||
coolDown: null, // ms
|
||||
lastCoolDownEnd: null,
|
||||
userlvl: USERLVL.ANONYM,
|
||||
// messages are sent by api/me, like not_verified status
|
||||
messages: [],
|
||||
mailreg: false,
|
||||
|
@ -15,8 +18,6 @@ const initialState = {
|
|||
isOnMobile: false,
|
||||
// small notifications for received cooldown
|
||||
notification: null,
|
||||
// 1: Admin, 2: Mod, 0: ordinary user
|
||||
userlvl: 0,
|
||||
};
|
||||
|
||||
export default function user(
|
||||
|
@ -103,7 +104,7 @@ export default function user(
|
|||
mailreg: false,
|
||||
blockDm: false,
|
||||
priv: false,
|
||||
userlvl: 0,
|
||||
userlvl: USERLVL.ANONYM,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { TILE_ZOOM_LEVEL, TILE_SIZE } from '../core/constants';
|
||||
import {
|
||||
TILE_ZOOM_LEVEL,
|
||||
TILE_SIZE,
|
||||
BACKGROUND_CLR_HEX,
|
||||
} from '../core/constants';
|
||||
|
||||
import {
|
||||
getTileOfPixel,
|
||||
|
@ -22,6 +26,8 @@ import Renderer from './Renderer';
|
|||
import ChunkLoader from './ChunkLoader2D';
|
||||
import pixelNotify from './PixelNotify';
|
||||
|
||||
const LOG_FRAC = Math.log2(TILE_ZOOM_LEVEL);
|
||||
|
||||
class Renderer2D extends Renderer {
|
||||
is3D = false;
|
||||
//
|
||||
|
@ -178,13 +184,12 @@ class Renderer2D extends Renderer {
|
|||
pixelNotify.updateScale(viewscale);
|
||||
let tiledScale = (viewscale > 0.5)
|
||||
? 0
|
||||
: Math.round(Math.log2(viewscale) * 2 / TILE_ZOOM_LEVEL);
|
||||
: Math.round(Math.log2(viewscale) / LOG_FRAC);
|
||||
tiledScale = TILE_ZOOM_LEVEL ** tiledScale;
|
||||
const canvasMaxTiledZoom = (isHistoricalView)
|
||||
? this.historicalCanvasMaxTiledZoom
|
||||
: this.canvasMaxTiledZoom;
|
||||
const tiledZoom = canvasMaxTiledZoom + Math.log2(tiledScale)
|
||||
* 2 / TILE_ZOOM_LEVEL;
|
||||
const tiledZoom = canvasMaxTiledZoom + Math.log2(tiledScale) / LOG_FRAC;
|
||||
const relScale = viewscale / tiledScale;
|
||||
|
||||
this.tiledScale = tiledScale;
|
||||
|
@ -345,7 +350,7 @@ class Renderer2D extends Renderer {
|
|||
if (scale > this.scaleThreshold) relScale = 1.0;
|
||||
// scale
|
||||
context.save();
|
||||
context.fillStyle = '#C4C4C4';
|
||||
context.fillStyle = BACKGROUND_CLR_HEX;
|
||||
context.scale(relScale, relScale);
|
||||
// decide if we update the timestamps of accessed chunks
|
||||
const curTime = Date.now();
|
||||
|
@ -576,7 +581,7 @@ class Renderer2D extends Renderer {
|
|||
const CHUNK_RENDER_RADIUS_Y = Math.ceil(height / TILE_SIZE / 2 / scale);
|
||||
|
||||
context.save();
|
||||
context.fillStyle = '#C4C4C4';
|
||||
context.fillStyle = BACKGROUND_CLR_HEX;
|
||||
// clear canvas and do nothing if no time selected
|
||||
if (!historicalDate || !historicalTime) {
|
||||
context.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '../core/utils';
|
||||
import {
|
||||
THREE_TILE_SIZE,
|
||||
BACKGROUND_CLR_HEX,
|
||||
} from '../core/constants';
|
||||
import {
|
||||
setHover,
|
||||
|
@ -145,7 +146,7 @@ class Renderer3D extends Renderer {
|
|||
oobGeometry.rotateX(-Math.PI / 2);
|
||||
oobGeometry.translate(THREE_TILE_SIZE / 2, 0.2, THREE_TILE_SIZE / 2);
|
||||
const oobMaterial = new THREE.MeshLambertMaterial({
|
||||
color: '#C4C4C4',
|
||||
color: BACKGROUND_CLR_HEX,
|
||||
});
|
||||
this.oobGeometry = oobGeometry;
|
||||
this.oobMaterial = oobMaterial;
|
||||
|
|
|
@ -129,14 +129,12 @@ class PcKeyProvider {
|
|||
}
|
||||
const queriesToday = Number(usage['Queries Today']) || 0;
|
||||
const availableBurst = Number(usage['Burst Tokens Available']) || 0;
|
||||
const burstActive = Number(usage['Burst Token Active']) === 1;
|
||||
let dailyLimit = Number(usage['Daily Limit']) || 0;
|
||||
let burstActive = false;
|
||||
let availableQueries = dailyLimit - queriesToday;
|
||||
if (availableQueries < 0) {
|
||||
burstActive = true;
|
||||
if (burstActive) {
|
||||
dailyLimit *= 5;
|
||||
availableQueries = dailyLimit - queriesToday;
|
||||
}
|
||||
const availableQueries = dailyLimit - queriesToday;
|
||||
// eslint-disable-next-line max-len
|
||||
this.logger.info(`PCKey: ${key}, Queries Today: ${availableQueries} / ${dailyLimit} (Burst: ${availableBurst}, ${burstActive ? 'active' : 'inactive'})`);
|
||||
const keyData = [
|
||||
|
@ -146,7 +144,7 @@ class PcKeyProvider {
|
|||
availableBurst,
|
||||
false,
|
||||
];
|
||||
if (burstActive || availableQueries > HYSTERESIS) {
|
||||
if (availableQueries > HYSTERESIS) {
|
||||
/*
|
||||
* data is a few minutes old, stop at HYSTERESIS
|
||||
*/
|
||||
|
@ -157,7 +155,6 @@ class PcKeyProvider {
|
|||
}
|
||||
|
||||
/*
|
||||
* TODO: proxycheck added the used burst token to API
|
||||
* query the API for limits
|
||||
* @param key
|
||||
*/
|
||||
|
@ -411,8 +408,6 @@ class ProxyCheck {
|
|||
|
||||
/*
|
||||
* same as for ip
|
||||
* TODO: cache for mail providers, remember
|
||||
* a disposable provider for an hour or so
|
||||
* @param email
|
||||
* @return Promise that resolves to
|
||||
* null: failure
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* set CORS Headers
|
||||
* express middleware to set CORS Headers
|
||||
*/
|
||||
import { CORS_HOSTS } from '../core/config';
|
||||
|
||||
|
@ -31,6 +31,7 @@ export default (req, res, next) => {
|
|||
res.set({
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Access-Control-Allow-Methods': 'GET,POST',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
});
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
|
|
351
src/utils/ip.js
351
src/utils/ip.js
|
@ -1,49 +1,9 @@
|
|||
/**
|
||||
*
|
||||
* basic functions to get data from headers and parse IPs
|
||||
* basic functions for parsing IPs
|
||||
*/
|
||||
|
||||
import { USE_XREALIP } from '../core/config';
|
||||
|
||||
/*
|
||||
* Parse ip4 string to 32bit integer
|
||||
* @param ipString ip string
|
||||
* @return ipNum numerical ip
|
||||
*/
|
||||
function ip4ToNum(ipString) {
|
||||
if (!ipString) {
|
||||
return null;
|
||||
}
|
||||
const ipArr = ipString
|
||||
.trim()
|
||||
.split('.')
|
||||
.map((numString) => parseInt(numString, 10));
|
||||
if (ipArr.length !== 4 || ipArr.some(
|
||||
(num) => Number.isNaN(num) || num > 255 || num < 0,
|
||||
)) {
|
||||
return null;
|
||||
}
|
||||
return (ipArr[0] << 24)
|
||||
+ (ipArr[1] << 16)
|
||||
+ (ipArr[2] << 8)
|
||||
+ ipArr[3];
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse ip4 number to string representation
|
||||
* @param ipNum numerical ip (32bit integer)
|
||||
* @return ipString string representation of ip (xxx.xxx.xxx.xxx)
|
||||
*/
|
||||
function ip4NumToStr(ipNum) {
|
||||
return [
|
||||
ipNum >>> 24,
|
||||
ipNum >>> 16 & 0xFF,
|
||||
ipNum >>> 8 & 0xFF,
|
||||
ipNum & 0xFF,
|
||||
].join('.');
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Get hostname from request
|
||||
* @param req express req object
|
||||
* @param includeProto if we include protocol (https, http)
|
||||
|
@ -64,37 +24,11 @@ export function getHostFromRequest(req, includeProto = true, stripSub = false) {
|
|||
if (!includeProto) {
|
||||
return host;
|
||||
}
|
||||
|
||||
const proto = headers['x-forwarded-proto'] || 'http';
|
||||
return `${proto}://${host}`;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get IP from request
|
||||
* @param req express req object
|
||||
* @return ip as string
|
||||
*/
|
||||
export function getIPFromRequest(req) {
|
||||
if (USE_XREALIP) {
|
||||
const ip = req.headers['x-real-ip'];
|
||||
if (ip) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
const { socket, connection } = req;
|
||||
let conip = (connection ? connection.remoteAddress : socket.remoteAddress);
|
||||
conip = conip || '0.0.0.1';
|
||||
if (!USE_XREALIP) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Connection not going through reverse proxy! IP: ${conip}`, req.headers,
|
||||
);
|
||||
}
|
||||
return conip;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Check if IP is v6 or v4
|
||||
* @param ip ip as string
|
||||
* @return true if ipv6, false otherwise
|
||||
|
@ -103,46 +37,129 @@ export function isIPv6(ip) {
|
|||
return ip.includes(':');
|
||||
}
|
||||
|
||||
/*
|
||||
* Set last digits of IPv6 to zero,
|
||||
* needed because IPv6 assigns subnets to customers, we don't want to
|
||||
* mess with individual ips
|
||||
* @param ip ip as string (v4 or v6)
|
||||
* @return ip as string, and if v6, the last digits set to 0
|
||||
/**
|
||||
* unpack IPv6 address into 8 blocks
|
||||
* @param ip IPv6 IP string
|
||||
* @return Array with length 8 of IPv6 parts as strings
|
||||
*/
|
||||
export function getIPv6Subnet(ip) {
|
||||
export function unpackIPv6(ip) {
|
||||
let ipUnpack = ip.split(':');
|
||||
const spacer = ipUnpack.indexOf('');
|
||||
if (~spacer) {
|
||||
ipUnpack = ipUnpack.filter((a) => a);
|
||||
ipUnpack.splice(spacer, 0, ...Array(8 - ipUnpack.length).fill('0'));
|
||||
}
|
||||
return ipUnpack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hex representation of IP
|
||||
* @param ip ip as string (if IPv6, the first 64bit have to be unpacked)
|
||||
* @return hex string (without leading '0x')
|
||||
*/
|
||||
export function ipToHex(ip) {
|
||||
if (isIPv6(ip)) {
|
||||
// eslint-disable-next-line max-len
|
||||
return `${ip.split(':')
|
||||
return ip.split(':')
|
||||
.slice(0, 4)
|
||||
.join(':')}:0000:0000:0000:0000`;
|
||||
.map((n) => `000${n}`.slice(-4).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
return ip.split('.')
|
||||
.map((n) => `0${parseInt(n, 10).toString(16)}`.slice(-2))
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse IPv4 string to Number and IPv6 string to 2xNumbers (first 64bit)
|
||||
* @param ipString ip string
|
||||
* @return numerical IP (Number or BigInt)
|
||||
*/
|
||||
function ipToNum(ipString) {
|
||||
if (!ipString) {
|
||||
return null;
|
||||
}
|
||||
if (isIPv6(ipString)) {
|
||||
// IPv6
|
||||
const hex = unpackIPv6(ipString.trim())
|
||||
.map((n) => `000${n}`.slice(-4))
|
||||
.slice(0, 4).join('');
|
||||
try {
|
||||
return BigInt(`0x${hex}`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// IPv4
|
||||
const ipArr = ipString
|
||||
.split('.')
|
||||
.map((numString) => parseInt(numString, 10));
|
||||
if (ipArr.length !== 4 || ipArr.some((num) => Number.isNaN(num))) {
|
||||
return null;
|
||||
}
|
||||
// >>>0 is needed to convert from signed to unsigned
|
||||
return ((ipArr[0] << 24)
|
||||
+ (ipArr[1] << 16)
|
||||
+ (ipArr[2] << 8)
|
||||
+ ipArr[3])>>>0;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse num representation of IP to hex
|
||||
* @param num IP as 64bit BigInt if IPv6, Number if IPv4
|
||||
* @return hex
|
||||
*/
|
||||
function numToHex(num) {
|
||||
let ip = `00000000${num.toString(16)}`;
|
||||
return ip.slice(
|
||||
(typeof num === 'bigint') ? -16 : -8,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* parse range into readable string
|
||||
* @param range [start, end, mask] with start and end in hex
|
||||
* @return string
|
||||
*/
|
||||
export function rangeToString(range) {
|
||||
if (!range) {
|
||||
return undefined;
|
||||
}
|
||||
return `${hexToIp(range[0])}/${range[2]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse hex representation of IP to string
|
||||
* @param hex IP as 64bit hex for IPv6 and 32bit hex for IPv4
|
||||
* @return ip as string
|
||||
*/
|
||||
export function hexToIp(hex) {
|
||||
let ip = '';
|
||||
if (hex.length === 8) {
|
||||
// IPv4
|
||||
let i = 0;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
ip += parseInt(hex.slice(i, i += 2), 16).toString(10);
|
||||
if (i >= 8) break;
|
||||
ip += '.';
|
||||
}
|
||||
} else {
|
||||
// IPv6
|
||||
for (let i = 0; i < 16; i += 4) {
|
||||
ip += hex.slice(i, i + 4);
|
||||
ip += ':';
|
||||
}
|
||||
ip += ':';
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get numerical start and end of range
|
||||
* @param range string of range in the format 'xxx.xxx.xxx.xxx - xxx.xxx.xxx.xxx'
|
||||
* @return [start, end] with numerical IPs (32bit integer)
|
||||
*/
|
||||
function ip4RangeStrToRangeNum(range) {
|
||||
if (!range) {
|
||||
return null;
|
||||
}
|
||||
const [start, end] = range.split('-')
|
||||
.map(ip4ToNum);
|
||||
if (!start || !end || start > end) {
|
||||
return null;
|
||||
}
|
||||
return [start, end];
|
||||
}
|
||||
|
||||
/*
|
||||
* Get Array of CIDRs for an numerical IPv4 range
|
||||
* @param [start, end] with numerical IPs (32bit integer)
|
||||
/**
|
||||
* Get Array of CIDRs for an 32bit numerical IP range
|
||||
* @param [start, end] with numerical IPs as Number
|
||||
* @return Array of CIDR strings
|
||||
*/
|
||||
function ip4RangeNumToCIDR([start, end]) {
|
||||
function ip32RangeNumToCIDR(start, end, ip) {
|
||||
let maskNum = 32;
|
||||
let mask = 0xFFFFFFFF;
|
||||
const diff = start ^ end;
|
||||
|
@ -151,62 +168,114 @@ function ip4RangeNumToCIDR([start, end]) {
|
|||
maskNum -= 1;
|
||||
}
|
||||
if ((start & (~mask)) || (~(end | mask))) {
|
||||
const divider = start | (~mask >> 1);
|
||||
return ip4RangeNumToCIDR([start, divider]).concat(
|
||||
ip4RangeNumToCIDR([divider + 1, end]),
|
||||
const divider = (start | (~mask >> 1))>>>0;
|
||||
if (ip) {
|
||||
if (ip <= divider) {
|
||||
return ip32RangeNumToCIDR(start, divider, ip);
|
||||
}
|
||||
return ip32RangeNumToCIDR(divider + 1, end, ip);
|
||||
}
|
||||
return ip32RangeNumToCIDR(start, divider).concat(
|
||||
ip32RangeNumToCIDR(divider + 1, end),
|
||||
);
|
||||
}
|
||||
return [`${ip4NumToStr(start)}/${maskNum}`];
|
||||
return [[start, end, maskNum]];
|
||||
}
|
||||
|
||||
/*
|
||||
* Get Array of CIDRs for an IPv4 range
|
||||
* @param range string of range in the format 'xxx.xxx.xxx.xxx - xxx.xxx.xxx.xxx'
|
||||
/**
|
||||
* Get Array of CIDRs for an 64bit numerical IP range
|
||||
* @param [start, end] with numerical IPs as BigInt
|
||||
* @param ip if given, return only range that includes ip
|
||||
* @return Array of CIDR strings
|
||||
*/
|
||||
export function ip4RangeToCIDR(range) {
|
||||
const rangeNum = ip4RangeStrToRangeNum(range);
|
||||
if (!rangeNum) {
|
||||
return null;
|
||||
}
|
||||
return ip4RangeNumToCIDR(rangeNum);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get specific CIDR in numeric range that includes numeric ip
|
||||
* @param ip numerical ip (32bit integer)
|
||||
* @param [start, end] with numerical IPs (32bit integer)
|
||||
* @return CIDR string
|
||||
*/
|
||||
function ip4NumInRangeNumToCIDR(ip, [start, end]) {
|
||||
let maskNum = 32;
|
||||
let mask = 0xFFFFFFFF;
|
||||
function ip64RangeNumToCIDR(start, end, ip) {
|
||||
let maskNum = 64;
|
||||
const mask64 = 0xFFFFFFFFFFFFFFFFn;
|
||||
let mask = mask64;
|
||||
const diff = start ^ end;
|
||||
while (diff & mask) {
|
||||
mask <<= 1;
|
||||
mask = (mask << 1n) & mask64;
|
||||
maskNum -= 1;
|
||||
}
|
||||
if ((start & (~mask)) || (~(end | mask))) {
|
||||
const divider = start | (~mask >> 1);
|
||||
if (ip <= divider) {
|
||||
return ip4NumInRangeNumToCIDR(ip, [start, divider]);
|
||||
const invMask = ~mask & mask64;
|
||||
if ((start & invMask) || (~(end | mask) & mask64)) {
|
||||
const divider = start | (invMask >> 1n);
|
||||
if (ip) {
|
||||
if (ip <= divider) {
|
||||
return ip64RangeNumToCIDR(start, divider, ip);
|
||||
}
|
||||
return ip64RangeNumToCIDR(divider + 1n, end, ip);
|
||||
}
|
||||
return ip4NumInRangeNumToCIDR(ip, [divider + 1, end]);
|
||||
return ip64RangeNumToCIDR(start, divider).concat(
|
||||
ip64RangeNumToCIDR(divider + 1n, end),
|
||||
);
|
||||
}
|
||||
return `${ip4NumToStr(start)}/${maskNum}`;
|
||||
return [[start, end, maskNum]];
|
||||
}
|
||||
|
||||
/*
|
||||
* Get specific CIDR in range that includes ip
|
||||
* @param ip ip string ('xxx.xxx.xxx.xxx')
|
||||
* @param range string ('xxx.xxx.xxx.xxx - xxx.xxx.xxx.xxx')
|
||||
* @return CIDR string
|
||||
/**
|
||||
* Parse subnet given as string into array numerical representations
|
||||
* @param subnet given as CIDR or range
|
||||
* @param ip if given, return only range that includes ip
|
||||
* @return [start, end, mask] start and end as hex and mask part of CIDR
|
||||
* Array of same if ip isn't given and there could be multiple
|
||||
*/
|
||||
export function ip4InRangeToCIDR(ip, range) {
|
||||
const rangeNum = ip4RangeStrToRangeNum(range);
|
||||
const ipNum = ip4ToNum(ip);
|
||||
if (!ipNum || !rangeNum || rangeNum[0] > ip || rangeNum[1] < ip) {
|
||||
export function ipSubnetToHex(subnet, ip) {
|
||||
const ipNum = ip && ipToNum(ip);
|
||||
if (!subnet || (ip && !ipNum)) {
|
||||
return null;
|
||||
}
|
||||
return ip4NumInRangeNumToCIDR(ipNum, rangeNum);
|
||||
let ranges;
|
||||
if (subnet.includes('-')) {
|
||||
// given as range
|
||||
const [start, end] = subnet.split('-').map(ipToNum);
|
||||
if (!start
|
||||
|| typeof start !== typeof end
|
||||
|| start > end
|
||||
|| (ipNum && typeof ipNum !== typeof start)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const numRanges = (typeof start === 'bigint')
|
||||
? ip64RangeNumToCIDR(start, end, ipNum)
|
||||
: ip32RangeNumToCIDR(start, end, ipNum);
|
||||
ranges = numRanges;
|
||||
} else {
|
||||
// given as CIDR
|
||||
let [start, mask] = subnet.split('/');
|
||||
start = ipToNum(start);
|
||||
mask = parseInt(mask, 10);
|
||||
if (!start || !mask) {
|
||||
return null;
|
||||
}
|
||||
let end;
|
||||
if (typeof start === 'bigint') {
|
||||
// IPv6
|
||||
if (mask >= 64) {
|
||||
end = start;
|
||||
} else {
|
||||
const bitmask = (0xFFFFFFFFFFFFFFFFn >> BigInt(mask));
|
||||
start = start & (~bitmask & 0xFFFFFFFFFFFFFFFFn);
|
||||
end = start | bitmask;
|
||||
}
|
||||
} else {
|
||||
// IPv4
|
||||
if (mask === 32) {
|
||||
end = start;
|
||||
} else {
|
||||
const bitmask = (0xFFFFFFFF >>> mask);
|
||||
start = (start & ~bitmask)>>>0;
|
||||
end = (start | bitmask)>>>0;
|
||||
}
|
||||
}
|
||||
ranges = [[start, end, mask]];
|
||||
}
|
||||
if (ipNum && (ipNum < ranges[0][0] || ipNum > ranges[0][1])) {
|
||||
return null;
|
||||
}
|
||||
ranges = ranges.map(([s, e, m]) => [numToHex(s), numToHex(e), m]);
|
||||
if (ip) {
|
||||
ranges = ranges[0];
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
|
63
src/utils/ipMiddleware.js
Normal file
63
src/utils/ipMiddleware.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* express middleware to add IP related getters to Request object
|
||||
* req.ip -> ip as string, IPv6 cut to 64bit block
|
||||
* req.ipHex -> ip as hex string
|
||||
* req.ipNum -> ip as BigInt if IPv6 and Number if IPv4
|
||||
* req.cc -> two character country code based on cloudflare header
|
||||
*/
|
||||
import { USE_XREALIP } from '../core/config';
|
||||
import { isIPv6, unpackIPv6, ipToHex } from './ip';
|
||||
|
||||
const ipGetter = {
|
||||
get() {
|
||||
let ip;
|
||||
if (USE_XREALIP) {
|
||||
ip = this.headers['x-real-ip'];
|
||||
}
|
||||
if (!ip) {
|
||||
ip = this.connection.remoteAddress;
|
||||
if (USE_XREALIP) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Connection not going through reverse proxy! IP: ${ip}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isIPv6(ip)) {
|
||||
ip = `${unpackIPv6(ip).slice(0, 4).join(':')}::`;
|
||||
}
|
||||
this.ip = ip;
|
||||
return ip;
|
||||
},
|
||||
};
|
||||
|
||||
const ipHexGetter = {
|
||||
get() {
|
||||
return ipToHex(this.ip);
|
||||
},
|
||||
};
|
||||
|
||||
const ipNumGetter = {
|
||||
get() {
|
||||
const ipHex = `0x${this.ipHex}`;
|
||||
return (ipHex.lengh > 10) ? BigInt(ipHex) : Number(ipHex);
|
||||
},
|
||||
};
|
||||
|
||||
const ccGetter = {
|
||||
get() {
|
||||
if (!USE_XREALIP) {
|
||||
return 'xx';
|
||||
}
|
||||
const cc = this.headers['cf-ipcountry'];
|
||||
return (cc) ? cc.toLowerCase() : 'xx';
|
||||
}
|
||||
}
|
||||
|
||||
export default (req, res, next) => {
|
||||
Object.defineProperty(req, 'ip', ipGetter);
|
||||
Object.defineProperty(req, 'ipHex', ipHexGetter);
|
||||
Object.defineProperty(req, 'ipNum', ipNumGetter);
|
||||
Object.defineProperty(req, 'cc', ccGetter);
|
||||
next();
|
||||
};
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
import net from 'net';
|
||||
|
||||
import { isIPv6, ip4InRangeToCIDR } from './ip';
|
||||
import { isIPv6, ipSubnetToHex } from './ip';
|
||||
import { OUTGOING_ADDRESS } from '../core/config';
|
||||
|
||||
const WHOIS_PORT = 43;
|
||||
const QUERY_SUFFIX = '\r\n';
|
||||
const WHOIS_TIMEOUT = 30000;
|
||||
const WHOIS_TIMEOUT = 10000;
|
||||
|
||||
/*
|
||||
* parse whois return into fields
|
||||
|
@ -18,14 +18,13 @@ function parseSimpleWhois(whois) {
|
|||
let data = {
|
||||
groups: {},
|
||||
};
|
||||
|
||||
const groups = [{}];
|
||||
const text = [];
|
||||
const lines = whois.split('\n');
|
||||
let lastLabel;
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i].trim();
|
||||
let line = lines[i].trim();
|
||||
if (line.startsWith('%') || line.startsWith('#')) {
|
||||
/*
|
||||
* detect if an ASN or IP has multiple WHOIS results,
|
||||
|
@ -39,6 +38,11 @@ function parseSimpleWhois(whois) {
|
|||
continue;
|
||||
}
|
||||
if (line) {
|
||||
// strip network prefix for rwhois
|
||||
if (line.startsWith('network:')) {
|
||||
line = line.slice(8);
|
||||
}
|
||||
|
||||
const sep = line.indexOf(':');
|
||||
if (~sep) {
|
||||
const label = line.slice(0, sep).toLowerCase();
|
||||
|
@ -86,25 +90,17 @@ function parseSimpleWhois(whois) {
|
|||
* @return object with whois data
|
||||
*/
|
||||
function parseWhois(ip, whoisReturn) {
|
||||
const data = {};
|
||||
if (!whoisReturn) {
|
||||
return data;
|
||||
}
|
||||
const whoisData = parseSimpleWhois(whoisReturn);
|
||||
|
||||
let cidr;
|
||||
if (isIPv6(ip)) {
|
||||
const range = whoisData.inet6num || whoisData.netrange || whoisData.inetnum
|
||||
|| whoisData.route || whoisData.cidr;
|
||||
cidr = range && !range.includes('-') && range;
|
||||
} else {
|
||||
const range = whoisData.inetnum || whoisData.netrange
|
||||
|| whoisData.route || whoisData.cidr;
|
||||
if (range) {
|
||||
if (range.includes('/') && !range.includes('-')) {
|
||||
cidr = range;
|
||||
} else {
|
||||
cidr = ip4InRangeToCIDR(ip, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let range = whoisData.inetnum || whoisData.inet6num || whoisData.cidr
|
||||
|| whoisData.netrange || whoisData.route || whoisData.route6
|
||||
|| whoisData['ip-network'] || whoisData['auth-area'];
|
||||
range = ipSubnetToHex(range, ip);
|
||||
if (range) data.range = range;
|
||||
let org = whoisData['org-name']
|
||||
|| whoisData.organization
|
||||
|| whoisData.orgname
|
||||
|
@ -117,52 +113,81 @@ function parseWhois(ip, whoisReturn) {
|
|||
if (contactGroup) {
|
||||
[org] = whoisData.groups[contactGroup].address.split('\n');
|
||||
} else {
|
||||
org = whoisData.owner || whoisData['mnt-by'] || 'N/A';
|
||||
org = whoisData.owner || whoisData['mnt-by'];
|
||||
}
|
||||
}
|
||||
const descr = whoisData.netname || whoisData.descr || 'N/A';
|
||||
const asn = whoisData.asn
|
||||
if (org) data.org = org;
|
||||
const descr = whoisData.netname || whoisData.descr;
|
||||
if (descr) data.descr = descr;
|
||||
let asn = whoisData.asn
|
||||
|| whoisData.origin
|
||||
|| whoisData.originas
|
||||
|| whoisData['aut-num'] || 'N/A';
|
||||
let country = whoisData.country
|
||||
|| (whoisData.organisation && whoisData.organisation.Country)
|
||||
|| 'xx';
|
||||
if (country.length > 2) {
|
||||
country = country.slice(0, 2);
|
||||
|| whoisData['aut-num'];
|
||||
if (asn) {
|
||||
// use only first ASN from possible list
|
||||
asn = asn.split(',')[0].split('\n')[0]
|
||||
// only number
|
||||
if (asn.startsWith('AS')) {
|
||||
asn = asn.slice(2);
|
||||
}
|
||||
const dotIndex = asn.indexOf('.');
|
||||
if (dotIndex === -1) {
|
||||
// asplain
|
||||
asn = parseInt(asn, 10);
|
||||
if (!Number.isNaN(asn)) {
|
||||
data.asn = asn;
|
||||
}
|
||||
} else {
|
||||
// asdot
|
||||
const p1 = parseInt(asn.slice(0, dotIndex));
|
||||
const p2 = parseInt(asn.slice(dotIndex + 1));
|
||||
if (!Number.isNaN(p1) && !Number.isNaN(p2)) {
|
||||
data.asn = (p1 << 16) | p2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ip,
|
||||
cidr: cidr || 'N/A',
|
||||
org,
|
||||
country,
|
||||
asn,
|
||||
descr,
|
||||
};
|
||||
let country = whoisData.country
|
||||
|| whoisData.organisation?.Country
|
||||
|| whoisData['country-code'];
|
||||
if (country) {
|
||||
data.country = country.slice(0, 2).toLowerCase();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
* send a raw whois query to server
|
||||
* @param query
|
||||
* @param host
|
||||
* @param hostInput host with or without port
|
||||
*/
|
||||
function singleWhoisQuery(
|
||||
query,
|
||||
host,
|
||||
hostInput,
|
||||
) {
|
||||
if (host.endsWith(':4321')) {
|
||||
throw new Error('no rwhois support');
|
||||
const options = {
|
||||
timeout: WHOIS_TIMEOUT,
|
||||
};
|
||||
|
||||
const pos = hostInput.indexOf(':');
|
||||
if (~pos) {
|
||||
// split port if neccessary
|
||||
options.host = hostInput.slice(0, pos);
|
||||
options.port = hostInput.slice(pos + 1);
|
||||
} else {
|
||||
options.host = hostInput;
|
||||
options.port = WHOIS_PORT;
|
||||
}
|
||||
if (OUTGOING_ADDRESS) {
|
||||
options.localAddress = OUTGOING_ADDRESS;
|
||||
options.family = isIPv6(OUTGOING_ADDRESS) ? 6 : 4;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
const socket = net.createConnection({
|
||||
host,
|
||||
port: WHOIS_PORT,
|
||||
localAddress: OUTGOING_ADDRESS,
|
||||
family: 4,
|
||||
timeout: WHOIS_TIMEOUT,
|
||||
}, () => socket.write(query + QUERY_SUFFIX));
|
||||
const socket = net.createConnection(
|
||||
options,
|
||||
() => socket.write(query + QUERY_SUFFIX),
|
||||
);
|
||||
socket.on('data', (chunk) => { data += chunk; });
|
||||
socket.on('close', () => resolve(data));
|
||||
socket.on('timeout', () => socket.destroy(new Error('Timeout')));
|
||||
|
@ -204,30 +229,33 @@ function checkForReferral(
|
|||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* whois ip
|
||||
* @param ip ip as string
|
||||
* @param host whois host (optional)
|
||||
* @returns {
|
||||
* range as [start: hex, end: hex, mask: number],
|
||||
* org as string,
|
||||
* descr as string,
|
||||
* asn as unsigned 32bit integer,
|
||||
* country as two letter lowercase code,
|
||||
* referralHost as string,
|
||||
* referralRange as [start: hex, end: hex, mask: number],
|
||||
* }
|
||||
*/
|
||||
export default async function whoisIp(
|
||||
ip,
|
||||
host = null,
|
||||
) {
|
||||
let useHost;
|
||||
if (!host) {
|
||||
if (Math.random() > 0.5) {
|
||||
useHost = 'whois.arin.net';
|
||||
} else {
|
||||
useHost = 'whois.iana.org';
|
||||
}
|
||||
} else {
|
||||
useHost = host;
|
||||
}
|
||||
let whoisResult = '';
|
||||
export default async function whoisIp(ip, options)
|
||||
{
|
||||
let host = options?.host || 'whois.iana.org';
|
||||
let logger = options?.logger || console;
|
||||
let whoisResult;
|
||||
let prevResult;
|
||||
let prevHost;
|
||||
let refCnt = 0;
|
||||
while (refCnt < 5) {
|
||||
let queryPrefix = '';
|
||||
if (useHost === 'whois.arin.net') {
|
||||
if (host === 'whois.arin.net') {
|
||||
queryPrefix = '+ n';
|
||||
} else if (useHost === 'whois.ripe.net') {
|
||||
} else if (host === 'whois.ripe.net') {
|
||||
/*
|
||||
* flag to not return personal information, otherwise
|
||||
* RIPE is gonna rate limit and ban
|
||||
|
@ -237,18 +265,37 @@ export default async function whoisIp(
|
|||
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
whoisResult = await singleWhoisQuery(`${queryPrefix} ${ip}`, useHost);
|
||||
whoisResult = await singleWhoisQuery(`${queryPrefix} ${ip}`, host);
|
||||
const ref = checkForReferral(whoisResult);
|
||||
if (!ref) {
|
||||
break;
|
||||
}
|
||||
useHost = ref;
|
||||
prevResult = whoisResult;
|
||||
prevHost = host;
|
||||
host = ref;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Error on WHOIS ${ip} ${useHost}: ${err.message}`);
|
||||
logger.error(`WHOIS ${ip} ${host}: ${err.message}`);
|
||||
host = prevHost;
|
||||
break;
|
||||
}
|
||||
refCnt += 1;
|
||||
}
|
||||
return parseWhois(ip, whoisResult);
|
||||
|
||||
let result = parseWhois(ip, whoisResult);
|
||||
if (!result.range) {
|
||||
// eslint-disable-next-line no-console
|
||||
logger.error(`WHOIS ${ip} ${host}: This host gives incomplete results.`);
|
||||
host = prevHost;
|
||||
}
|
||||
if (prevResult) {
|
||||
const pastWhois = parseWhois(ip, prevResult);
|
||||
if (host && pastWhois.range) {
|
||||
result.referralHost = host;
|
||||
result.referralRange = pastWhois.range;
|
||||
}
|
||||
result = {...pastWhois, ...result};
|
||||
}
|
||||
result.ip = ip;
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
/* @flow */
|
||||
// this script checks a ip with the pixelplanet proxychecker
|
||||
//
|
||||
|
||||
import fetch from '../src/utils/proxiedFetch.js';
|
||||
import isoFetch from 'isomorphic-fetch';
|
||||
|
||||
/*
|
||||
* check proxycheck.io if IP is proxy
|
||||
* Use proxiedFetch with random proxies
|
||||
* @param ip IP to check
|
||||
* @return true if proxy, false if not
|
||||
*/
|
||||
async function getProxyCheck(ip: string): Promise<boolean> {
|
||||
const url = `http://proxycheck.io/v2/${ip}?risk=1&vpn=1&asn=1`;
|
||||
//const url = 'http://pixel.space';
|
||||
console.log('fetching proxycheck', url);
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36',
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.log('proxycheck not ok ' + response.status + '/' + text);
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log('proxycheck.io is proxy?', ip, data);
|
||||
const ret = data.status == 'ok' && data[ip].proxy === 'yes';
|
||||
console.log(ret);
|
||||
}
|
||||
|
||||
async function getIPIntel(ip: string): Promise<boolean> {
|
||||
const email = Math.random().toString(36).substring(8) + "-" + Math.random().toString(36).substring(4) + "@gmail.com";
|
||||
const url = `http://check.getipintel.net/check.php?ip=${ip}&contact=${email}&flags=m`;
|
||||
console.log('fetching getipintel', url);
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
|
||||
Referer: 'http://check.getipintel.net/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36',
|
||||
}
|
||||
});
|
||||
// TODO log response code
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.log('getipintel not ok ' + response.status + '/' + text);
|
||||
return;
|
||||
}
|
||||
const body = await response.text();
|
||||
console.log('fetch getipintel is proxy?', ip, body);
|
||||
// returns tru iff we found 1 in the response and was ok (http code = 200)
|
||||
const value = parseFloat(body);
|
||||
return value > 0.995;
|
||||
}
|
||||
|
||||
|
||||
const ip = '188.172.220.70';
|
||||
getProxyCheck(ip);
|
||||
getIPIntel(ip);
|
|
@ -1,6 +0,0 @@
|
|||
email="cxyvsf@gmail.com"
|
||||
while read i; do
|
||||
RESULT=`geoiplookup $i | sed -e 's/.*, //'`
|
||||
#PROXY=`wget "http://check.getipintel.net/check.php?ip=$i&contact=alpha@gmail.com" -qO -`
|
||||
echo "$i $RESULT"
|
||||
done < ./ips
|
|
@ -1,4 +0,0 @@
|
|||
email="cxyvsf2@gmail.com"
|
||||
RESULT=`geoiplookup $1 | sed -e 's/.*, //'`
|
||||
PROXY=`wget "http://check.getipintel.net/check.php?ip=$1&contact=alpha@gmail.com" -qO -`
|
||||
echo "$1 $RESULT $PROXY"
|
Binary file not shown.
BIN
utils/pp-center--250_-243.png
Normal file
BIN
utils/pp-center--250_-243.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
Before Width: | Height: | Size: 43 KiB |
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
BIN
utils/pp-center-link--250_-243.png
Normal file
BIN
utils/pp-center-link--250_-243.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
Loading…
Reference in New Issue
Block a user