Support marksharing ranges (#8498)

This commit is contained in:
Mia 2021-10-24 14:44:46 -05:00 committed by GitHub
parent 0f5c9c133b
commit 88ef7fdf1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 111 additions and 46 deletions

View File

@ -243,7 +243,7 @@ export const commands: Chat.ChatCommands = {
status.push(punishMsg);
}
}
if (Punishments.sharedIps.has(ip)) {
if (Punishments.isSharedIp(ip)) {
let sharedStr = 'shared';
if (Punishments.sharedIps.get(ip)) {
sharedStr += `: ${Punishments.sharedIps.get(ip)}`;

View File

@ -902,7 +902,7 @@ export const commands: Chat.ChatCommands = {
let affected = [];
if (targetUser) {
const ignoreAlts = Punishments.sharedIps.has(targetUser.latestIp);
const ignoreAlts = Punishments.isSharedIp(targetUser.latestIp);
affected = await Punishments.lock(targetUser, duration, null, ignoreAlts, publicReason);
} else {
affected = await Punishments.lock(userid, duration, null, false, publicReason);

View File

@ -768,7 +768,7 @@ export function notifyStaff() {
function checkIp(ip: string) {
for (const t in tickets) {
if (tickets[t].ip === ip && tickets[t].open && !Punishments.sharedIps.has(ip)) {
if (tickets[t].ip === ip && tickets[t].open && !Punishments.isSharedIp(ip)) {
return tickets[t];
}
}
@ -2097,7 +2097,7 @@ export const commands: Chat.ChatCommands = {
return this.parse(`/join help-${ticket.userid}`);
}
if (Monitor.countTickets(user.latestIp)) {
const maxTickets = Punishments.sharedIps.has(user.latestIp) ? `50` : `5`;
const maxTickets = Punishments.isSharedIp(user.latestIp) ? `50` : `5`;
return this.popupReply(this.tr`Due to high load, you are limited to creating ${maxTickets} tickets every hour.`);
}
let [

View File

@ -438,9 +438,24 @@ export const commands: Chat.ChatCommands = {
if (!target) return this.parse('/help markshared');
checkCanPerform(this, user, 'globalban');
const [ip, note] = this.splitOne(target);
if (!IPTools.ipRegex.test(ip)) return this.errorReply("Please enter a valid IP address.");
if (!IPTools.ipRegex.test(ip)) {
const pattern = IPTools.stringToRange(ip);
if (!pattern) {
return this.errorReply("Please enter a valid IP address.");
}
if (!user.can('rangeban')) {
return this.errorReply('Only upper staff can markshare ranges.');
}
for (const range of Punishments.sharedRanges.keys()) {
if (IPTools.rangeIntersects(range, pattern)) {
return this.errorReply(
`Range ${IPTools.rangeToString(pattern)} intersects with shared range ${IPTools.rangeToString(range)}`
);
}
}
}
if (Punishments.sharedIps.has(ip)) return this.errorReply("This IP is already marked as shared.");
if (Punishments.isSharedIp(ip)) return this.errorReply("This IP is already marked as shared.");
if (Punishments.isBlacklistedSharedIp(ip)) {
return this.errorReply(`This IP is blacklisted from being marked as shared.`);
}
@ -464,7 +479,7 @@ export const commands: Chat.ChatCommands = {
checkCanPerform(this, user, 'globalban');
if (!IPTools.ipRegex.test(target)) return this.errorReply("Please enter a valid IP address.");
if (!Punishments.sharedIps.has(target)) return this.errorReply("This IP isn't marked as shared.");
if (!Punishments.isSharedIp(target)) return this.errorReply("This IP isn't marked as shared.");
Punishments.removeSharedIp(target);
@ -512,7 +527,7 @@ export const commands: Chat.ChatCommands = {
}
}
} else {
if (Punishments.sharedIps.has(ip)) this.parse(`/unmarkshared ${ip}`);
if (Punishments.isSharedIp(ip)) this.parse(`/unmarkshared ${ip}`);
}
const reason = reasonArr.join(',');

View File

@ -184,6 +184,9 @@ export const IPTools = new class {
if (minIP === null || maxIP === null || maxIP < minIP) return null;
return {minIP, maxIP};
}
rangeToString(range: AddressRange, sep = '-') {
return `${this.numberToIP(range.minIP)}${sep}${this.numberToIP(range.maxIP)}`;
}
/******************************
* Range management functions *
@ -295,7 +298,7 @@ export const IPTools = new class {
}
IPTools.sortRanges();
for (const range of IPTools.ranges) {
const data = `RANGE,${IPTools.numberToIP(range.minIP)},${IPTools.numberToIP(range.maxIP)}${range.host ? `,${range.host}` : ``}\n`;
const data = `RANGE,${IPTools.rangeToString(range, ',')}${range.host ? `,${range.host}` : ``}\n`;
if (range.host?.endsWith('/proxy')) {
proxiesData += data;
} else {
@ -362,10 +365,19 @@ export const IPTools = new class {
return IPTools.saveHostsAndRanges();
}
rangeIntersects(a: AddressRange, b: AddressRange) {
try {
this.checkRangeConflicts(a, [b]);
} catch {
return true;
}
return false;
}
checkRangeConflicts(insertion: AddressRange, sortedRanges: AddressRange[], widen?: boolean) {
if (insertion.maxIP < insertion.minIP) {
throw new Error(
`Invalid data for address range ${IPTools.numberToIP(insertion.minIP)}-${IPTools.numberToIP(insertion.maxIP)} (${insertion.host})`
`Invalid data for address range ${IPTools.rangeToString(insertion)} (${insertion.host})`
);
}
@ -382,7 +394,7 @@ export const IPTools = new class {
if (iMin < sortedRanges.length) {
const next = sortedRanges[iMin];
if (insertion.minIP === next.minIP && insertion.maxIP === next.maxIP) {
throw new Error(`The address range ${IPTools.numberToIP(insertion.minIP)}-${IPTools.numberToIP(insertion.maxIP)} (${insertion.host}) already exists`);
throw new Error(`The address range ${IPTools.rangeToString(insertion)} (${insertion.host}) already exists`);
}
if (insertion.minIP <= next.minIP && insertion.maxIP >= next.maxIP) {
if (widen) {
@ -392,14 +404,14 @@ export const IPTools = new class {
return iMin;
}
throw new Error(
`Too wide: ${IPTools.numberToIP(insertion.minIP)}-${IPTools.numberToIP(insertion.maxIP)} (${insertion.host})\n` +
`Intersects with: ${IPTools.numberToIP(next.minIP)}-${IPTools.numberToIP(next.maxIP)} (${next.host})`
`Too wide: ${IPTools.rangeToString(insertion)} (${insertion.host})\n` +
`Intersects with: ${IPTools.rangeToString(next)} (${next.host})`
);
}
if (insertion.maxIP >= next.minIP) {
throw new Error(
`Could not insert: ${IPTools.numberToIP(insertion.minIP)}-${IPTools.numberToIP(insertion.maxIP)} ${insertion.host}\n` +
`Intersects with: ${IPTools.numberToIP(next.minIP)}-${IPTools.numberToIP(next.maxIP)} (${next.host})`
`Could not insert: ${IPTools.rangeToString(insertion)} ${insertion.host}\n` +
`Intersects with: ${IPTools.rangeToString(next)} (${next.host})`
);
}
}
@ -407,14 +419,14 @@ export const IPTools = new class {
const prev = sortedRanges[iMin - 1];
if (insertion.minIP >= prev.minIP && insertion.maxIP <= prev.maxIP) {
throw new Error(
`Too narrow: ${IPTools.numberToIP(insertion.minIP)}-${IPTools.numberToIP(insertion.maxIP)} (${insertion.host})\n` +
`Intersects with: ${IPTools.numberToIP(prev.minIP)}-${IPTools.numberToIP(prev.maxIP)} (${prev.host})`
`Too narrow: ${IPTools.rangeToString(insertion)} (${insertion.host})\n` +
`Intersects with: ${IPTools.rangeToString(prev)} (${prev.host})`
);
}
if (insertion.minIP <= prev.maxIP) {
throw new Error(
`Could not insert: ${IPTools.numberToIP(insertion.minIP)}-${IPTools.numberToIP(insertion.maxIP)} (${insertion.host})\n` +
`Intersects with: ${IPTools.numberToIP(prev.minIP)}-${IPTools.numberToIP(prev.maxIP)} (${prev.host})`
`Could not insert: ${IPTools.rangeToString(insertion)} (${insertion.host})\n` +
`Intersects with: ${IPTools.rangeToString(prev)} (${prev.host})`
);
}
}
@ -568,7 +580,7 @@ export const IPTools = new class {
* - 'unknown' - no rdns entry, treat with suspicion
*/
getHostType(host: string, ip: string) {
if (Punishments.sharedIps.has(ip)) {
if (Punishments.isSharedIp(ip)) {
return 'shared';
}
if (this.singleIPOpenProxies.has(ip) || this.torProxyIps.has(ip)) {

View File

@ -206,7 +206,7 @@ export const Monitor = new class {
if (Config.noipchecks || Config.nothrottle) return false;
const count = this.battlePreps.increment(ip, 3 * 60 * 1000)[0];
if (count <= 12) return false;
if (count < 120 && Punishments.sharedIps.has(ip)) return false;
if (count < 120 && Punishments.isSharedIp(ip)) return false;
connection.popup('Due to high load, you are limited to 12 battles and team validations every 3 minutes.');
return true;
}
@ -236,7 +236,7 @@ export const Monitor = new class {
if (Config.noipchecks || Config.nothrottle) return false;
const [count] = this.netRequests.increment(ip, 1 * 60 * 1000);
if (count <= 10) return false;
if (count < 120 && Punishments.sharedIps.has(ip)) return false;
if (count < 120 && Punishments.isSharedIp(ip)) return false;
return true;
}
@ -246,7 +246,7 @@ export const Monitor = new class {
countTickets(ip: string) {
if (Config.noipchecks || Config.nothrottle) return false;
const count = this.tickets.increment(ip, 60 * 60 * 1000)[0];
if (Punishments.sharedIps.has(ip)) {
if (Punishments.isSharedIp(ip)) {
return count >= 20;
} else {
return count >= 5;

View File

@ -12,6 +12,7 @@
*/
import {FS, Utils} from '../lib';
import type {AddressRange} from './ip-tools';
const PUNISHMENT_FILE = 'config/punishments.tsv';
const ROOM_PUNISHMENT_FILE = 'config/room-punishments.tsv';
@ -228,6 +229,11 @@ export const Punishments = new class {
* sharedIps is an ip:note Map
*/
readonly sharedIps = new Map<string, string>();
/**
* AddressRange:note map. In a separate map so we iterate a massive map a lot less.
* (AddressRange is a bit of a premature optimization, but it saves us a conversion call on some trafficked spots)
*/
readonly sharedRanges = new Map<AddressRange, string>();
/**
* sharedIpBlacklist is an ip:note Map
*/
@ -457,7 +463,15 @@ export const Punishments = new class {
for (const row of data.replace('\r', '').split("\n")) {
if (!row) continue;
const [ip, type, note] = row.trim().split("\t");
if (!IPTools.ipRegex.test(ip)) continue;
if (!IPTools.ipRegex.test(ip)) {
const pattern = IPTools.stringToRange(ip);
if (pattern) {
Punishments.sharedRanges.set(pattern, note);
} else {
Monitor.adminlog(`Invalid range data in '${SHAREDIPS_FILE}': "${row}".`);
}
continue;
}
if (type !== 'SHARED') continue;
Punishments.sharedIps.set(ip, note);
@ -465,15 +479,23 @@ export const Punishments = new class {
}
appendSharedIp(ip: string, note: string) {
const buf = `${ip}\tSHARED\t${note}\r\n`;
const pattern = IPTools.stringToRange(ip);
let ipString = ip;
if (pattern && pattern.minIP !== pattern.maxIP) {
ipString = IPTools.rangeToString(pattern);
}
const buf = `${ipString}\tSHARED\t${note}\r\n`;
return FS(SHAREDIPS_FILE).append(buf);
}
saveSharedIps() {
let buf = 'IP\tType\tNote\r\n';
Punishments.sharedIps.forEach((note, ip) => {
for (const [note, ip] of Punishments.sharedIps) {
buf += `${ip}\tSHARED\t${note}\r\n`;
});
}
for (const [range, note] of Punishments.sharedRanges) {
buf += `${IPTools.rangeToString(range)}\tSHARED\t${note}\r\n`;
}
return FS(SHAREDIPS_FILE).write(buf);
}
@ -1113,7 +1135,7 @@ export const Punishments = new class {
for (const ip of user.ips) {
punishment = Punishments.ips.getByType(ip, 'BATTLEBAN');
if (punishment) {
if (Punishments.sharedIps.has(ip) && user.autoconfirmed) return;
if (Punishments.isSharedIp(ip) && user.autoconfirmed) return;
return punishment;
}
}
@ -1178,7 +1200,7 @@ export const Punishments = new class {
for (const ip of targetUser.ips) {
punishment = Punishments.ips.getByType(ip, 'GROUPCHATBAN');
if (punishment) {
if (Punishments.sharedIps.has(ip) && targetUser.autoconfirmed) return;
if (Punishments.isSharedIp(ip) && targetUser.autoconfirmed) return;
return punishment;
}
}
@ -1196,7 +1218,7 @@ export const Punishments = new class {
if (punishment) return punishment;
// skip if the user is autoconfirmed and on a shared ip
// [0] is forced to be the latestIp
if (Punishments.sharedIps.has(ips[0])) return false;
if (Punishments.isSharedIp(ips[0])) return false;
for (const ip of ips) {
const curPunishment = Punishments.ips.getByType(ip, 'TICKETBAN');
@ -1357,11 +1379,20 @@ export const Punishments = new class {
}
addSharedIp(ip: string, note: string) {
Punishments.sharedIps.set(ip, note);
const pattern = IPTools.stringToRange(ip);
const isRange = pattern && pattern.minIP !== pattern.maxIP;
if (isRange) {
Punishments.sharedRanges.set(pattern, note);
} else {
Punishments.sharedIps.set(ip, note);
}
void Punishments.appendSharedIp(ip, note);
for (const user of Users.users.values()) {
if (user.locked && user.locked !== user.id && user.ips.includes(ip)) {
const sharedIp = user.ips.some(
curIP => (isRange ? IPTools.checkPattern([pattern], IPTools.ipToNumber(curIP)) : curIP === ip)
);
if (user.locked && user.locked !== user.id && sharedIp) {
if (!user.autoconfirmed) {
user.semilocked = `#sharedip ${user.locked}` as PunishType;
}
@ -1374,6 +1405,17 @@ export const Punishments = new class {
}
}
isSharedIp(ip: string) {
if (this.sharedIps.has(ip)) return true;
const num = IPTools.ipToNumber(ip);
for (const range of this.sharedRanges.keys()) {
if (IPTools.checkPattern([range], num)) {
return true;
}
}
return false;
}
removeSharedIp(ip: string) {
Punishments.sharedIps.delete(ip);
void Punishments.saveSharedIps();
@ -1552,7 +1594,7 @@ export const Punishments = new class {
`<a href="view-help-request--appeal"><button class="button"><strong>Appeal your punishment</strong></button></a>` : '';
if (battleban) {
if (battleban.id !== user.id && Punishments.sharedIps.has(user.latestIp) && user.autoconfirmed) {
if (battleban.id !== user.id && Punishments.isSharedIp(user.latestIp) && user.autoconfirmed) {
Punishments.unpunish(userid, 'BATTLEBAN');
} else {
void Punishments.punish(user, battleban, false);
@ -1582,7 +1624,7 @@ export const Punishments = new class {
}
const bannedUnder = punishUserid !== userid ? ` because you have the same IP as banned user: ${punishUserid}` : '';
if ((id === 'LOCK' || id === 'NAMELOCK') && punishUserid !== userid && Punishments.sharedIps.has(user.latestIp)) {
if ((id === 'LOCK' || id === 'NAMELOCK') && punishUserid !== userid && Punishments.isSharedIp(user.latestIp)) {
if (!user.autoconfirmed) {
user.semilocked = `#sharedip ${user.locked}` as PunishType;
}
@ -1636,7 +1678,7 @@ export const Punishments = new class {
if (punishments) {
let shared = false;
for (const punishment of punishments) {
if (Punishments.sharedIps.has(user.latestIp)) {
if (Punishments.isSharedIp(user.latestIp)) {
if (!user.locked && !user.autoconfirmed) {
user.semilocked = `#sharedip ${punishment.id}` as PunishType;
}
@ -1684,7 +1726,7 @@ export const Punishments = new class {
return '#cflood';
}
if (Punishments.sharedIps.has(ip)) return false;
if (Punishments.isSharedIp(ip)) return false;
let banned: false | string = false;
const punishment = Punishments.ipSearch(ip, 'BAN');
@ -1792,7 +1834,7 @@ export const Punishments = new class {
if (punishment.type === 'ROOMBAN') {
return punishment;
} else if (punishment.type === 'BLACKLIST') {
if (Punishments.sharedIps.has(ip) && user.autoconfirmed) return;
if (Punishments.isSharedIp(ip) && user.autoconfirmed) return;
return punishment;
}
@ -1815,18 +1857,14 @@ export const Punishments = new class {
}
isBlacklistedSharedIp(ip: string) {
const num = IPTools.ipToNumber(ip);
if (!num) {
if (IPTools.ipRangeRegex.test(ip)) {
return this.sharedIpBlacklist.has(ip);
} else {
throw new Error(`Invalid IP address: '${ip}'`);
}
const pattern = IPTools.stringToRange(ip);
if (!pattern) {
throw new Error(`Invalid IP address: '${ip}'`);
}
for (const [blacklisted, reason] of this.sharedIpBlacklist) {
const range = IPTools.stringToRange(blacklisted);
if (!range) throw new Error("Falsy range in sharedIpBlacklist");
if (IPTools.checkPattern([range], num)) return reason;
if (IPTools.rangeIntersects(range, pattern)) return reason;
}
return false;
}

View File

@ -149,7 +149,7 @@ function getExactUser(name: string | User) {
*/
function findUsers(userids: ID[], ips: string[], options: {forPunishment?: boolean, includeTrusted?: boolean} = {}) {
const matches: User[] = [];
if (options.forPunishment) ips = ips.filter(ip => !Punishments.sharedIps.has(ip));
if (options.forPunishment) ips = ips.filter(ip => !Punishments.isSharedIp(ip));
const ipMatcher = IPTools.checker(ips);
for (const user of users.values()) {
if (!options.forPunishment && !user.named && !user.connected) continue;