Spaces:
Running
Running
; | |
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 | |