pokemon-showdown/monitor.js
Guangcong Luo d79e348ebc Refactor banlistTable -> ruleTable
PS's rule table has been renamed from banlistTable, and works a bit
differently now. It's a Map instead of an object now, and the keys
work a bit differently.

The original banlistTable was designed to store bans, and later
additions shoved rules and then unbans in there. The new table is
designed to support all of these.
2017-07-20 12:50:41 -05:00

301 lines
7.1 KiB
JavaScript

/**
* Monitor
* Pokemon Showdown - http://pokemonshowdown.com/
*
* Various utility functions to make sure PS is running healthily.
*
* @license MIT license
*/
'use strict';
const FS = require('./fs');
const MONITOR_CLEAN_TIMEOUT = 2 * 60 * 60 * 1000;
/**
* This counts the number of times an action has been committed, and tracks the
* delta of time since the last time it was committed. Actions include
* connecting to the server, starting a battle, validating a team, and
* sending/receiving data over a connection's socket.
* @augments {Map<string, [number, number]>}
*/
// @ts-ignore TypeScript bug
class TimedCounter extends Map {
/**
* Increments the number of times an action has been committed by one, and
* updates the delta of time since it was last committed.
*
* @param {string} key
* @param {number} timeLimit
* @return {[number, number]} - [action count, time delta]
*/
increment(key, timeLimit) {
let val = this.get(key);
let now = Date.now();
if (!val || now > val[1] + timeLimit) {
this.set(key, [1, Date.now()]);
return [1, 0];
} else {
val[0]++;
return [val[0], now - val[1]];
}
}
}
// Config.loglevel is:
// 0 = everything
// 1 = debug (same as 0 for now)
// 2 = notice (default)
// 3 = warning
// (4 is currently unused)
// 5 = supposedly completely silent, but for now a lot of PS output doesn't respect loglevel
if (('Config' in global) &&
(typeof Config.loglevel !== 'number' || Config.loglevel < 0 || Config.loglevel > 5)) {
Config.loglevel = 2;
}
// @ts-ignore
const Monitor = module.exports = {
/*********************************************************
* Logging
*********************************************************/
/**
* @param {string} text
*/
log(text) {
this.notice(text);
if (Rooms('staff')) {
Rooms('staff').add(`|c|~|${text}`).update();
}
},
/**
* @param {string} text
*/
adminlog(text) {
this.notice(text);
if (Rooms('upperstaff')) {
Rooms('upperstaff').add(`|c|~|${text}`).update();
}
},
/**
* @param {string} text
*/
logHTML(text) {
this.notice(text);
if (Rooms('staff')) {
Rooms('staff').add(`|html|${text}`).update();
}
},
/**
* @param {string} text
*/
debug(text) {
if (Config.loglevel <= 1) console.log(text);
},
/**
* @param {string} text
*/
warn(text) {
if (Config.loglevel <= 3) console.log(text);
},
/**
* @param {string} text
*/
notice(text) {
if (Config.loglevel <= 2) console.log(text);
},
/*********************************************************
* Resource Monitor
*********************************************************/
clean() {
this.clearNetworkUse();
this.battlePreps.clear();
this.battles.clear();
this.connections.clear();
Dnsbl.cache.clear();
},
connections: new TimedCounter(),
battles: new TimedCounter(),
battlePreps: new TimedCounter(),
groupChats: new TimedCounter(),
/** @type {?string} */
activeIp: null,
networkUse: {},
networkCount: {},
hotpatchLock: false,
/**
* Counts a connection. Returns true if the connection should be terminated for abuse.
*
* @param {string} ip
* @param {string} [name = '']
* @return {boolean}
*/
countConnection(ip, name = '') {
let [count, duration] = this.connections.increment(ip, 30 * 60 * 1000);
if (count === 500) {
this.adminlog(`[ResourceMonitor] IP ${ip} banned for cflooding (${count} times in ${Chat.toDurationString(duration)}${name ? `: ${name}` : ''})`);
return true;
}
if (count > 500) {
if (count % 500 === 0) {
let c = count / 500;
if (c === 2 || c === 4 || c === 10 || c === 20 || c % 40 === 0) {
this.adminlog(`[ResourceMonitor] IP ${ip} still cflooding (${count} times in ${Chat.toDurationString(duration)}${name ? `: ${name}` : ''})`);
}
}
return true;
}
return false;
},
/**
* Counts battles created. Returns true if the connection should be
* terminated for abuse.
*
* @param {string} ip
* @param {string} [name = '']
* @return {boolean}
*/
countBattle(ip, name = '') {
let [count, duration] = this.battles.increment(ip, 30 * 60 * 1000);
if (duration < 5 * 60 * 1000 && count % 30 === 0) {
this.adminlog(`[ResourceMonitor] IP ${ip} has battled ${count} times in the last ${Chat.toDurationString(duration)}${name ? `: name` : ''})`);
return true;
}
if (count % 150 === 0) {
this.adminlog('[ResourceMonitor] IP ' + ip + ' has battled ' + count + ' times in the last ' + Chat.toDurationString(duration) + name);
return true;
}
return false;
},
/**
* Counts team validations. Returns true if too many.
*
* @param {string} ip
* @param {Connection} connection
* @return {boolean}
*/
countPrepBattle(ip, connection) {
let count = this.battlePreps.increment(ip, 3 * 60 * 1000)[0];
if (count <= 12) return false;
if (count < 120 && Punishments.sharedIps.has(ip)) return false;
connection.popup('Due to high load, you are limited to 12 battles and team validations every 3 minutes.');
return true;
},
/**
* Counts concurrent battles. Returns true if too many.
*
* @param {number} count
* @param {Connection} connection
* @return {boolean}
*/
countConcurrentBattle(count, connection) {
if (count <= 5) return false;
connection.popup(`Due to high load, you are limited to 5 games at the same time.`);
return true;
},
/**
* Counts group chat creation. Returns true if too much.
*
* @param {string} ip
* @return {boolean}
*/
countGroupChat(ip) {
let count = this.groupChats.increment(ip, 60 * 60 * 1000)[0];
return count > 4;
},
/**
* Counts the data length received by the last connection to send a
* message, as well as the data length in the server's response.
*
* @param {number} size
*/
countNetworkUse(size) {
if (Config.emergency && this.activeIp) {
if (this.activeIp in this.networkUse) {
this.networkUse[this.activeIp] += size;
this.networkCount[this.activeIp]++;
} else {
this.networkUse[this.activeIp] = size;
this.networkCount[this.activeIp] = 1;
}
}
},
writeNetworkUse() {
let buf = '';
for (let i in this.networkUse) {
buf += `${this.networkUse[i]}\t${this.networkCount[i]}\t${i}\n`;
}
FS('logs/networkuse.tsv').write(buf);
},
clearNetworkUse() {
if (Config.emergency) {
this.networkUse = {};
this.networkCount = {};
}
},
/**
* Counts roughly the size of an object to have an idea of the server load.
*
* @param {any} object
* @return {number}
*/
sizeOfObject(object) {
/** @type {Set<(Array | Object)>} */
let objectCache = new Set();
let stack = [object];
let bytes = 0;
while (stack.length) {
let value = stack.pop();
switch (typeof value) {
case 'boolean':
bytes += 4;
break;
case 'string':
bytes += value.length * 2;
break;
case 'number':
bytes += 8;
break;
case 'object':
if (!objectCache.has(value)) objectCache.add(value);
if (Array.isArray(value)) {
for (let el of value) stack.push(el);
} else {
for (let i in value) stack.push(value[i]);
}
break;
}
}
return bytes;
},
/** @type {{new(entries: [any, [number, number]]): TimedCounter}} */
TimedCounter,
};
Monitor.cleanInterval = setInterval(() => Monitor.clean(), MONITOR_CLEAN_TIMEOUT);