"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