Jofthomas's picture
Upload 4781 files
5c2ed06 verified
"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