mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
Support marksharing ranges (#8498)
This commit is contained in:
parent
0f5c9c133b
commit
88ef7fdf1f
|
|
@ -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)}`;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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(',');
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user