/** * Chat plugin to help manage hosts, proxies, and datacenters * Written by Annika * Original /adddatacenters command written by Zarel */ import { Utils } from "../../lib"; import type { AddressRange } from "../ip-tools"; import type { GlobalPermission } from "../user-groups"; const HOST_SUFFIXES = ['res', 'proxy', 'mobile']; const SUFFIX_ALIASES: { [k: string]: string } = { residential: 'res', }; const WHITELISTED_USERIDS: ID[] = []; function checkCanPerform( context: Chat.PageContext | Chat.CommandContext, user: User, permission: GlobalPermission = 'lockdown' ) { if (!WHITELISTED_USERIDS.includes(user.id)) context.checkCan(permission); } function getHostType(type: string) { type = toID(type); if (HOST_SUFFIXES.includes(type)) return type; if (SUFFIX_ALIASES[type]) return SUFFIX_ALIASES[type]; throw new Chat.ErrorMessage(`'${type}' is not a valid host type. Please specify one of ${HOST_SUFFIXES.join(', ')}.`); } export function visualizeRangeList(ranges: AddressRange[]) { let html = `Lowest IP addressHighest IP addressHost`; for (const range of ranges) { html += ``; html += `${IPTools.numberToIP(range.minIP)}`; html += `${IPTools.numberToIP(range.maxIP)}`; html += Utils.html`${range.host}`; html += ``; } 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: Chat.PageTable = { proxies(query, user) { this.title = "[Proxies]"; checkCanPerform(this, user, 'globalban'); const openProxies = [...IPTools.singleIPOpenProxies]; const proxyHosts = [...IPTools.proxyHosts]; Utils.sortBy(openProxies, ip => { const number = IPTools.ipToNumber(ip); if (number === null) { Rooms.get('upperstaff')?.add(`|error|Invalid IP address in IPTools.singleIPOpenProxies: '${ip}'`); return -1; } return number; }); proxyHosts.sort(); IPTools.sortRanges(); let html = `

Single IP proxies:

`; for (const proxyIP of openProxies) { html += ``; } html += `
IPType
${proxyIP}Single IP open proxy
`; html += `

Proxy hosts:

`; for (const proxyHost of proxyHosts) { html += ``; } html += `
HostType
${proxyHost}Proxy host
`; html += `

Proxy IP Ranges:

`; html += visualizeRangeList(IPTools.ranges.filter(r => r.host?.endsWith('/proxy'))); html += `
`; 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 = `

Mobile hosts:

`; for (const mobileHost of mobileHosts) { html += ``; } html += `
HostType
${mobileHost}Mobile host
`; html += `

Residential hosts:

`; for (const resHost of residentialHosts) { html += ``; } html += `
HostType
${resHost}Residential host
`; html += `

ISP IP Ranges:

`; html += visualizeRangeList(hostRanges); html += `
`; 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 += `

Mobile IP Ranges:

`; html += visualizeRangeList(IPTools.ranges.filter(range => range.host?.endsWith('/mobile'))); html += `
`; } if (['all', 'res', 'residential'].includes(type)) { html += `

Residential IP Ranges:

`; html += visualizeRangeList(IPTools.ranges.filter(range => range.host?.endsWith('/res'))); html += `
`; } if (['all', 'proxy', 'proxies'].includes(type)) { html += `

Proxy IP Ranges:

`; html += visualizeRangeList(IPTools.ranges.filter(range => range.host?.endsWith('/proxy'))); html += `
`; } if (['all', 'openproxy'].includes(type)) { html += `

Single-IP Open Proxies:

`; for (const ip of IPTools.singleIPOpenProxies) { html += ``; } html += `
${ip}
`; } return html; }, sharedipblacklist(args, user, connection) { this.title = `[Shared IP Blacklist]`; checkCanPerform(this, user, 'lock'); let buf = `

IPs blocked from being marked as shared

`; if (!Punishments.sharedIpBlacklist.size) { buf += `

None currently.

`; } else { buf += `
`; const sortedSharedIPBlacklist = [...Punishments.sharedIpBlacklist]; Utils.sortBy(sortedSharedIPBlacklist, ([ipOrRange]) => { if (IPTools.ipRegex.test(ipOrRange)) { const number = IPTools.ipToNumber(ipOrRange); if (number === null) { Monitor.error(`Invalid blacklisted-from-markshared IP: '${ipOrRange}`); return -1; } return number; } return IPTools.stringToRange(ipOrRange)!.minIP; }); for (const [ip, reason] of sortedSharedIPBlacklist) { buf += ``; } buf += `
IPReason
${ip}${reason}
`; } buf += `
`; return buf; }, sharedips(args, user, connection) { this.title = `[Shared IPs]`; checkCanPerform(this, user, 'globalban'); let buf = `

IPs marked as shared

`; if (!Punishments.sharedIps.size) { buf += `

None currently.

`; } else { buf += `
`; const sortedSharedIPs = [...Punishments.sharedIps]; Utils.sortBy(sortedSharedIPs, ([ip]) => { const number = IPTools.ipToNumber(ip); if (number === null) { Monitor.error(`Invalid shared IP address: '${ip}'`); return -1; } return number; }); for (const [ip, location] of sortedSharedIPs) { buf += ``; } buf += `
IPLocation
${ip}${location}
`; } buf += `
`; return buf; }, }; export const commands: Chat.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', 'openproxy']; 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 [typeString, stringRange, host] = target.split(',').map(part => part.trim()); if (!host || !IPTools.hostRegex.test(host)) { return this.errorReply(`Invalid data: ${target}`); } const type = getHostType(typeString); 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: any) { 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 = [ `/ipranges view [type]: view the list of a particular type of IP range (residential, mobile, or proxy). Requires: hosts manager @ ~`, `/ipranges add [type], [low IP]-[high IP], [host]: add IP ranges (can be multiline). Requires: hosts manager ~/ipranges view: view the list of all IP ranges. Requires: hosts manager @ ~`, `/ipranges widen [type], [low IP]-[high IP], [host]: add IP ranges, 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 IP], [high IP] are the range in the last inetnum.`, `/ipranges remove [low IP]-[high IP]: remove IP range(s). Can be multiline. Requires: hosts manager ~`, `For example: /ipranges remove 5.152.192.0, 5.152.223.255.`, `/ipranges rename [type], [low IP]-[high IP], [host]: changes the host an IP range resolves to. Requires: hosts manager ~`, ]; return this.sendReply(`|html|
${help.join('
')}`); }, 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(', ')}) ${removing ? 'from' : '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)) { 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.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.`); } 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'); target = target.trim(); const pattern = IPTools.stringToRange(target); if (!pattern) return this.errorReply("Please enter a valid IP address."); if (pattern.minIP !== pattern.maxIP && !user.can('rangeban')) { return this.errorReply(`Only administrators can unmarkshare ranges.`); } let shared = false; if (pattern.minIP !== pattern.maxIP) { for (const range of Punishments.sharedRanges.keys()) { shared = range.minIP === pattern.minIP && range.maxIP === pattern.maxIP; if (shared) break; } } else { shared = Punishments.sharedIps.has(target); } if (!shared) return this.errorReply(`That IP/range 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, 'globalban'); const [ip, ...reasonArr] = target.split(','); if (!IPTools.ipRangeRegex.test(ip)) return this.errorReply(`Please enter a valid IP address or range.`); if (!reasonArr?.length) { this.errorReply(`A reason is required.`); this.parse(`/help nomarkshared`); return; } if (Punishments.isBlacklistedSharedIp(ip)) { return this.errorReply(`This IP is already blacklisted from being marked as shared.`); } // this works because we test ipRangeRegex above, which works for both ranges AND single ips. // so we know here this is one of the two. // If it doesn't work as a single IP, it's a range. if (!IPTools.ipRegex.test(ip)) { // if it doesn't end with *, it doesn't function as a range in IPTools#stringToRange, only as a single IP. // that's valid behavior, but it's detrimental here. if (!ip.endsWith('*')) { this.errorReply(`That looks like a range, but it is invalid.`); this.errorReply(`Append * to the end of the range and try again.`); return; } if (!user.can('bypassall')) { return this.errorReply(`Only Administrators can add ranges.`); } const range = IPTools.stringToRange(ip); if (!range) return this.errorReply(`Invalid IP range.`); for (const sharedIp of Punishments.sharedIps.keys()) { const ipNum = IPTools.ipToNumber(sharedIp); if (IPTools.checkPattern([range], ipNum)) { this.parse(`/unmarkshared ${sharedIp}`); } } } else { if (Punishments.isSharedIp(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', null, reason.trim(), ip); }, remove(target, room, user) { if (!target) return this.parse(`/help nomarkshared`); checkCanPerform(this, user); if (!IPTools.ipRangeRegex.test(target)) return this.errorReply(`Please enter a valid IP address or range.`); 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', null, null, 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 @ ~`, ], };