Merge branch 'master' into devel

This commit is contained in:
HF 2022-09-17 17:59:56 +02:00
commit f35a3ac9f2
8 changed files with 152 additions and 45 deletions

View File

@ -18,6 +18,7 @@ import {
unmute, unmute,
allowedChat, allowedChat,
} from '../data/redis/chat'; } from '../data/redis/chat';
import { banIP } from '../data/sql/Ban';
import { DailyCron } from '../utils/cron'; import { DailyCron } from '../utils/cron';
import { escapeMd } from './utils'; import { escapeMd } from './utils';
import ttags from './ttag'; import ttags from './ttag';
@ -52,6 +53,7 @@ export class ChatProvider {
this.enChannelId = 0; this.enChannelId = 0;
this.infoUserId = 1; this.infoUserId = 1;
this.eventUserId = 1; this.eventUserId = 1;
this.autobanPhrase = null;
this.apiSocketUserId = 1; this.apiSocketUserId = 1;
this.caseCheck = /^[A-Z !.]*$/; this.caseCheck = /^[A-Z !.]*$/;
this.cyrillic = /[\u0436-\u043B]'/; this.cyrillic = /[\u0436-\u043B]'/;
@ -360,6 +362,22 @@ export class ChatProvider {
return 'No country is currently muted from this channel'; return 'No country is currently muted from this channel';
} }
case 'autoban': {
if (args[0]) {
this.autobanPhrase = args.join(' ');
if (this.autobanPhrase === 'unset' || this.autobanPhrase.length < 5) {
this.autobanPhrase = null;
}
return `Set autoban phrase on shard to ${this.autobanPhrase}`;
}
// eslint-disable-next-line
if (this.autobanPhrase) {
// eslint-disable-next-line
return `Current autoban phrase on shard is ${this.autobanPhrase}, use "/autoban unset" to remove it`;
}
return 'Autoban phrase is currently not set on this shard';
}
default: default:
return `Couln't parse command ${cmd}`; return `Couln't parse command ${cmd}`;
} }
@ -387,6 +405,20 @@ export class ChatProvider {
} }
const country = user.regUser.flag || 'xx'; const country = user.regUser.flag || 'xx';
if (this.autobanPhrase && message.includes(this.autobanPhrase)) {
const { ipSub } = user;
if (!user.banned) {
banIP(ipSub, 'CHATBAN', 0, 1);
mute(id);
logger.info(`CHAT AUTOBANNED: ${ipSub}`);
user.banned = true;
}
return 'nope';
}
if (user.banned) {
return 'nope';
}
if (!user.userlvl) { if (!user.userlvl) {
const [allowed, needProxycheck] = await allowedChat( const [allowed, needProxycheck] = await allowedChat(
channelId, channelId,
@ -405,6 +437,8 @@ export class ChatProvider {
} if (allowed === 101) { } if (allowed === 101) {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
return t`You are permanently muted, join our guilded to apppeal the mute`; return t`You are permanently muted, join our guilded to apppeal the mute`;
} if (allowed === 102) {
return t`You must solve a captcha. Place a pixel to get one.`;
} if (allowed === 2) { } if (allowed === 2) {
return t`You are banned`; return t`You are banned`;
} if (allowed === 3) { } if (allowed === 3) {

View File

@ -16,9 +16,16 @@ import { proxyLogger as logger } from './logger';
import { USE_PROXYCHECK, PROXYCHECK_KEY } from './config'; import { USE_PROXYCHECK, PROXYCHECK_KEY } from './config';
const checker = (USE_PROXYCHECK && PROXYCHECK_KEY) // checker for IP address validity (proxy or vpn or not)
? new ProxyCheck(PROXYCHECK_KEY, logger).checkIp let checker = () => ({ allowed: true, status: 0, pcheck: 'dummy' });
: () => ({ allowed: true, status: 0, pcheck: 'dummy' }); // checker for mail address (disposable or not)
let mailChecker = () => false;
if (USE_PROXYCHECK && PROXYCHECK_KEY) {
const pc = new ProxyCheck(PROXYCHECK_KEY, logger);
checker = pc.checkIp;
mailChecker = pc.checkEmail;
}
/* /*
* save information of ip into database * save information of ip into database
@ -125,7 +132,7 @@ async function withCache(f, ip) {
* check if ip is allowed * check if ip is allowed
* @param ip IP * @param ip IP
* @param disableCache if we fetch result from cache * @param disableCache if we fetch result from cache
* @return { * @return Promise {
* allowed: boolean if allowed to use site * allowed: boolean if allowed to use site
* , status: -2: not yet checked * , status: -2: not yet checked
* -1: whitelisted * -1: whitelisted
@ -143,4 +150,16 @@ function checkIfAllowed(ip, disableCache = false) {
return withCache(checker, ip); return withCache(checker, ip);
} }
/*
* check if email is disposable
* @param email
* @return Promise
* null: some error occured
* false: legit provider
* true: disposable
*/
export function checkIfMailDisposable(email) {
return mailChecker(email);
}
export default checkIfAllowed; export default checkIfAllowed;

View File

@ -12,7 +12,7 @@ import RedditStrategy from 'passport-reddit/lib/passport-reddit/strategy';
import VkontakteStrategy from 'passport-vkontakte/lib/strategy'; import VkontakteStrategy from 'passport-vkontakte/lib/strategy';
import { sanitizeName } from '../utils/validation'; import { sanitizeName } from '../utils/validation';
import logger from './logger';
import { RegUser } from '../data/sql'; import { RegUser } from '../data/sql';
import User, { regUserQueryInclude as include } from '../data/User'; import User, { regUserQueryInclude as include } from '../data/User';
import { auth } from './config'; import { auth } from './config';
@ -70,7 +70,7 @@ passport.use(new JsonStrategy({
* OAuth SignIns, mail based * OAuth SignIns, mail based
* *
*/ */
async function oauthLogin(email, name, discordid = null) { async function oauthLogin(provider, email, name, discordid = null) {
if (!email) { if (!email) {
throw new Error('You don\'t have a mail set in your account.'); throw new Error('You don\'t have a mail set in your account.');
} }
@ -94,6 +94,8 @@ async function oauthLogin(email, name, discordid = null) {
where: { name }, where: { name },
}); });
} }
// eslint-disable-next-line max-len
logger.info(`Create new user from ${provider} oauth login ${email} / ${name}`);
reguser = await RegUser.create({ reguser = await RegUser.create({
email, email,
name, name,
@ -121,7 +123,7 @@ passport.use(new FacebookStrategy({
try { try {
const { displayName: name, emails } = profile; const { displayName: name, emails } = profile;
const email = emails[0].value; const email = emails[0].value;
const user = await oauthLogin(email, name); const user = await oauthLogin('facebook', email, name);
done(null, user); done(null, user);
} catch (err) { } catch (err) {
done(err); done(err);
@ -144,7 +146,7 @@ passport.use(new DiscordStrategy({
'Sorry, you can not use discord login with an discord account that does not have email set.', 'Sorry, you can not use discord login with an discord account that does not have email set.',
); );
} }
const user = await oauthLogin(email, name, id); const user = await oauthLogin('discord', email, name, id);
done(null, user); done(null, user);
} catch (err) { } catch (err) {
done(err); done(err);
@ -162,7 +164,7 @@ passport.use(new GoogleStrategy({
try { try {
const { displayName: name, emails } = profile; const { displayName: name, emails } = profile;
const email = emails[0].value; const email = emails[0].value;
const user = await oauthLogin(email, name); const user = await oauthLogin('google', email, name);
done(null, user); done(null, user);
} catch (err) { } catch (err) {
done(err); done(err);
@ -201,6 +203,8 @@ passport.use(new RedditStrategy({
where: { name }, where: { name },
}); });
} }
// eslint-disable-next-line max-len
logger.info(`Create new user from reddit oauth login ${name} / ${redditid}`);
reguser = await RegUser.create({ reguser = await RegUser.create({
name, name,
verified: 1, verified: 1,
@ -234,7 +238,7 @@ passport.use(new VkontakteStrategy({
'Sorry, you can not use vk login with an account that does not have a verified email set.', 'Sorry, you can not use vk login with an account that does not have a verified email set.',
); );
} }
const user = await oauthLogin(email, name); const user = await oauthLogin('vkontakte', email, name);
done(null, user); done(null, user);
} catch (err) { } catch (err) {
done(err); done(err);

View File

@ -3,6 +3,8 @@
*/ */
import client from './client'; import client from './client';
import { PREFIX as ALLOWED_PREFIX } from './isAllowedCache'; import { PREFIX as ALLOWED_PREFIX } from './isAllowedCache';
import { PREFIX as CAPTCHA_PREFIX } from './captcha';
import { CAPTCHA_TIME } from '../../core/config';
const MUTE_PREFIX = 'MUTE_PREFIX'; const MUTE_PREFIX = 'MUTE_PREFIX';
const MUTEC_PREFIX = 'MUTE_PREFIXc'; const MUTEC_PREFIX = 'MUTE_PREFIXc';
@ -22,8 +24,9 @@ export async function allowedChat(
const mutecKey = `${MUTEC_PREFIX}:${channelId}`; const mutecKey = `${MUTEC_PREFIX}:${channelId}`;
const muteKey = `${MUTE_PREFIX}:${userId}`; const muteKey = `${MUTE_PREFIX}:${userId}`;
const isalKey = `${ALLOWED_PREFIX}:${ip}`; const isalKey = `${ALLOWED_PREFIX}:${ip}`;
const captKey = (CAPTCHA_TIME >= 0) ? `${CAPTCHA_PREFIX}:${ip}` : 'nope';
return client.allowedChat( return client.allowedChat(
mutecKey, muteKey, isalKey, mutecKey, muteKey, isalKey, captKey,
cc, cc,
); );
} }

View File

@ -18,7 +18,7 @@ const scripts = {
transformReply(arr) { return arr.map((r) => Number(r)); }, transformReply(arr) { return arr.map((r) => Number(r)); },
}), }),
allowedChat: defineScript({ allowedChat: defineScript({
NUMBER_OF_KEYS: 3, NUMBER_OF_KEYS: 4,
SCRIPT: fs.readFileSync('./workers/lua/allowedChat.lua'), SCRIPT: fs.readFileSync('./workers/lua/allowedChat.lua'),
transformArguments(...args) { transformArguments(...args) {
return args.map((a) => ((typeof a === 'string') ? a : a.toString())); return args.map((a) => ((typeof a === 'string') ? a : a.toString()));

View File

@ -3,6 +3,7 @@
-- mutecKey: 'mutec:cid' hash of channel for country mutes -- mutecKey: 'mutec:cid' hash of channel for country mutes
-- muteKey: 'mute:uid' key for user mute -- muteKey: 'mute:uid' key for user mute
-- isalKey: 'isal:ip' (proxycheck, blacklist, whitelist) -- isalKey: 'isal:ip' (proxycheck, blacklist, whitelist)
-- ishuman: 'human:ip' if captcha needed
-- Args: -- Args:
-- cc: two letter country code of user -- cc: two letter country code of user
-- Returns: -- Returns:
@ -10,6 +11,7 @@
-- 1: return status code -- 1: return status code
-- 100: country muted -- 100: country muted
-- 101: user permanently muted -- 101: user permanently muted
-- 102: got captcha
-- >0: isAllowed status code (see core/isAllowed) -- >0: isAllowed status code (see core/isAllowed)
-- 0: success -- 0: success
-- <0: time left for mute in seconds * -1 -- <0: time left for mute in seconds * -1
@ -27,6 +29,12 @@ if ttl == -1 then
ret[1] = 101 ret[1] = 101
return ret return ret
end end
-- check if captcha is needed
if KEYS[4] ~= "nope" and not redis.call('get', KEYS[4]) then
-- captcha
ret[1] = 102
return ret
end
if ttl > 0 then if ttl > 0 then
ret[1] = -ttl ret[1] = -ttl
return ret return ret

View File

@ -5,6 +5,7 @@ import { RegUser } from '../../../data/sql';
import mailProvider from '../../../core/MailProvider'; import mailProvider from '../../../core/MailProvider';
import getMe from '../../../core/me'; import getMe from '../../../core/me';
import { getIPFromRequest, getHostFromRequest } from '../../../utils/ip'; import { getIPFromRequest, getHostFromRequest } from '../../../utils/ip';
import { checkIfMailDisposable } from '../../../core/isAllowed';
import { import {
validateEMail, validateEMail,
validateName, validateName,
@ -17,7 +18,11 @@ import {
async function validate(email, name, password, captcha, captchaid, t, gettext) { async function validate(email, name, password, captcha, captchaid, t, gettext) {
const errors = []; const errors = [];
const emailerror = gettext(validateEMail(email)); const emailerror = gettext(validateEMail(email));
if (emailerror) errors.push(emailerror); if (emailerror) {
errors.push(emailerror);
} else if (await checkIfMailDisposable(email)) {
errors.push(t`This email provider is not allowed`);
}
const nameerror = validateName(name); const nameerror = validateName(name);
if (nameerror) errors.push(nameerror); if (nameerror) errors.push(nameerror);
const passworderror = gettext(validatePassword(password)); const passworderror = gettext(validatePassword(password));

View File

@ -157,6 +157,7 @@ class PcKeyProvider {
} }
/* /*
* TODO: proxycheck added the used burst token to API
* query the API for limits * query the API for limits
* @param key * @param key
*/ */
@ -229,7 +230,7 @@ class ProxyCheck {
* queue of ip-checking tasks * queue of ip-checking tasks
* [[ip, callbackFunction],...] * [[ip, callbackFunction],...]
*/ */
this.ipQueue = []; this.queue = [];
this.fetching = false; this.fetching = false;
this.checkFromQueue = this.checkFromQueue.bind(this); this.checkFromQueue = this.checkFromQueue.bind(this);
this.checkIp = this.checkIp.bind(this); this.checkIp = this.checkIp.bind(this);
@ -237,7 +238,7 @@ class ProxyCheck {
this.logger = logger; this.logger = logger;
} }
reqProxyCheck(ips) { reqProxyCheck(values) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const key = this.pcKeyProvider.getKey(); const key = this.pcKeyProvider.getKey();
if (!key) { if (!key) {
@ -247,7 +248,7 @@ class ProxyCheck {
); );
return; return;
} }
const postData = `ips=${ips.join(',')}`; const postData = `ips=${values.join(',')}`;
const options = { const options = {
hostname: 'proxycheck.io', hostname: 'proxycheck.io',
@ -276,16 +277,17 @@ class ProxyCheck {
const jsonString = data.join(''); const jsonString = data.join('');
const result = JSON.parse(jsonString); const result = JSON.parse(jsonString);
if (result.status !== 'ok') { if (result.status !== 'ok') {
if (result.status === 'error' && ips.length === 1) { if (result.status === 'error' && values.length === 1) {
/* /*
* invalid ip, like a link local address * invalid ip, like a link local address
* Error is either thrown in the top, when requesting only one ip * Error is either thrown in the top, when requesting only one ip
* or in the ip-part as "error": "No valid.." when multiple * or in the ip-part as "error": "No valid.." when multiple
* */ * */
resolve({ resolve({
[ips[0]]: { [values[0]]: {
proxy: 'yes', proxy: 'yes',
type: 'Invalid IP', type: 'Invalid IP',
disposable: 'yes',
}, },
}); });
return; return;
@ -299,11 +301,12 @@ class ProxyCheck {
this.logger.warn(`Warning: ${key}: ${result.message}`); this.logger.warn(`Warning: ${key}: ${result.message}`);
} }
} }
ips.forEach((ip) => { values.forEach((value) => {
if (result[ip] && result[ip].error) { if (result[value] && result[value].error) {
result[ip] = { result[value] = {
proxy: 'yes', proxy: 'yes',
type: 'Invalid IP', type: 'Invalid IP',
disposable: 'yes',
}; };
} }
}); });
@ -330,45 +333,57 @@ class ProxyCheck {
} }
async checkFromQueue() { async checkFromQueue() {
const { ipQueue } = this; const { queue } = this;
if (!ipQueue.length) { if (!queue.length) {
this.fetching = false; this.fetching = false;
return; return;
} }
this.fetching = true; this.fetching = true;
const tasks = ipQueue.slice(0, 50); const tasks = queue.slice(0, 50);
const ips = tasks.map((i) => i[0]); const values = tasks.map((i) => i[0]);
let res = {}; let res = {};
try { try {
res = await this.reqProxyCheck(ips); res = await this.reqProxyCheck(values);
} catch (err) { } catch (err) {
this.logger.error(`Eroor: ${err.message}`); this.logger.error(`Eroor: ${err.message}`);
} }
for (let i = 0; i < tasks.length; i += 1) { for (let i = 0; i < tasks.length; i += 1) {
const task = tasks[i]; const task = tasks[i];
const pos = ipQueue.indexOf(task); const pos = queue.indexOf(task);
if (~pos) ipQueue.splice(pos, 1); if (~pos) queue.splice(pos, 1);
const [ip, cb] = task; const [value, cb] = task;
let allowed = true; if (~value.indexOf('@')) {
let status = -2; // email check
let pcheck = 'N/A'; let disposable = null;
if (res[ip]) { if (res[value]) {
this.logger.info(`${ip}: ${JSON.stringify(res[ip])}`); disposable = !!res[value].disposable;
const { proxy, type, city } = res[ip]; }
allowed = proxy === 'no';
status = (allowed) ? 0 : 1; cb(disposable);
pcheck = `${type},${city}`; } else {
// ip check
let allowed = true;
let status = -2;
let pcheck = 'N/A';
if (res[value]) {
this.logger.info(`${value}: ${JSON.stringify(res[value])}`);
const { proxy, type, city } = res[value];
allowed = proxy === 'no';
status = (allowed) ? 0 : 1;
pcheck = `${type},${city}`;
}
cb({
allowed,
status,
pcheck,
});
} }
cb({
allowed,
status,
pcheck,
});
} }
setTimeout(this.checkFromQueue, 10); setTimeout(this.checkFromQueue, 10);
} }
@ -385,7 +400,26 @@ class ProxyCheck {
*/ */
checkIp(ip) { checkIp(ip) {
return new Promise((resolve) => { return new Promise((resolve) => {
this.ipQueue.push([ip, resolve]); this.queue.push([ip, resolve]);
if (!this.fetching) {
this.checkFromQueue();
}
});
}
/*
* 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
* false: is legit provider
* true: is disposable provider
*/
checkEmail(email) {
return new Promise((resolve) => {
this.queue.push([email, resolve]);
if (!this.fetching) { if (!this.fetching) {
this.checkFromQueue(); this.checkFromQueue();
} }