Spaces:
Paused
Paused
| /** | |
| * 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'; | |
| import * as dns from 'dns'; | |
| import { FS, Net, Utils } from '../lib'; | |
| export interface AddressRange { | |
| minIP: number; | |
| maxIP: number; | |
| host?: string; | |
| } | |
| function removeNohost(hostname: string) { | |
| // Convert from old domain.tld.type-nohost format to new domain.tld?/type format | |
| if (hostname?.includes('-nohost')) { | |
| const parts = hostname.split('.'); | |
| const suffix = parts.pop(); | |
| return `${parts.join('.')}?/${suffix?.replace('-nohost', '')}`; | |
| } | |
| return hostname; | |
| } | |
| export const IPTools = new class { | |
| readonly dnsblCache = new Map<string, string | null>([ | |
| ['127.0.0.1', null], | |
| ]); | |
| readonly connectionTestCache = new Map<string, boolean>(); | |
| readonly 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])$/; | |
| readonly 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]|\*)$/; | |
| readonly hostRegex = /^.+\..{2,}$/; | |
| async lookup(ip: string) { | |
| 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: string, callback: (val: string | null) => void, reversedIpDot: string, index: number) { | |
| if (index >= BLOCKLISTS.length) { | |
| // not in any blocklist | |
| IPTools.dnsblCache.set(ip, null); | |
| callback(null); | |
| return; | |
| } | |
| const blocklist = BLOCKLISTS[index]; | |
| dns.lookup(reversedIpDot + blocklist, 4, (err, res) => { | |
| if (!err) { | |
| // blocked | |
| IPTools.dnsblCache.set(ip, blocklist); | |
| callback(blocklist); | |
| return; | |
| } | |
| // not blocked, try next blocklist | |
| 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: string) { | |
| 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<string | null>((resolve, reject) => { | |
| IPTools.queryDnsblLoop(ip, resolve, reversedIpDot, 0); | |
| }); | |
| } | |
| /********************************************************* | |
| * IP parsing | |
| *********************************************************/ | |
| ipToNumber(ip: string) { | |
| ip = ip.trim(); | |
| if (ip.includes(':') && !ip.includes('.')) { | |
| // IPv6, which PS does not support | |
| 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 = Utils.parseExactInt(part); | |
| if (isNaN(partAsInt) || partAsInt < 0 || partAsInt > 255) return null; | |
| num += partAsInt; | |
| } | |
| return num; | |
| } | |
| numberToIP(num: number) { | |
| const ipParts: string[] = []; | |
| 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: string): AddressRange | null { | |
| 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 = Utils.parseExactInt(cidr.slice(index + 1)); | |
| // fun fact: IPTools fails if bits <= 1 because JavaScript | |
| // does << with signed int32s. | |
| 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: string): boolean { | |
| return IPTools.stringToRange(range) !== null; | |
| } | |
| stringToRange(this: void, range: string | null): AddressRange | null { | |
| if (!range) return null; | |
| if (range.endsWith('*')) { | |
| const parts = range.replace('.*', '').split('.'); | |
| if (parts.length > 3) return null; | |
| const [a, b, c] = parts; | |
| const minIP = IPTools.ipToNumber(`${a || '0'}.${b || '0'}.${c || '0'}.0`); | |
| const maxIP = IPTools.ipToNumber(`${a || '255'}.${b || '255'}.${c || '255'}.255`); | |
| if (minIP === null || maxIP === null) return null; | |
| return { minIP, maxIP }; | |
| } | |
| 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: AddressRange, sep = '-') { | |
| return `${this.numberToIP(range.minIP)}${sep}${this.numberToIP(range.maxIP)}`; | |
| } | |
| /****************************** | |
| * Range management functions * | |
| ******************************/ | |
| checkPattern(patterns: AddressRange[], num: number | null) { | |
| 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: string | string[]): (ip: string) => boolean { | |
| if (!rangeString?.length) return () => false; | |
| let ranges: AddressRange[] = []; | |
| if (typeof rangeString === 'string') { | |
| const rangePatterns = IPTools.stringToRange(rangeString); | |
| if (rangePatterns) ranges = [rangePatterns]; | |
| } else { | |
| ranges = rangeString.map(IPTools.stringToRange).filter(x => x) as AddressRange[]; | |
| } | |
| return (ip: string) => { | |
| const ipNumber = IPTools.ipToNumber(ip); | |
| return IPTools.checkPattern(ranges, ipNumber); | |
| }; | |
| } | |
| /** | |
| * Proxy and host management functions | |
| */ | |
| ranges: (AddressRange & { host: string })[] = []; | |
| singleIPOpenProxies = new Set<string>(); | |
| torProxyIps = new Set<string>(); | |
| proxyHosts = new Set<string>(); | |
| residentialHosts = new Set<string>(); | |
| mobileHosts = new Set<string>(); | |
| async loadHostsAndRanges() { | |
| const data = await FS(HOSTS_FILE).readIfExists() + await FS(PROXIES_FILE).readIfExists(); | |
| // Strip carriage returns for Windows compatibility | |
| 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; | |
| // Handle legacy data format | |
| 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}\n`; | |
| } | |
| for (const host of IPTools.proxyHosts) { | |
| proxiesData += `HOST,${host}\n`; | |
| } | |
| for (const host of IPTools.residentialHosts) { | |
| hostsData += `RESIDENTIAL,${host}\n`; | |
| } | |
| for (const host of IPTools.mobileHosts) { | |
| hostsData += `MOBILE,${host}\n`; | |
| } | |
| IPTools.sortRanges(); | |
| for (const range of IPTools.ranges) { | |
| const data = `RANGE,${IPTools.rangeToString(range, ',')}${range.host ? `,${range.host}` : ``}\n`; | |
| if (range.host?.endsWith('/proxy')) { | |
| proxiesData += data; | |
| } else { | |
| hostsData += data; | |
| } | |
| } | |
| void FS(HOSTS_FILE).write(hostsData); | |
| void FS(PROXIES_FILE).write(proxiesData); | |
| } | |
| addOpenProxies(ips: string[]) { | |
| for (const ip of ips) { | |
| IPTools.singleIPOpenProxies.add(ip); | |
| } | |
| return IPTools.saveHostsAndRanges(); | |
| } | |
| addProxyHosts(hosts: string[]) { | |
| for (const host of hosts) { | |
| IPTools.proxyHosts.add(host); | |
| } | |
| return IPTools.saveHostsAndRanges(); | |
| } | |
| addMobileHosts(hosts: string[]) { | |
| for (const host of hosts) { | |
| IPTools.mobileHosts.add(host); | |
| } | |
| return IPTools.saveHostsAndRanges(); | |
| } | |
| addResidentialHosts(hosts: string[]) { | |
| for (const host of hosts) { | |
| IPTools.residentialHosts.add(host); | |
| } | |
| return IPTools.saveHostsAndRanges(); | |
| } | |
| removeOpenProxies(ips: string[]) { | |
| for (const ip of ips) { | |
| IPTools.singleIPOpenProxies.delete(ip); | |
| } | |
| return IPTools.saveHostsAndRanges(); | |
| } | |
| removeResidentialHosts(hosts: string[]) { | |
| for (const host of hosts) { | |
| IPTools.residentialHosts.delete(host); | |
| } | |
| return IPTools.saveHostsAndRanges(); | |
| } | |
| removeProxyHosts(hosts: string[]) { | |
| for (const host of hosts) { | |
| IPTools.proxyHosts.delete(host); | |
| } | |
| return IPTools.saveHostsAndRanges(); | |
| } | |
| removeMobileHosts(hosts: string[]) { | |
| for (const host of hosts) { | |
| IPTools.mobileHosts.delete(host); | |
| } | |
| return IPTools.saveHostsAndRanges(); | |
| } | |
| rangeIntersects(a: AddressRange, b: AddressRange) { | |
| try { | |
| this.checkRangeConflicts(a, [b]); | |
| } catch { | |
| return true; | |
| } | |
| return false; | |
| } | |
| checkRangeConflicts(insertion: AddressRange, sortedRanges: AddressRange[], widen?: boolean) { | |
| 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})\n` + | |
| `Intersects with: ${IPTools.rangeToString(next)} (${next.host})` | |
| ); | |
| } | |
| if (insertion.maxIP >= next.minIP) { | |
| throw new Error( | |
| `Could not insert: ${IPTools.rangeToString(insertion)} ${insertion.host}\n` + | |
| `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})\n` + | |
| `Intersects with: ${IPTools.rangeToString(prev)} (${prev.host})` | |
| ); | |
| } | |
| if (insertion.minIP <= prev.maxIP) { | |
| throw new Error( | |
| `Could not insert: ${IPTools.rangeToString(insertion)} (${insertion.host})\n` + | |
| `Intersects with: ${IPTools.rangeToString(prev)} (${prev.host})` | |
| ); | |
| } | |
| } | |
| } | |
| /********************************************************* | |
| * Range handling functions | |
| *********************************************************/ | |
| urlToHost(url: string) { | |
| 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() { | |
| Utils.sortBy(IPTools.ranges, range => range.minIP); | |
| } | |
| getRange(minIP: number, maxIP: number) { | |
| for (const range of IPTools.ranges) { | |
| if (range.minIP === minIP && range.maxIP === maxIP) return range; | |
| } | |
| } | |
| addRange(range: AddressRange & { host: string }) { | |
| if (IPTools.getRange(range.minIP, range.maxIP)) { | |
| IPTools.removeRange(range.minIP, range.maxIP); | |
| } | |
| IPTools.ranges.push(range); | |
| return IPTools.saveHostsAndRanges(); | |
| } | |
| removeRange(minIP: number, maxIP: number) { | |
| 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: string) { | |
| return new Promise<string>(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: string, callback: (result: boolean) => void) { | |
| const cachedValue = this.connectionTestCache.get(ip); | |
| if (cachedValue !== undefined) { | |
| return callback(cachedValue); | |
| } | |
| // Node.js's documentation does not make this easy to write. I discovered | |
| // this behavior by manual testing: | |
| // A successful connection emits 'connect', which you should react to | |
| // with socket.destroy(), which emits 'close'. | |
| // Some IPs instantly reject connections, emitting 'error' followed | |
| // immediately by 'close'. | |
| // Some IPs just never respond, leaving you to time out. Node will | |
| // emit the 'timeout' event, but not actually do anything else, leaving | |
| // you to manually use socket.destroy(), which emits 'close' | |
| let connected = false; | |
| const socket = require('net').createConnection({ | |
| port: 80, | |
| host: ip, | |
| timeout: 1000, | |
| }, () => { | |
| 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: string) { | |
| if (host.split('.').pop()?.includes('/')) return host; // It has a suffix, e.g. leaseweb.com?/proxy | |
| 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: string, ip: string) { | |
| if (Punishments.isSharedIp(ip)) { | |
| return 'shared'; | |
| } | |
| if (this.singleIPOpenProxies.has(ip) || this.torProxyIps.has(ip)) { | |
| // single-IP open proxies | |
| return 'proxy'; | |
| } | |
| if (/^he\.net(\?|)\/proxy$/.test(host)) { | |
| // Known to only be VPN services | |
| if (['74.82.60.', '72.52.87.', '65.49.126.'].some(range => ip.startsWith(range))) { | |
| return 'proxy'; | |
| } | |
| // Hurricane Electric has an annoying habit of having residential | |
| // internet and datacenters on the same IP ranges - we get a lot of | |
| // legitimate users as well as spammers on VPNs from HE. | |
| // This splits the difference and treats it like any other unknown IP. | |
| return 'unknown'; | |
| } | |
| // There were previously special cases for | |
| // 'digitalocean.proxy-nohost', 'servihosting.es.proxy-nohost' | |
| // DO is commonly used to host bots; I don't know who whitelisted | |
| // servihosting but I assume for a similar reason. This isn't actually | |
| // tenable; any service that can host bots can and does also host proxies. | |
| 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)) { | |
| // OVH | |
| return 'proxy'; | |
| } | |
| if (host.endsWith('/unknown')) { | |
| // rdns entry doesn't exist, and IP doesn't respond to a probe on port 80 | |
| return 'unknown'; | |
| } | |
| // rdns entry exists but is unrecognized | |
| return 'res?'; | |
| } | |
| async updateTorRanges() { | |
| try { | |
| const raw = await 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: AddressRange & { host: string } = { | |
| minIP: IPTools.ipToNumber("101.160.0.0")!, | |
| maxIP: IPTools.ipToNumber("101.191.255.255")!, | |
| host: 'telstra.net?/res', | |
| }; | |
| export default IPTools; | |
| void IPTools.updateTorRanges(); | |