"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var ip_tools_exports = {}; __export(ip_tools_exports, { IPTools: () => IPTools, default: () => ip_tools_default }); module.exports = __toCommonJS(ip_tools_exports); var dns = __toESM(require("dns")); var import_lib = require("../lib"); /** * IP Tools * Pokemon Showdown - http://pokemonshowdown.com/ * * IPTools file has various tools for IP parsing and IP-based blocking. * * These include DNSBLs: DNS-based blackhole lists, which list IPs known for * running proxies, spamming, or other abuse. * * We also maintain our own database of datacenter IP ranges (usually * proxies). These are taken from https://github.com/client9/ipcat * but include our own database as well. * * @license MIT */ const BLOCKLISTS = ["sbl.spamhaus.org", "rbl.efnetrbl.org"]; const HOSTS_FILE = "config/hosts.csv"; const PROXIES_FILE = "config/proxies.csv"; function removeNohost(hostname) { if (hostname?.includes("-nohost")) { const parts = hostname.split("."); const suffix = parts.pop(); return `${parts.join(".")}?/${suffix?.replace("-nohost", "")}`; } return hostname; } const IPTools = new class { constructor() { this.dnsblCache = /* @__PURE__ */ new Map([ ["127.0.0.1", null] ]); this.connectionTestCache = /* @__PURE__ */ new Map(); this.ipRegex = /^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/; this.ipRangeRegex = /^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]|\*)){0,2}\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]|\*)$/; this.hostRegex = /^.+\..{2,}$/; /** * Proxy and host management functions */ this.ranges = []; this.singleIPOpenProxies = /* @__PURE__ */ new Set(); this.torProxyIps = /* @__PURE__ */ new Set(); this.proxyHosts = /* @__PURE__ */ new Set(); this.residentialHosts = /* @__PURE__ */ new Set(); this.mobileHosts = /* @__PURE__ */ new Set(); } async lookup(ip) { const [dnsbl, host] = await Promise.all([ IPTools.queryDnsbl(ip), IPTools.getHost(ip) ]); const shortHost = this.shortenHost(host); const hostType = this.getHostType(shortHost, ip); return { dnsbl, host, shortHost, hostType }; } queryDnsblLoop(ip, callback, reversedIpDot, index) { if (index >= BLOCKLISTS.length) { IPTools.dnsblCache.set(ip, null); callback(null); return; } const blocklist = BLOCKLISTS[index]; dns.lookup(reversedIpDot + blocklist, 4, (err, res) => { if (!err) { IPTools.dnsblCache.set(ip, blocklist); callback(blocklist); return; } IPTools.queryDnsblLoop(ip, callback, reversedIpDot, index + 1); }); } /** * IPTools.queryDnsbl(ip, callback) * * Calls callb * ack(blocklist), where blocklist is the blocklist domain * if the passed IP is in a blocklist, or null if the IP is not in * any blocklist. * * Return value matches isBlocked when treated as a boolean. */ queryDnsbl(ip) { if (!Config.dnsbl) return Promise.resolve(null); if (IPTools.dnsblCache.has(ip)) { return Promise.resolve(IPTools.dnsblCache.get(ip) || null); } const reversedIpDot = ip.split(".").reverse().join(".") + "."; return new Promise((resolve, reject) => { IPTools.queryDnsblLoop(ip, resolve, reversedIpDot, 0); }); } /********************************************************* * IP parsing *********************************************************/ ipToNumber(ip) { ip = ip.trim(); if (ip.includes(":") && !ip.includes(".")) { return null; } if (ip.startsWith("::ffff:")) ip = ip.slice(7); else if (ip.startsWith("::")) ip = ip.slice(2); let num = 0; const parts = ip.split("."); if (parts.length !== 4) return null; for (const part of parts) { num *= 256; const partAsInt = import_lib.Utils.parseExactInt(part); if (isNaN(partAsInt) || partAsInt < 0 || partAsInt > 255) return null; num += partAsInt; } return num; } numberToIP(num) { const ipParts = []; if (num < 0 || num >= 256 ** 4 || num !== Math.trunc(num)) return null; while (num) { const part = num % 256; num = (num - part) / 256; ipParts.unshift(part.toString()); } while (ipParts.length < 4) ipParts.unshift("0"); if (ipParts.length !== 4) return null; return ipParts.join("."); } getCidrRange(cidr) { if (!cidr) return null; const index = cidr.indexOf("/"); if (index <= 0) { const ip = IPTools.ipToNumber(cidr); if (ip === null) return null; return { minIP: ip, maxIP: ip }; } const low = IPTools.ipToNumber(cidr.slice(0, index)); const bits = import_lib.Utils.parseExactInt(cidr.slice(index + 1)); if (low === null || !bits || bits < 2 || bits > 32) return null; const high = low + (1 << 32 - bits) - 1; return { minIP: low, maxIP: high }; } /** Is this an IP range supported by `stringToRange`? Note that exact IPs are also valid IP ranges. */ isValidRange(range) { return IPTools.stringToRange(range) !== null; } stringToRange(range) { if (!range) return null; if (range.endsWith("*")) { const parts = range.replace(".*", "").split("."); if (parts.length > 3) return null; const [a, b, c] = parts; const minIP2 = IPTools.ipToNumber(`${a || "0"}.${b || "0"}.${c || "0"}.0`); const maxIP2 = IPTools.ipToNumber(`${a || "255"}.${b || "255"}.${c || "255"}.255`); if (minIP2 === null || maxIP2 === null) return null; return { minIP: minIP2, maxIP: maxIP2 }; } const index = range.indexOf("-"); if (index <= 0) { if (range.includes("/")) return IPTools.getCidrRange(range); const ip = IPTools.ipToNumber(range); if (ip === null) return null; return { maxIP: ip, minIP: ip }; } const minIP = IPTools.ipToNumber(range.slice(0, index)); const maxIP = IPTools.ipToNumber(range.slice(index + 1)); if (minIP === null || maxIP === null || maxIP < minIP) return null; return { minIP, maxIP }; } rangeToString(range, sep = "-") { return `${this.numberToIP(range.minIP)}${sep}${this.numberToIP(range.maxIP)}`; } /****************************** * Range management functions * ******************************/ checkPattern(patterns, num) { if (num === null) return false; for (const pattern of patterns) { if (num >= pattern.minIP && num <= pattern.maxIP) { return true; } } return false; } /** * Returns a checker function for the passed IP range or array of * ranges. The checker function returns true if its passed IP is * in the range. */ checker(rangeString) { if (!rangeString?.length) return () => false; let ranges = []; if (typeof rangeString === "string") { const rangePatterns = IPTools.stringToRange(rangeString); if (rangePatterns) ranges = [rangePatterns]; } else { ranges = rangeString.map(IPTools.stringToRange).filter((x) => x); } return (ip) => { const ipNumber = IPTools.ipToNumber(ip); return IPTools.checkPattern(ranges, ipNumber); }; } async loadHostsAndRanges() { const data = await (0, import_lib.FS)(HOSTS_FILE).readIfExists() + await (0, import_lib.FS)(PROXIES_FILE).readIfExists(); const rows = data.split("\n").map((row) => row.replace("\r", "")); const ranges = []; for (const row of rows) { if (!row) continue; let [type, hostOrLowIP, highIP, host] = row.split(","); if (!hostOrLowIP) continue; host = removeNohost(host); hostOrLowIP = removeNohost(hostOrLowIP); switch (type) { case "IP": IPTools.singleIPOpenProxies.add(hostOrLowIP); break; case "HOST": IPTools.proxyHosts.add(hostOrLowIP); break; case "RESIDENTIAL": IPTools.residentialHosts.add(hostOrLowIP); break; case "MOBILE": IPTools.mobileHosts.add(hostOrLowIP); break; case "RANGE": if (!host) continue; const minIP = IPTools.ipToNumber(hostOrLowIP); if (minIP === null) { Monitor.error(`Bad IP address in host or proxy file: '${hostOrLowIP}'`); continue; } const maxIP = IPTools.ipToNumber(highIP); if (maxIP === null) { Monitor.error(`Bad IP address in host or proxy file: '${highIP}'`); continue; } const range = { host: IPTools.urlToHost(host), maxIP, minIP }; if (range.maxIP < range.minIP) throw new Error(`Bad range at ${hostOrLowIP}.`); ranges.push(range); break; } } IPTools.ranges = ranges; IPTools.sortRanges(); } saveHostsAndRanges() { let hostsData = ""; let proxiesData = ""; for (const ip of IPTools.singleIPOpenProxies) { proxiesData += `IP,${ip} `; } for (const host of IPTools.proxyHosts) { proxiesData += `HOST,${host} `; } for (const host of IPTools.residentialHosts) { hostsData += `RESIDENTIAL,${host} `; } for (const host of IPTools.mobileHosts) { hostsData += `MOBILE,${host} `; } IPTools.sortRanges(); for (const range of IPTools.ranges) { const data = `RANGE,${IPTools.rangeToString(range, ",")}${range.host ? `,${range.host}` : ``} `; if (range.host?.endsWith("/proxy")) { proxiesData += data; } else { hostsData += data; } } void (0, import_lib.FS)(HOSTS_FILE).write(hostsData); void (0, import_lib.FS)(PROXIES_FILE).write(proxiesData); } addOpenProxies(ips) { for (const ip of ips) { IPTools.singleIPOpenProxies.add(ip); } return IPTools.saveHostsAndRanges(); } addProxyHosts(hosts) { for (const host of hosts) { IPTools.proxyHosts.add(host); } return IPTools.saveHostsAndRanges(); } addMobileHosts(hosts) { for (const host of hosts) { IPTools.mobileHosts.add(host); } return IPTools.saveHostsAndRanges(); } addResidentialHosts(hosts) { for (const host of hosts) { IPTools.residentialHosts.add(host); } return IPTools.saveHostsAndRanges(); } removeOpenProxies(ips) { for (const ip of ips) { IPTools.singleIPOpenProxies.delete(ip); } return IPTools.saveHostsAndRanges(); } removeResidentialHosts(hosts) { for (const host of hosts) { IPTools.residentialHosts.delete(host); } return IPTools.saveHostsAndRanges(); } removeProxyHosts(hosts) { for (const host of hosts) { IPTools.proxyHosts.delete(host); } return IPTools.saveHostsAndRanges(); } removeMobileHosts(hosts) { for (const host of hosts) { IPTools.mobileHosts.delete(host); } return IPTools.saveHostsAndRanges(); } rangeIntersects(a, b) { try { this.checkRangeConflicts(a, [b]); } catch { return true; } return false; } checkRangeConflicts(insertion, sortedRanges, widen) { if (insertion.maxIP < insertion.minIP) { throw new Error( `Invalid data for address range ${IPTools.rangeToString(insertion)} (${insertion.host})` ); } let iMin = 0; let iMax = sortedRanges.length; while (iMin < iMax) { const i = Math.floor((iMax + iMin) / 2); if (insertion.minIP > sortedRanges[i].minIP) { iMin = i + 1; } else { iMax = i; } } if (iMin < sortedRanges.length) { const next = sortedRanges[iMin]; if (insertion.minIP === next.minIP && insertion.maxIP === next.maxIP) { throw new Error(`The address range ${IPTools.rangeToString(insertion)} (${insertion.host}) already exists`); } if (insertion.minIP <= next.minIP && insertion.maxIP >= next.maxIP) { if (widen) { if (sortedRanges[iMin + 1]?.minIP <= insertion.maxIP) { throw new Error("You can only widen one address range at a time."); } return iMin; } throw new Error( `Too wide: ${IPTools.rangeToString(insertion)} (${insertion.host}) Intersects with: ${IPTools.rangeToString(next)} (${next.host})` ); } if (insertion.maxIP >= next.minIP) { throw new Error( `Could not insert: ${IPTools.rangeToString(insertion)} ${insertion.host} Intersects with: ${IPTools.rangeToString(next)} (${next.host})` ); } } if (iMin > 0) { const prev = sortedRanges[iMin - 1]; if (insertion.minIP >= prev.minIP && insertion.maxIP <= prev.maxIP) { throw new Error( `Too narrow: ${IPTools.rangeToString(insertion)} (${insertion.host}) Intersects with: ${IPTools.rangeToString(prev)} (${prev.host})` ); } if (insertion.minIP <= prev.maxIP) { throw new Error( `Could not insert: ${IPTools.rangeToString(insertion)} (${insertion.host}) Intersects with: ${IPTools.rangeToString(prev)} (${prev.host})` ); } } } /********************************************************* * Range handling functions *********************************************************/ urlToHost(url) { if (url.startsWith("http://")) url = url.slice(7); if (url.startsWith("https://")) url = url.slice(8); if (url.startsWith("www.")) url = url.slice(4); const slashIndex = url.indexOf("/"); if (slashIndex > 0 && url[slashIndex - 1] !== "?") url = url.slice(0, slashIndex); return url; } sortRanges() { import_lib.Utils.sortBy(IPTools.ranges, (range) => range.minIP); } getRange(minIP, maxIP) { for (const range of IPTools.ranges) { if (range.minIP === minIP && range.maxIP === maxIP) return range; } } addRange(range) { if (IPTools.getRange(range.minIP, range.maxIP)) { IPTools.removeRange(range.minIP, range.maxIP); } IPTools.ranges.push(range); return IPTools.saveHostsAndRanges(); } removeRange(minIP, maxIP) { IPTools.ranges = IPTools.ranges.filter((dc) => dc.minIP !== minIP || dc.maxIP !== maxIP); return IPTools.saveHostsAndRanges(); } /** * Will not reject; IPs with no RDNS entry will resolve to * '[byte1].[byte2]?/unknown'. */ getHost(ip) { return new Promise((resolve) => { if (!ip) { resolve(""); return; } const ipNumber = IPTools.ipToNumber(ip); if (ipNumber === null) throw new Error(`Bad IP address: '${ip}'`); for (const range of IPTools.ranges) { if (ipNumber >= range.minIP && ipNumber <= range.maxIP) { resolve(range.host); return; } } dns.reverse(ip, (err, hosts) => { if (err) { resolve(`${ip.split(".").slice(0, 2).join(".")}?/unknown`); return; } if (!hosts?.[0]) { if (ip.startsWith("50.")) { resolve("comcast.net?/res"); } else if (ipNumber >= telstraRange.minIP && ipNumber <= telstraRange.maxIP) { resolve(telstraRange.host); } else { this.testConnection(ip, (result) => { if (result) { resolve(`${ip.split(".").slice(0, 2).join(".")}?/proxy`); } else { resolve(`${ip.split(".").slice(0, 2).join(".")}?/unknown`); } }); } } else { resolve(hosts[0]); } }); }); } /** * Does this IP respond to port 80? In theory, proxies are likely to * respond, while residential connections are likely to reject connections. * * Callback is guaranteed to be called exactly once, within a 1000ms * timeout. */ testConnection(ip, callback) { const cachedValue = this.connectionTestCache.get(ip); if (cachedValue !== void 0) { return callback(cachedValue); } let connected = false; const socket = require("net").createConnection({ port: 80, host: ip, timeout: 1e3 }, () => { connected = true; this.connectionTestCache.set(ip, true); socket.destroy(); return callback(true); }); socket.on("error", () => { }); socket.on("timeout", () => socket.destroy()); socket.on("close", () => { if (!connected) { this.connectionTestCache.set(ip, false); return callback(false); } }); } shortenHost(host) { if (host.split(".").pop()?.includes("/")) return host; let dotLoc = host.lastIndexOf("."); const tld = host.slice(dotLoc); if (tld === ".uk" || tld === ".au" || tld === ".br") dotLoc = host.lastIndexOf(".", dotLoc - 1); dotLoc = host.lastIndexOf(".", dotLoc - 1); return host.slice(dotLoc + 1); } /** * Host types: * - 'res' - normal residential ISP * - 'shared' - like res, but shared among many people: bans will have collateral damage * - 'mobile' - like res, but unstable IP (IP bans don't work) * - 'proxy' - datacenters, VPNs, proxy services, other untrustworthy sources * (note that bots will usually be hosted on these) * - 'res?' - likely res, but host not specifically whitelisted * - 'unknown' - no rdns entry, treat with suspicion */ getHostType(host, ip) { if (Punishments.isSharedIp(ip)) { return "shared"; } if (this.singleIPOpenProxies.has(ip) || this.torProxyIps.has(ip)) { return "proxy"; } if (/^he\.net(\?|)\/proxy$/.test(host)) { if (["74.82.60.", "72.52.87.", "65.49.126."].some((range) => ip.startsWith(range))) { return "proxy"; } return "unknown"; } if (this.proxyHosts.has(host) || host.endsWith("/proxy")) { return "proxy"; } if (this.residentialHosts.has(host) || host.endsWith("/res")) { return "res"; } if (this.mobileHosts.has(host) || host.endsWith("/mobile")) { return "mobile"; } if (/^ip-[0-9]+-[0-9]+-[0-9]+\.net$/.test(host) || /^ip-[0-9]+-[0-9]+-[0-9]+\.eu$/.test(host)) { return "proxy"; } if (host.endsWith("/unknown")) { return "unknown"; } return "res?"; } async updateTorRanges() { try { const raw = await (0, import_lib.Net)("https://check.torproject.org/torbulkexitlist").get(); const torIps = raw.split("\n"); for (const ip of torIps) { if (this.ipRegex.test(ip)) { this.torProxyIps.add(ip); } } } catch { } } }(); const telstraRange = { minIP: IPTools.ipToNumber("101.160.0.0"), maxIP: IPTools.ipToNumber("101.191.255.255"), host: "telstra.net?/res" }; var ip_tools_default = IPTools; void IPTools.updateTorRanges(); //# sourceMappingURL=ip-tools.js.map