pokemon-showdown/server/chat-plugins/hosts.ts
2020-12-22 15:13:36 -08:00

497 lines
19 KiB
TypeScript

/**
* Chat plugin to help manage hosts, proxies, and datacenters
* Written by Annika
* Original /adddatacenters command written by Zarel
*/
import {Utils} from "../../lib/utils";
import {AddressRange} from "../ip-tools";
import {GlobalPermission} from "../user-groups";
const HOST_SUFFIXES = ['res', 'proxy', 'mobile'];
const WHITELISTED_USERIDS: ID[] = [];
function checkCanPerform(context: PageContext | CommandContext, user: User, permission: GlobalPermission = 'lockdown') {
if (!WHITELISTED_USERIDS.includes(user.id)) context.checkCan(permission);
}
export function visualizeRangeList(ranges: AddressRange[]) {
let html = `<tr><th>Lowest IP address</th><th>Highest IP address</th><th>Host</th></tr>`;
for (const range of ranges) {
html += `<tr>`;
html += `<td>${IPTools.numberToIP(range.minIP)}</td>`;
html += `<td>${IPTools.numberToIP(range.maxIP)}</td>`;
html += Utils.html`<td>${range.host}</td>`;
html += `</tr>`;
}
return html;
}
function formatRange(range: AddressRange, includeModlogBrackets?: boolean) {
const startBracket = includeModlogBrackets ? '[' : '';
const endBracket = includeModlogBrackets ? ']' : '';
let result = `${startBracket}${IPTools.numberToIP(range.minIP)}${endBracket}`;
result += `-${startBracket}${IPTools.numberToIP(range.maxIP)}${endBracket}`;
if (range.host) result += ` (${range.host})`;
return result;
}
export const pages: PageTable = {
proxies(query, user) {
this.title = "[Proxies]";
checkCanPerform(this, user, 'globalban');
const openProxies = [...IPTools.singleIPOpenProxies];
const proxyHosts = [...IPTools.proxyHosts];
openProxies.sort(IPTools.ipSort);
proxyHosts.sort();
IPTools.sortRanges();
let html = `<div class="ladder pad"><h2>Single IP proxies:</h2><table><tr><th>IP</th><th>Type</th></tr>`;
for (const proxyIP of openProxies) {
html += `<tr><td>${proxyIP}</td><td>Single IP open proxy</td></tr>`;
}
html += `</table></div>`;
html += `<div class="ladder pad"><h2>Proxy hosts:</h2><table><tr><th>Host</th><th>Type</th></tr>`;
for (const proxyHost of proxyHosts) {
html += `<tr><td>${proxyHost}</td><td>Proxy host</td></tr>`;
}
html += `</table></div>`;
html += `<div class="ladder pad"><h2>Proxy IP Ranges:</h2><table>`;
html += visualizeRangeList(IPTools.ranges.filter(r => r.host?.endsWith('/proxy')));
html += `</table></div>`;
return html;
},
hosts(query, user) {
this.title = "[Hosts]";
checkCanPerform(this, user, 'globalban');
const type = toID(query[0]) || 'all';
IPTools.sortRanges();
const mobileHosts = ['all', 'mobile'].includes(type) ? [...IPTools.mobileHosts] : [];
const residentialHosts = ['all', 'residential', 'res'].includes(type) ? [...IPTools.residentialHosts] : [];
const hostRanges = ['all', 'ranges', 'isps'].includes(type) ?
IPTools.ranges.filter(r => r.host && !r.host.endsWith('/proxy')) :
[];
mobileHosts.sort();
residentialHosts.sort();
let html = `<div class="ladder pad"><h2>Mobile hosts:</h2><table><tr><th>Host</th><th>Type</th></tr>`;
for (const mobileHost of mobileHosts) {
html += `<tr><td>${mobileHost}</td><td>Mobile host</td></tr>`;
}
html += `</table></div>`;
html += `<div class="ladder pad"><h2>Residential hosts:</h2><table><tr><th>Host</th><th>Type</th></tr>`;
for (const resHost of residentialHosts) {
html += `<tr><td>${resHost}</td><td>Residential host</td></tr>`;
}
html += `</table></div>`;
html += `<div class="ladder pad"><h2>ISP IP Ranges:</h2><table>`;
html += visualizeRangeList(hostRanges);
html += `</table></div>`;
return html;
},
ranges(query, user) {
this.title = "[IP Ranges]";
checkCanPerform(this, user, 'globalban');
const type = toID(query[0]) || 'all';
IPTools.sortRanges();
let html = ``;
if (['all', 'mobile'].includes(type)) {
html += `<div class="ladder pad"><h2>Mobile IP Ranges:</h2><table>`;
html += visualizeRangeList(IPTools.ranges.filter(range => range.host?.endsWith('/mobile')));
html += `</table></div>`;
}
if (['all', 'res', 'residential'].includes(type)) {
html += `<div class="ladder pad"><h2>Residential IP Ranges:</h2><table>`;
html += visualizeRangeList(IPTools.ranges.filter(range => range.host?.endsWith('/res')));
html += `</table></div>`;
}
if (['all', 'proxy', 'proxies'].includes(type)) {
html += `<div class="ladder pad"><h2>Proxy IP Ranges:</h2><table>`;
html += visualizeRangeList(IPTools.ranges.filter(range => range.host?.endsWith('/proxy')));
html += `</table></div>`;
}
return html;
},
sharedipblacklist(args, user, connection) {
this.title = `[Shared IP Blacklist]`;
checkCanPerform(this, user, 'globalban');
let buf = `<div class="pad"><h2>IPs blocked from being marked as shared</h2>`;
if (!Punishments.sharedIpBlacklist.size) {
buf += `<p>None currently.</p>`;
} else {
buf += `<div class="ladder"><table><tr><th>IP</th><th>Reason</th></tr>`;
const sortedSharedIPBlacklist = [...Punishments.sharedIpBlacklist];
sortedSharedIPBlacklist.sort((a, b) => IPTools.ipSort(a[0], b[0]));
for (const [reason, ip] of sortedSharedIPBlacklist) {
buf += `<tr><td>${ip}</td><td>${reason}</td></tr>`;
}
buf += `</table></div>`;
}
buf += `</div>`;
return buf;
},
sharedips(args, user, connection) {
this.title = `[Shared IPs]`;
checkCanPerform(this, user, 'globalban');
let buf = `<div class="pad"><h2>IPs marked as shared</h2>`;
if (!Punishments.sharedIps.size) {
buf += `<p>None currently.</p>`;
} else {
buf += `<div class="ladder"><table><tr><th>IP</th><th>Location</th></tr>`;
const sortedSharedIPs = [...Punishments.sharedIps];
sortedSharedIPs.sort((a, b) => IPTools.ipSort(a[0], b[0]));
for (const [ip, location] of sortedSharedIPs) {
buf += `<tr><td>${ip}</td><td>${location}</td></tr>`;
}
buf += `</table></div>`;
}
buf += `</div>`;
return buf;
},
};
export const commands: ChatCommands = {
dc: 'ipranges',
datacenter: 'ipranges',
datacenters: 'ipranges',
iprange: 'ipranges',
ipranges: {
'': 'help',
help() {
return this.parse('/help ipranges');
},
show: 'view',
view(target, room, user) {
checkCanPerform(this, user, 'globalban');
const types = ['all', 'residential', 'res', 'mobile', 'proxy'];
const type = target ? toID(target) : 'all';
if (!types.includes(type)) {
return this.errorReply(`'${type}' isn't a valid host type. Specify one of ${types.join(', ')}.`);
}
return this.parse(`/join view-ranges-${type}`);
},
viewhelp: [
`/ipranges view - View the list of all IP ranges. Requires: hosts manager @ &`,
`/ipranges view [type] - View the list of a particular type of IP range ('residential', 'mobile', or 'proxy'). Requires: hosts manager @ &`,
],
// Originally by Zarel
widen: 'add',
add(target, room, user, connection, cmd) {
checkCanPerform(this, user, 'globalban');
if (!target) return this.parse('/help ipranges add');
// should be in the format: IP, IP, name, URL
const widen = cmd.includes('widen');
const [type, stringRange, host] = target.split(',').map(part => part.trim());
if (!host || !IPTools.hostRegex.test(host)) {
return this.errorReply(`Invalid data: ${target}`);
}
if (!HOST_SUFFIXES.includes(type)) {
return this.errorReply(`'${type}' is not a valid host type. Please specify one of ${HOST_SUFFIXES.join(', ')}.`);
}
const range = IPTools.stringToRange(stringRange);
if (!range) return this.errorReply(`Couldn't parse IP range '${stringRange}'.`);
range.host = `${IPTools.urlToHost(host)}?/${type}`;
IPTools.sortRanges();
let result;
try {
result = IPTools.checkRangeConflicts(range, IPTools.ranges, widen);
} catch (e) {
return this.errorReply(e.message);
}
if (typeof result === 'number') {
// Remove the range that is being widened
IPTools.removeRange(IPTools.ranges[result].minIP, IPTools.ranges[result].maxIP);
}
IPTools.addRange(range as AddressRange & {host: string});
this.privateGlobalModAction(`${user.name} added the IP range ${formatRange(range)} to the list of ${type} ranges.`);
this.globalModlog('IPRANGE ADD', null, formatRange(range, true));
},
addhelp: [
`/ipranges add [type], [low]-[high], [host] - Adds an IP range. Requires: hosts manager &`,
`/ipranges widen [type], [low]-[high], [host] - Adds an IP range, allowing a new range to completely cover an old range. Requires: hosts manager &`,
`For example: /ipranges add proxy, 5.152.192.0 - 5.152.223.255, redstation.com`,
`Get datacenter info from whois; [low], [high] are the range in the last inetnum; [type] is one of res, proxy, or mobile.`,
],
remove(target, room, user) {
checkCanPerform(this, user);
if (!target) return this.parse('/help ipranges remove');
const range = IPTools.stringToRange(target);
if (!range) return this.errorReply(`Couldn't parse the IP range '${target}'.`);
if (!IPTools.getRange(range.minIP, range.maxIP)) return this.errorReply(`No IP range found at '${target}'.`);
void IPTools.removeRange(range.minIP, range.maxIP);
this.privateGlobalModAction(`${user.name} removed the IP range ${formatRange(range)}.`);
this.globalModlog('IPRANGE REMOVE', null, formatRange(range, true));
},
removehelp: [
`/ipranges remove [low IP]-[high IP] - Removes an IP range. Requires: hosts manager &`,
`Example: /ipranges remove 5.152.192.0-5.152.223.255`,
],
rename(target, room, user) {
checkCanPerform(this, user);
if (!target) return this.parse('/help ipranges rename');
const [type, rangeString, url] = target.split(',').map(part => part.trim());
if (!url) {
return this.parse('/help ipranges rename');
}
const toRename = IPTools.stringToRange(rangeString);
if (!toRename) return this.errorReply(`Couldn't parse IP range '${rangeString}'.`);
const exists = IPTools.getRange(toRename.minIP, toRename.maxIP);
if (!exists) return this.errorReply(`No IP range found at '${rangeString}'.`);
const range = {
minIP: toRename.minIP,
maxIP: toRename.maxIP,
host: `${IPTools.urlToHost(url)}?/${type}`,
};
void IPTools.addRange(range);
this.privateGlobalModAction(`${user.name} renamed the IP range ${formatRange(toRename)} to ${range.host}.`);
this.globalModlog('IPRANGE RENAME', null, `IP range ${formatRange(toRename, true)} to ${range.host}`);
},
renamehelp: [
`/ipranges rename [type], [low IP]-[high IP], [host] - Changes the host an IP range resolves to. Requires: hosts manager &`,
],
},
iprangeshelp() {
const help = [
`<code>/ipranges view [type]</code>: view the list of a particular type of IP range (<code>residential</code>, <code>mobile</code>, or <code>proxy</code>). Requires: hosts manager @ &`,
`<code>/ipranges add [type], [low IP]-[high IP], [host]</code>: add IP ranges (can be multiline). Requires: hosts manager &</summary><code>/ipranges view</code>: view the list of all IP ranges. Requires: hosts manager @ &`,
`<code>/ipranges widen [type], [low IP]-[high IP], [host]</code>: add IP ranges, allowing a new range to completely cover an old range. Requires: hosts manager &`,
`For example: <code>/ipranges add proxy, 5.152.192.0-5.152.223.255, redstation.com</code>.`,
`Get datacenter info from <code>/whois</code>; <code>[low IP]</code>, <code>[high IP]</code> are the range in the last inetnum.`,
`<code>/ipranges remove [low IP]-[high IP]</code>: remove IP range(s). Can be multiline. Requires: hosts manager &`,
`For example: <code>/ipranges remove 5.152.192.0, 5.152.223.255</code>.`,
`<code>/ipranges rename [low IP]-[high IP], [host]</code>: changes the host an IP range resolves to. Requires: hosts manager &`,
];
return this.sendReply(`|html|<details class="readmore"><summary>${help.join('<br />')}`);
},
viewhosts(target, room, user) {
checkCanPerform(this, user, 'globalban');
const types = ['all', 'residential', 'mobile', 'ranges'];
const type = target ? toID(target) : 'all';
if (!types.includes(type)) {
return this.errorReply(`'${type}' isn't a valid host type. Specify one of ${types.join(', ')}.`);
}
return this.parse(`/join view-hosts-${type}`);
},
viewhostshelp: [
`/viewhosts - View the list of hosts. Requires: hosts manager @ &`,
`/viewhosts [type] - View the list of a particular type of host. Requires: hosts manager @ &`,
`Host types are: 'all', 'residential', 'mobile', and 'ranges'.`,
],
removehost: 'addhosts',
removehosts: 'addhosts',
addhost: 'addhosts',
addhosts(target, room, user, connection, cmd) {
checkCanPerform(this, user);
const removing = cmd.includes('remove');
let [type, ...hosts] = target.split(',');
type = toID(type);
hosts = hosts.map(host => host.trim());
if (!hosts.length) return this.parse('/help addhosts');
switch (type) {
case 'openproxy':
for (const host of hosts) {
if (!IPTools.ipRegex.test(host)) return this.errorReply(`'${host}' is not a valid IP address.`);
if (removing !== IPTools.singleIPOpenProxies.has(host)) {
return this.errorReply(`'${host}' is ${removing ? 'not' : 'already'} in the list of proxy IPs.`);
}
}
if (removing) {
void IPTools.removeOpenProxies(hosts);
} else {
void IPTools.addOpenProxies(hosts);
}
break;
case 'proxy':
for (const host of hosts) {
if (!IPTools.hostRegex.test(host)) return this.errorReply(`'${host}' is not a valid host.`);
if (removing !== IPTools.proxyHosts.has(host)) {
return this.errorReply(`'${host}' is ${removing ? 'not' : 'already'} in the list of proxy hosts.`);
}
}
if (removing) {
void IPTools.removeProxyHosts(hosts);
} else {
void IPTools.addProxyHosts(hosts);
}
break;
case 'residential':
for (const host of hosts) {
if (!IPTools.hostRegex.test(host)) return this.errorReply(`'${host}' is not a valid host.`);
if (removing !== IPTools.residentialHosts.has(host)) {
return this.errorReply(`'${host}' is ${removing ? 'not' : 'already'} in the list of residential hosts.`);
}
}
if (removing) {
void IPTools.removeResidentialHosts(hosts);
} else {
void IPTools.addResidentialHosts(hosts);
}
break;
case 'mobile':
for (const host of hosts) {
if (!IPTools.hostRegex.test(host)) return this.errorReply(`'${host}' is not a valid host.`);
if (removing !== IPTools.mobileHosts.has(host)) {
return this.errorReply(`'${host}' is ${removing ? 'not' : 'already'} in the list of mobile hosts.`);
}
}
if (removing) {
void IPTools.removeMobileHosts(hosts);
} else {
void IPTools.addMobileHosts(hosts);
}
break;
default:
return this.errorReply(`'${type}' isn't one of 'openproxy', 'proxy', 'residential', or 'mobile'.`);
}
this.privateGlobalModAction(
`${user.name} ${removing ? 'removed' : 'added'} ${hosts.length} hosts (${hosts.join(', ')}) to the ${type} category!`
);
this.globalModlog(removing ? 'REMOVEHOSTS' : 'ADDHOSTS', null, `${type}: ${hosts.join(', ')}`);
},
addhostshelp: [
`/addhosts [category], host1, host2, ... - Adds hosts to the given category. Requires: hosts manager &`,
`/removehosts [category], host1, host2, ... - Removes hosts from the given category. Requires: hosts manager &`,
`Categories are: 'openproxy' (which takes IP addresses, not hosts), 'proxy', 'residential', and 'mobile'.`,
],
viewproxies(target, room, user) {
checkCanPerform(this, user, 'globalban');
return this.parse('/join view-proxies');
},
viewproxieshelp: [
`/viewproxies - View the list of proxies. Requires: hosts manager @ &`,
],
markshared(target, room, user) {
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 (Punishments.sharedIps.has(ip)) return this.errorReply("This IP is already marked as shared.");
if (Punishments.sharedIpBlacklist.has(ip)) {
return this.errorReply(`This IP is blacklisted from being marked as shared.`);
}
if (!note) {
this.errorReply(`You must specify who owns this shared IP.`);
this.parse(`/help markshared`);
return;
}
Punishments.addSharedIp(ip, note);
this.privateGlobalModAction(`The IP '${ip}' was marked as shared by ${user.name}. (${note})`);
this.globalModlog('SHAREDIP', null, note, ip);
},
marksharedhelp: [
`/markshared [IP], [owner/organization of IP] - Marks an IP address as shared.`,
`Note: the owner/organization (i.e., University of Minnesota) of the shared IP is required. Requires @ &`,
],
unmarkshared(target, room, user) {
if (!target) return this.parse('/help unmarkshared');
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.");
Punishments.removeSharedIp(target);
this.privateGlobalModAction(`The IP '${target}' was unmarked as shared by ${user.name}.`);
this.globalModlog('UNSHAREDIP', null, null, target);
},
unmarksharedhelp: [`/unmarkshared [IP] - Unmarks a shared IP address. Requires @ &`],
marksharedblacklist: 'nomarkshared',
marksharedbl: 'nomarkshared',
nomarkshared: {
add(target, room, user) {
if (!target) return this.parse(`/help nomarkshared`);
checkCanPerform(this, user);
const [ip, ...reasonArr] = target.split(',');
if (!IPTools.ipRegex.test(ip)) return this.errorReply(`Please enter a valid IP address.`);
if (!reasonArr?.length) {
this.errorReply(`A reason is required.`);
this.parse(`/help nomarkshared`);
return;
}
if (Punishments.sharedIpBlacklist.has(ip)) {
return this.errorReply(`This IP is already blacklisted from being marked as shared.`);
}
if (Punishments.sharedIps.has(ip)) this.parse(`/unmarkshared ${ip}`);
const reason = reasonArr.join(',');
Punishments.addBlacklistedSharedIp(ip, reason);
this.privateGlobalModAction(`The IP '${ip}' was blacklisted from being marked as shared by ${user.name}.`);
this.globalModlog('SHAREDIP BLACKLIST', ip, reason.trim());
},
remove(target, room, user) {
if (!target) return this.parse(`/help nomarkshared`);
checkCanPerform(this, user);
if (!IPTools.ipRegex.test(target)) return this.errorReply(`Please enter a valid IP address.`);
if (!Punishments.sharedIpBlacklist.has(target)) {
return this.errorReply(`This IP is not blacklisted from being marked as shared.`);
}
Punishments.removeBlacklistedSharedIp(target);
this.privateGlobalModAction(`The IP '${target}' was unblacklisted from being marked as shared by ${user.name}.`);
this.globalModlog('SHAREDIP UNBLACKLIST', target);
},
view() {
return this.parse(`/join view-sharedipblacklist`);
},
help: '',
''() {
return this.parse(`/help nomarkshared`);
},
},
nomarksharedhelp: [
`/nomarkshared add [IP], [reason] - Prevents an IP from being marked as shared until it's removed from this list. Requires &`,
`Note: Reasons are required.`,
`/nomarkshared remove [IP] - Removes an IP from the nomarkshared list. Requires &`,
`/nomarkshared view - Lists all IPs prevented from being marked as shared. Requires @ &`,
],
sharedips: 'viewsharedips',
viewsharedips() {
return this.parse('/join view-sharedips');
},
viewsharedipshelp: [
`/viewsharedips — Lists IP addresses marked as shared. Requires: hosts manager @ &`,
],
};