/ src / services / ipRotatorService.js
ipRotatorService.js
   1  import fetch from "node-fetch";
   2  import { HttpsProxyAgent } from "https-proxy-agent";
   3  import WebSocket from "ws";
   4  import { logger } from "@/middleware/logging.js";
   5  import config from "@/config.js";
   6  import { SocksProxyAgent } from "socks-proxy-agent";
   7  import crypto from "crypto";
   8  import https from "https";
   9  import net from "net";
  10  import { getRandomUserAgent } from "@/utils/userAgentGenerator.js";
  11  
  12  class IpRotatorService {
  13    constructor() {
  14      this.tor = {
  15        available: false,
  16        controlAvailable: false,
  17        agent: null,
  18        currentExit: null,
  19        failures: 0,
  20        maliciousExits: new Set(),
  21      };
  22      this.proxyPool = [];
  23      this.deadProxies = new Map();
  24      this.proxyStats = new Map();
  25      this.realIp = null;
  26      this.blacklistedProxies = new Set();
  27      this.proxyFailureHistory = new Map();
  28      this.domainRateLimits = new Map();
  29      this.stats = {
  30        totalRequests: 0,
  31        successfulRequests: 0,
  32        failedRequests: 0,
  33        uniqueIps: new Set(),
  34        torUsed: 0,
  35        proxyUsed: 0,
  36        directUsed: 0,
  37        avgLatency: 0,
  38        poolHealth: 0,
  39      };
  40      this.isInitialized = false;
  41      this.refreshInterval = null;
  42      this.cleanupInterval = null;
  43      this.healthCheckInterval = null;
  44      this.monitoringInterval = null;
  45      this.lastHealthCheck = 0;
  46      this.requestCounts = new Map();
  47      this.sessionProxies = new Map();
  48      this.requestTimestamps = [];
  49      this.ipCheckUrls = config.ipRotator.ipCheckUrls;
  50      this.proxySources = config.ipRotator.proxySources;
  51      this.blacklistTimeout = config.ipRotator.blacklistTimeout;
  52      this.generalRateLimit = config.security.rateLimit.limit || 100;
  53      this.httpsAgent = new https.Agent({
  54        rejectUnauthorized: true,
  55        keepAlive: true,
  56        timeout: config.timeouts.medium,
  57      });
  58    }
  59  
  60    async initialize() {
  61      if (this.isInitialized) {
  62        return;
  63      }
  64  
  65      try {
  66        await this.detectRealIp();
  67        await this.initializeTor();
  68  
  69        this.refreshProxyPool().catch((error) => {
  70          logger.error("Error refreshing proxy pool:", error);
  71        });
  72        this.startBackgroundTasks();
  73        this.isInitialized = true;
  74  
  75        logger.info("IP Rotator Service initialized successfully", {
  76          torAvailable: this.tor.available,
  77          proxyCount: this.proxyPool.length,
  78          realIp: this.realIp ? "detected" : "unknown",
  79        });
  80      } catch (error) {
  81        logger.error("Failed to initialize IP Rotator Service:", error);
  82  
  83      }
  84    }
  85  
  86    async detectRealIp() {
  87      try {
  88        const response = await fetch(this.ipCheckUrls[1], { timeout: config.timeouts.short });
  89        if (!response.ok) {
  90          throw new Error(`HTTP ${response.status}`);
  91        }
  92        const data = await response.json();
  93        this.realIp = data.ip;
  94        logger.debug(`Real IP detected: ${this.realIp}`);
  95      } catch (error) {
  96        logger.warn("Could not detect real IP address:", error.message);
  97      }
  98    }
  99  
 100    async initializeTor() {
 101      try {
 102        this.tor.agent = new SocksProxyAgent("socks5h://127.0.0.1:9050");
 103  
 104        const testResponse = await fetch(this.ipCheckUrls[1], {
 105          agent: this.tor.agent,
 106          timeout: config.timeouts.short,
 107        });
 108  
 109        if (testResponse.ok) {
 110          this.tor.available = true;
 111          logger.info("Tor connection established");
 112  
 113          setInterval(async () => {
 114            try {
 115              await this.refreshTorExit();
 116            } catch (error) {
 117              logger.error("Failed to refresh Tor exit:", error);
 118            }
 119          }, config.timeouts.veryLong);
 120        }
 121      } catch (error) {
 122        this.tor.available = false;
 123        logger.error("Tor not available - will use proxies only", error);
 124      }
 125    }
 126  
 127    async scrapeProxies() {
 128      const sources = this.proxySources;
 129  
 130      const allProxies = new Set();
 131      const fetchPromises = sources.map(async (url) => {
 132        try {
 133          const controller = new AbortController();
 134          const timeoutId = setTimeout(() => controller.abort(), config.timeouts.short);
 135  
 136          const response = await fetch(url, {
 137            signal: controller.signal,
 138            timeout: config.timeouts.short,
 139            headers: {
 140              "User-Agent": this.getRandomUserAgent(),
 141            },
 142          });
 143  
 144          clearTimeout(timeoutId);
 145  
 146          if (!response.ok) {
 147            return;
 148          }
 149  
 150          const text = await response.text();
 151          const lines = text.split("\n");
 152  
 153          for (const line of lines) {
 154            const trimmed = line.trim();
 155            if (trimmed && trimmed.includes(":") && !trimmed.startsWith("#")) {
 156              const parts = trimmed.split(":");
 157              if (parts.length >= 2) {
 158                const ip = parts[0];
 159                const port = parts[1];
 160                if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip) && !isNaN(port) && parseInt(port) > 0 && parseInt(port) <= 65535) { // eslint-disable-line security/detect-unsafe-regex
 161                  const proxy = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
 162                  allProxies.add(proxy);
 163                }
 164              }
 165            }
 166          }
 167        } catch (error) {
 168          logger.debug(`Failed to fetch proxies from ${url}:`, error.message);
 169        }
 170      });
 171  
 172      await Promise.allSettled(fetchPromises);
 173      logger.important(`Scraped ${allProxies.size} unique proxies from sources`);
 174      return Array.from(allProxies);
 175    }
 176  
 177    async testProxy(proxy, timeout = null) {
 178      const timeoutMs = timeout ?? config.timeouts.medium;
 179      const startTime = Date.now();
 180      try {
 181        let agent;
 182  
 183        if (proxy.startsWith("socks5://")) {
 184          agent = new SocksProxyAgent(proxy);
 185        } else if (proxy.startsWith("http://") || proxy.startsWith("https://")) {
 186          agent = new HttpsProxyAgent(proxy, {
 187            rejectUnauthorized: true,
 188            timeout: timeoutMs / 2,
 189          });
 190        } else {
 191          agent = new HttpsProxyAgent(`http://${proxy}`, {
 192            rejectUnauthorized: true,
 193            timeout: timeoutMs / 2,
 194          });
 195        }
 196  
 197        const controller = new AbortController();
 198        const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
 199  
 200        const testUrls = this.ipCheckUrls;
 201  
 202        let response, data, ip;
 203        for (const url of testUrls) {
 204          try {
 205            response = await fetch(url, {
 206              agent,
 207              signal: controller.signal,
 208              timeout: timeoutMs / 2,
 209              headers: {
 210                "User-Agent": this.getRandomUserAgent(),
 211              },
 212            });
 213  
 214            if (response.ok) {
 215              const text = await response.text();
 216  
 217              try {
 218                data = JSON.parse(text);
 219              } catch (parseError) {
 220                logger.debug(`Invalid JSON from ${url}:`, parseError.message);
 221                continue;
 222              }
 223  
 224              ip = data.ip || data.origin;
 225              if (ip && typeof ip === "string" && ip.length > 0) {
 226                break;
 227              }
 228            }
 229          } catch (e) {
 230            logger.debug(`Proxy ${proxy} failed for ${url}:`, e.message);
 231            continue;
 232          }
 233        }
 234  
 235        clearTimeout(timeoutId);
 236  
 237        if (!ip) {
 238          logger.debug(`Proxy ${proxy}: No IP returned from test endpoints`);
 239          return null;
 240        }
 241  
 242        const latency = Date.now() - startTime;
 243        if (this.realIp && ip.includes(this.realIp)) {
 244          logger.warn(`Proxy ${proxy} leaked real IP - blacklisting`);
 245          return null;
 246        }
 247  
 248        const category = this.categorizeProxy(proxy, ip);
 249  
 250        if (this.blacklistedProxies.has(proxy)) {
 251          logger.debug(`Proxy ${proxy} is blacklisted - skipping`);
 252          return null;
 253        }
 254  
 255        return {
 256          proxy,
 257          ip,
 258          lastUsed: Date.now(),
 259          failures: 0,
 260          latency,
 261          category,
 262          successRate: 100,
 263          lastHealthCheck: Date.now(),
 264          rpcFriendly: true,
 265        };
 266      } catch (error) {
 267        if (error.name === "AbortError") {
 268          logger.debug(`Proxy ${proxy} timed out`);
 269        } else {
 270          logger.debug(`Proxy ${proxy} failed:`, error.message);
 271        }
 272        return null;
 273      }
 274    }
 275  
 276    async validateProxies(proxies, targetCount = 200) {
 277      const batchSize = 50;
 278      const working = [];
 279  
 280      for (let i = 0; i < proxies.length && working.length < targetCount; i += batchSize) {
 281        const batch = proxies.slice(i, i + batchSize);
 282        const testPromises = batch.map((proxy) => this.testProxy(proxy));
 283        const results = await Promise.allSettled(testPromises);
 284  
 285        for (const result of results) {
 286          if (result.status === "fulfilled" && result.value) {
 287            working.push(result.value);
 288            if (working.length >= targetCount) {
 289              break;
 290            }
 291          }
 292        }
 293  
 294        await new Promise(resolve => setTimeout(resolve, 100));
 295      }
 296  
 297      working.sort((a, b) => a.latency - b.latency);
 298      return working;
 299    }
 300  
 301    async refreshProxyPool() {
 302      try {
 303        const scraped = await this.scrapeProxies();
 304  
 305        if (scraped.length === 0) {
 306          logger.warn("No proxies scraped from sources");
 307          return;
 308        }
 309  
 310        const targetPoolSize = 500;
 311        const validated = await this.validateProxies(scraped, targetPoolSize);
 312  
 313        if (validated.length > 0) {
 314          const newProxies = validated.filter(p =>
 315            !this.proxyPool.some(existing => existing.proxy === p.proxy),
 316          );
 317  
 318          this.proxyPool = [...this.proxyPool, ...newProxies];
 319          this.proxyPool.sort((a, b) => a.latency - b.latency);
 320  
 321          this.proxyPool = this.proxyPool.slice(-500);
 322  
 323          logger.info(`Proxy pool refreshed: +${newProxies.length} new, total: ${this.proxyPool.length}`);
 324  
 325          newProxies.forEach(p => {
 326            this.proxyStats.set(p.proxy, {
 327              successes: 0,
 328              failures: 0,
 329              totalRequests: 0,
 330              avgLatency: p.latency,
 331              lastUsed: Date.now(),
 332            });
 333          });
 334        } else {
 335          logger.warn("No working proxies found during refresh");
 336        }
 337      } catch (error) {
 338        logger.error("Error refreshing proxy pool:", error);
 339      }
 340    }
 341  
 342    cleanupRequestCounts() {
 343      const now = Date.now();
 344      const staleTimeout = config.timeouts.veryLong;
 345  
 346      for (const [proxy, count] of this.requestCounts.entries()) {
 347        if (count === 0 || (now - (this.proxyStats.get(proxy)?.lastUsed || 0)) > staleTimeout) {
 348          this.requestCounts.delete(proxy);
 349        }
 350      }
 351  
 352      if (this.requestCounts.size > 1000) {
 353        logger.warn(`RequestCounts map size is ${this.requestCounts.size}, forcing cleanup`);
 354        this.requestCounts.clear();
 355      }
 356    }
 357  
 358    cleanupDeadProxies() {
 359      const now = Date.now();
 360      const baseDeadTimeout = config.timeouts.extraLong;
 361      const baseQuarantineTimeout = config.timeouts.extraLong / 2;
 362  
 363      const failureRate = this.stats.totalRequests > 0 ?
 364        (this.stats.failedRequests / this.stats.totalRequests) : 0;
 365  
 366      const adaptiveMultiplier = Math.max(0.5, Math.min(2.0, 1 + failureRate));
 367      const deadTimeout = baseDeadTimeout * adaptiveMultiplier;
 368      const quarantineTimeout = baseQuarantineTimeout * adaptiveMultiplier;
 369  
 370      for (const [proxy, data] of this.deadProxies.entries()) {
 371        const age = now - data.markedAt;
 372        const consecutiveFailures = data.consecutiveFailures || 0;
 373        const failureMultiplier = Math.max(1, consecutiveFailures * 0.5);
 374        const adjustedDeadTimeout = deadTimeout * failureMultiplier;
 375  
 376        if (age > adjustedDeadTimeout) {
 377          this.deadProxies.delete(proxy);
 378          this.proxyStats.delete(proxy);
 379          this.requestCounts.delete(proxy);
 380          logger.debug(`Proxy ${proxy} removed from dead list after ${Math.round(age / 1000)}s`);
 381        } else if (age > quarantineTimeout && data.attempts < 3) {
 382          this.deadProxies.delete(proxy);
 383          logger.info(`Proxy ${proxy} released from quarantine for retest`);
 384        }
 385      }
 386  
 387      this.proxyPool = this.proxyPool.filter((proxyData) => {
 388        const stats = this.proxyStats.get(proxyData.proxy);
 389        if (!stats) {
 390          return true;
 391        }
 392  
 393        const successRate = stats.totalRequests > 0 ? (stats.successes / stats.totalRequests) * 100 : 100;
 394        const failureThreshold = Math.min(5, Math.max(3, Math.floor(5 * (1 + failureRate))));
 395        const successThreshold = Math.max(70, 80 - (failureRate * 10));
 396  
 397        if (proxyData.failures >= failureThreshold || successRate < successThreshold) {
 398          const deadData = this.deadProxies.get(proxyData.proxy);
 399          if (deadData) {
 400            deadData.attempts++;
 401            deadData.consecutiveFailures = (deadData.consecutiveFailures || 0) + 1;
 402            if (deadData.attempts >= 3) {
 403              logger.warn(`Proxy ${proxyData.proxy} permanently removed (success rate: ${successRate.toFixed(1)}%, failures: ${proxyData.failures})`);
 404              this.proxyStats.delete(proxyData.proxy);
 405            }
 406          } else {
 407            this.deadProxies.set(proxyData.proxy, {
 408              markedAt: now,
 409              attempts: 1,
 410              consecutiveFailures: 1,
 411              lastError: successRate < successThreshold ? "Low success rate" : "Multiple failures",
 412            });
 413          }
 414          return false;
 415        }
 416        return true;
 417      });
 418  
 419      this.updatePoolHealth();
 420    }
 421  
 422    startBackgroundTasks() {
 423      this.refreshInterval = setInterval(() => {
 424        this.refreshProxyPool();
 425      }, (config.timeouts.veryLong || 300000));
 426  
 427      this.cleanupInterval = setInterval(() => {
 428        this.cleanupDeadProxies();
 429        this.cleanupRequestCounts();
 430        this.cleanupBlacklistedProxies();
 431      }, config.timeouts.extraLong || 600000);
 432  
 433      this.healthCheckInterval = setInterval(() => {
 434        this.performHealthChecks();
 435      }, config.timeouts.veryLong || 600000);
 436  
 437      this.monitoringInterval = setInterval(() => {
 438        this.logPoolHealth();
 439      }, config.timeouts.extraLong || 600000);
 440    }
 441  
 442    stopBackgroundTasks() {
 443      if (this.refreshInterval) {
 444        clearInterval(this.refreshInterval);
 445        this.refreshInterval = null;
 446      }
 447      if (this.cleanupInterval) {
 448        clearInterval(this.cleanupInterval);
 449        this.cleanupInterval = null;
 450      }
 451      if (this.healthCheckInterval) {
 452        clearInterval(this.healthCheckInterval);
 453        this.healthCheckInterval = null;
 454      }
 455      if (this.monitoringInterval) {
 456        clearInterval(this.monitoringInterval);
 457        this.monitoringInterval = null;
 458      }
 459      logger.info("Background tasks stopped");
 460    }
 461  
 462    selectProxy(sessionId = null) {
 463      if (this.proxyPool.length === 0) {
 464        return null;
 465      }
 466  
 467      if (sessionId && this.sessionProxies.has(sessionId)) {
 468        const sessionProxy = this.sessionProxies.get(sessionId);
 469        if (this.proxyPool.some(p => p.proxy === sessionProxy.proxy)) {
 470          return sessionProxy;
 471        } else {
 472          this.sessionProxies.delete(sessionId);
 473        }
 474      }
 475  
 476      const availableProxies = this.proxyPool.filter(p => {
 477        const stats = this.proxyStats.get(p.proxy);
 478        if (!stats) {
 479          return true;
 480        }
 481  
 482        const successRate = stats.totalRequests > 0 ? (stats.successes / stats.totalRequests) * 100 : 100;
 483        const recentRequests = this.requestCounts.get(p.proxy) || 0;
 484  
 485        return successRate >= 80 && recentRequests < 10;
 486      });
 487  
 488      if (availableProxies.length === 0) {
 489        logger.warn("No healthy proxies available, using least failed proxy");
 490        return this.proxyPool.reduce((best, current) =>
 491          current.failures < best.failures ? current : best,
 492        );
 493      }
 494  
 495      availableProxies.sort((a, b) => {
 496        const statsA = this.proxyStats.get(a.proxy) || {};
 497        const statsB = this.proxyStats.get(b.proxy) || {};
 498  
 499        const successRateA = statsA.totalRequests > 0 ? (statsA.successes / statsA.totalRequests) * 100 : 100;
 500        const successRateB = statsB.totalRequests > 0 ? (statsB.successes / statsB.totalRequests) * 100 : 100;
 501  
 502        if (Math.abs(successRateA - successRateB) > 5) {
 503          return successRateB - successRateA;
 504        }
 505  
 506        return a.latency - b.latency;
 507      });
 508  
 509      const topCandidates = availableProxies.slice(0, Math.min(10, availableProxies.length));
 510      const selected = topCandidates[Math.floor(Math.random() * topCandidates.length)];
 511  
 512      if (sessionId) {
 513        this.sessionProxies.set(sessionId, selected);
 514      }
 515  
 516      return selected;
 517    }
 518  
 519    async makeRequest(url, options = {}) {
 520      if (!this.isInitialized) {
 521        throw new Error("IP Rotator Service not initialized. Call initialize() first.");
 522      }
 523  
 524      const now = Date.now();
 525      this.requestTimestamps = this.requestTimestamps.filter(t => now - t < config.timeouts.long || 60000);
 526  
 527      if (this.requestTimestamps.length >= this.generalRateLimit) {
 528        logger.warn("Request rate limit approached", {
 529          current: this.requestTimestamps.length,
 530          limit: this.generalRateLimit,
 531        });
 532      }
 533  
 534      this.requestTimestamps.push(now);
 535      this.stats.totalRequests++;
 536      const maxRetries = options.maxRetries || 5;
 537      const timeout = options.timeout || config.timeouts.medium || 10000;
 538      const method = options.method || "GET";
 539      const sessionId = options.sessionId || this.generateSessionId();
 540      const prioritizeTor = options.prioritizeTor !== false;
 541  
 542      let lastError = null;
 543      let attempt = 0;
 544      const useTor = !options.sensitive && this.tor.available && this.tor.failures < 3;
 545      const tryTorFirst = prioritizeTor && useTor;
 546      const tryTorSecond = !prioritizeTor && useTor;
 547  
 548      const attemptRequest = async (useTorMethod) => {
 549        let agent = null;
 550        let method_used = "direct";
 551        let proxyData = null;
 552  
 553        if (useTorMethod) {
 554          agent = this.tor.agent;
 555          method_used = "tor";
 556        } else {
 557          proxyData = this.selectProxy(sessionId);
 558          if (proxyData) {
 559            if (proxyData.proxy.startsWith("socks5://")) {
 560              agent = new SocksProxyAgent(proxyData.proxy);
 561            } else {
 562              agent = new HttpsProxyAgent(proxyData.proxy);
 563            }
 564            method_used = "proxy";
 565          }
 566        }
 567  
 568        if (!agent) {
 569          return { success: false, proxyData: null, method_used };
 570        }
 571  
 572        const startTime = Date.now();
 573        const controller = new AbortController();
 574        const timeoutId = setTimeout(() => controller.abort(), timeout);
 575  
 576        const fetchOptions = {
 577          ...options,
 578          method,
 579          timeout,
 580          signal: controller.signal,
 581          agent: this.httpsAgent,
 582          headers: {
 583            ...options.headers,
 584            "User-Agent": this.getRandomUserAgent(),
 585            "Accept": options.headers?.Accept || "application/json, text/plain, */*",
 586            "Accept-Language": "en-US,en;q=0.9",
 587            "Accept-Encoding": "gzip, deflate, br",
 588            "DNT": "1",
 589            "Connection": "keep-alive",
 590            "Upgrade-Insecure-Requests": "1",
 591          },
 592        };
 593  
 594        if (agent) {
 595          fetchOptions.agent = agent;
 596        }
 597  
 598        try {
 599          const response = await fetch(url, fetchOptions);
 600          clearTimeout(timeoutId);
 601  
 602          const latency = Date.now() - startTime;
 603  
 604          if (!response.ok && response.status >= 400 && response.status < 600) {
 605            if (response.status === 429) {
 606              const domain = new URL(url).hostname;
 607              this.handleDomainRateLimit(domain);
 608              
 609              if (proxyData) {
 610                logger.debug(`Proxy ${proxyData.proxy} hit rate limit, trying different proxy`);
 611              }
 612              
 613              throw new Error("Rate limited (429)");
 614            } else if (response.status >= 500) {
 615              throw new Error(`Server error ${response.status}`);
 616            } else {
 617              throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 618            }
 619          }
 620  
 621        if (proxyData) {
 622          proxyData.lastUsed = Date.now();
 623          proxyData.failures = 0;
 624  
 625          const stats = this.proxyStats.get(proxyData.proxy);
 626          if (stats) {
 627            stats.successes++;
 628            stats.totalRequests++;
 629            stats.avgLatency = (stats.avgLatency + latency) / 2;
 630            stats.lastUsed = Date.now();
 631          }
 632        }
 633  
 634          this.stats.successfulRequests++;
 635          this.stats[`${method_used}Used`]++;
 636          this.stats.avgLatency = (this.stats.avgLatency + latency) / 2;
 637  
 638          const result = {
 639            response,
 640            method: method_used,
 641            proxy: proxyData?.proxy,
 642            attempt,
 643            latency,
 644          };
 645  
 646          try {
 647            const text = await response.clone().text();
 648            let testData;
 649  
 650            try {
 651              testData = JSON.parse(text);
 652            } catch (parseError) {
 653              logger.debug("Invalid JSON in response:", parseError.message);
 654              return { success: true, result, proxyData, method_used };
 655            }
 656  
 657            if (testData.origin || testData.ip) {
 658              const ip = (testData.origin || testData.ip).split(",")[0].trim();
 659  
 660              if (this.realIp && ip === this.realIp) {
 661                logger.warn(`Real IP exposed via ${method_used} - retrying with different method`);
 662                if (proxyData) {
 663                  proxyData.failures += 10;
 664                } else if (method_used === "tor") {
 665                  this.tor.failures++;
 666                  await this.refreshTorExit();
 667                }
 668                throw new Error("Real IP exposed");
 669              }
 670  
 671              this.stats.uniqueIps.add(ip);
 672  
 673              if (method_used === "tor") {
 674                this.tor.currentExit = ip;
 675              }
 676            }
 677          } catch { }
 678  
 679          const currentCount = this.requestCounts.get(proxyData?.proxy || "direct") || 0;
 680          this.requestCounts.set(proxyData?.proxy || "direct", currentCount + 1);
 681  
 682          setTimeout(() => {
 683            const count = this.requestCounts.get(proxyData?.proxy || "direct") || 0;
 684            if (count > 0) {
 685              this.requestCounts.set(proxyData?.proxy || "direct", count - 1);
 686            }
 687          }, config.timeouts.long || 60000);
 688  
 689          return { success: true, result, proxyData, method_used };
 690        } catch (error) {
 691          clearTimeout(timeoutId);
 692          return { success: false, error, proxyData, method_used };
 693        }
 694      };
 695  
 696      if (tryTorFirst) {
 697        for (let retry = 0; retry < maxRetries; retry++) {
 698          attempt++;
 699          const response = await attemptRequest(true);
 700          if (response.success) {
 701            return response.result;
 702          }
 703          
 704          lastError = response.error;
 705          this.tor.failures++;
 706          if (this.tor.failures >= 3) {
 707            logger.error("Tor failed multiple times, switching to proxies");
 708          }
 709          if (lastError?.message.includes("429")) {
 710            logger.debug(`Rate limit detected on Tor, switching to proxies`);
 711            break;
 712          }
 713          if (retry < maxRetries - 1) {
 714            const baseDelay = Math.min(1000 * Math.pow(2, retry), 5000);
 715            const jitter = Math.random() * 1000;
 716            await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
 717          }
 718        }
 719      }
 720  
 721      for (let retry = 0; retry < maxRetries; retry++) {
 722        attempt++;
 723        const response = await attemptRequest(false);
 724        if (response.success) {
 725          return response.result;
 726        }
 727        
 728        lastError = response.error;
 729        const proxyData = response.proxyData;
 730        
 731        if (proxyData) {
 732          proxyData.failures++;
 733  
 734          const stats = this.proxyStats.get(proxyData.proxy);
 735          if (stats) {
 736            stats.failures++;
 737            stats.totalRequests++;
 738          }
 739  
 740          this.trackProxyFailure(proxyData.proxy, lastError?.message || "Unknown error");
 741  
 742          if (this.shouldBlacklistProxy(proxyData.proxy)) {
 743            this.blacklistProxy(proxyData.proxy, "Auto-blacklisted due to poor performance");
 744          }
 745  
 746          if (lastError?.message.includes("429") || lastError?.message.includes("rate limit")) {
 747            logger.debug(`Proxy ${proxyData.proxy} failed (failures: ${proxyData.failures}):`, lastError?.message);
 748            continue;
 749          }
 750  
 751          logger.debug(`Proxy ${proxyData.proxy} failed (failures: ${proxyData.failures}):`, lastError?.message);
 752        }
 753        
 754        if (retry < maxRetries - 1) {
 755          const baseDelay = Math.min(1000 * Math.pow(2, retry), 5000);
 756          const jitter = Math.random() * 1000;
 757          await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
 758        }
 759      }
 760  
 761      if (tryTorSecond) {
 762        for (let retry = 0; retry < maxRetries; retry++) {
 763          attempt++;
 764          const response = await attemptRequest(true);
 765          if (response.success) {
 766            return response.result;
 767          }
 768          
 769          lastError = response.error;
 770          this.tor.failures++;
 771          if (this.tor.failures >= 3) {
 772            logger.error("Tor failed multiple times");
 773          }
 774          if (retry < maxRetries - 1) {
 775            const baseDelay = Math.min(1000 * Math.pow(2, retry), 5000);
 776            const jitter = Math.random() * 1000;
 777            await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
 778          }
 779        }
 780      }
 781  
 782      if (!useTor && this.proxyPool.length === 0) {
 783        logger.error("All proxy methods failed, would fall back to direct connection - blocked for security");
 784        throw new Error("No anonymous connection methods available");
 785      }
 786  
 787      this.stats.failedRequests++;
 788  
 789      if (lastError?.message.includes("429") || lastError?.message.includes("Too Many Requests")) {
 790        logger.warn(`Rate limited on all endpoints after ${attempt} attempts`, {
 791          url,
 792          error: lastError?.message,
 793        });
 794      } else {
 795        logger.error(`Request failed after ${attempt} attempts:`, {
 796          url,
 797          error: lastError?.message,
 798        });
 799      }
 800  
 801      throw lastError || new Error("Request failed after all retries");
 802    }
 803  
 804    async get(url, options = {}) {
 805      return this.makeRequest(url, { ...options, method: "GET", prioritizeTor: true });
 806    }
 807  
 808    async post(url, options = {}) {
 809      return this.makeRequest(url, { ...options, method: "POST", prioritizeTor: true });
 810    }
 811  
 812    getStats() {
 813      const healthyProxies = this.proxyPool.filter(p => {
 814        const stats = this.proxyStats.get(p.proxy);
 815        return stats ? stats.successes / stats.totalRequests >= 0.8 : true;
 816      }).length;
 817  
 818      return {
 819        ...this.stats,
 820        uniqueIps: this.stats.uniqueIps.size,
 821        successRate: this.stats.totalRequests > 0
 822          ? ((this.stats.successfulRequests / this.stats.totalRequests) * 100).toFixed(2) + "%"
 823          : "0%",
 824        proxyPoolSize: this.proxyPool.length,
 825        healthyProxies,
 826        deadProxiesCount: this.deadProxies.size,
 827        blacklistedProxiesCount: this.blacklistedProxies.size,
 828        torAvailable: this.tor.available,
 829        torFailures: this.tor.failures,
 830        currentTorExit: this.tor.currentExit,
 831        anonymityProtection: this.realIp ? "ACTIVE" : "UNKNOWN",
 832        poolHealth: this.stats.poolHealth.toFixed(1) + "%",
 833        avgLatency: Math.round(this.stats.avgLatency) + "ms",
 834      };
 835    }
 836  
 837    logStats() {
 838      const stats = this.getStats();
 839      logger.info("IP Rotator Statistics:", stats);
 840    }
 841  
 842    async getWsAgent(preferTor = false, sessionId = null) {
 843      if (preferTor && this.tor.available && this.tor.failures < 3) {
 844        return this.tor.agent;
 845      }
 846  
 847      const proxyData = this.selectProxy(sessionId);
 848      if (proxyData) {
 849        let agent;
 850        if (proxyData.proxy.startsWith("socks5://")) {
 851          agent = new SocksProxyAgent(proxyData.proxy);
 852        } else {
 853          agent = new HttpsProxyAgent(proxyData.proxy);
 854        }
 855        agent.proxy = proxyData.proxy;
 856        return agent;
 857      }
 858  
 859      return null;
 860    }
 861  
 862    createProxiedWebSocket(url, options = {}) {
 863      const { preferTor = false, agent: customAgent, sessionId = null } = options;
 864      let agent = customAgent;
 865  
 866      if (!agent) {
 867        agent = this.getWsAgent(preferTor, sessionId);
 868      }
 869  
 870      const wsOptions = {
 871        handshakeTimeout: config.timeouts.medium,
 872        perMessageDeflate: false,
 873        headers: {
 874          "User-Agent": this.getRandomUserAgent(),
 875          "Origin": "https://google.com",
 876        },
 877      };
 878  
 879      if (agent) {
 880        wsOptions.agent = agent;
 881      }
 882  
 883      const ws = new WebSocket(url, wsOptions);
 884  
 885      if (agent) {
 886        ws.on("error", (error) => {
 887          logger.debug("WebSocket error through proxy:", error.message);
 888          if (agent === this.tor.agent) {
 889            this.tor.failures++;
 890            this.stats.torUsed++;
 891          } else {
 892            const proxyData = this.proxyPool.find(p => p.proxy === agent.proxy);
 893            if (proxyData) {
 894              proxyData.failures++;
 895              const stats = this.proxyStats.get(proxyData.proxy);
 896              if (stats) {
 897                stats.failures++;
 898                stats.totalRequests++;
 899              }
 900            }
 901            this.stats.proxyUsed++;
 902          }
 903        });
 904  
 905        ws.on("close", (code, reason) => {
 906          if (code !== 1000) {
 907            logger.debug(`WebSocket closed with code ${code}:`, reason?.toString());
 908          }
 909        });
 910      } else {
 911        this.stats.directUsed++;
 912        logger.warn("WebSocket connection without proxy - anonymity risk");
 913      }
 914  
 915      return ws;
 916    }
 917  
 918    async createProxiedWebSocketAsync(url, options = {}) {
 919      const { preferTor = false, agent: customAgent, sessionId = null } = options;
 920      let agent = customAgent;
 921  
 922      if (!agent) {
 923        agent = this.getWsAgent(preferTor, sessionId);
 924      }
 925  
 926      const wsOptions = {
 927        handshakeTimeout: config.timeouts.medium,
 928        perMessageDeflate: false,
 929        headers: {
 930          "User-Agent": this.getRandomUserAgent(),
 931          "Origin": "https://google.com",
 932        },
 933      };
 934  
 935      if (agent) {
 936        wsOptions.agent = agent;
 937      }
 938  
 939      const ws = new WebSocket(url, wsOptions);
 940  
 941      if (agent) {
 942        ws.on("error", (error) => {
 943          logger.debug("WebSocket error through proxy:", error.message);
 944          if (agent === this.tor.agent) {
 945            this.tor.failures++;
 946            this.stats.torUsed++;
 947          } else {
 948            const proxyData = this.proxyPool.find(p => p.proxy === agent.proxy);
 949            if (proxyData) {
 950              proxyData.failures++;
 951              const stats = this.proxyStats.get(proxyData.proxy);
 952              if (stats) {
 953                stats.failures++;
 954                stats.totalRequests++;
 955              }
 956            }
 957            this.stats.proxyUsed++;
 958          }
 959        });
 960  
 961        ws.on("close", (code, reason) => {
 962          if (code !== 1000) {
 963            logger.debug(`WebSocket closed with code ${code}:`, reason?.toString());
 964          }
 965        });
 966      } else {
 967        this.stats.directUsed++;
 968        logger.warn("WebSocket connection without proxy - anonymity risk");
 969      }
 970  
 971      return ws;
 972    }
 973  
 974    async testWebSocketProxy(url, timeout = null) {
 975      const timeoutMs = timeout ?? config.timeouts.medium;
 976      return new Promise((resolve) => {
 977        const ws = this.createProxiedWebSocket(url, { preferTor: true });
 978        let resolved = false;
 979  
 980        const timer = setTimeout(() => {
 981          if (!resolved) {
 982            resolved = true;
 983            ws.terminate();
 984            resolve(false);
 985          }
 986        }, timeoutMs);
 987  
 988        ws.on("open", () => {
 989          if (!resolved) {
 990            resolved = true;
 991            clearTimeout(timer);
 992            ws.close();
 993            resolve(true);
 994          }
 995        });
 996  
 997        ws.on("error", () => {
 998          if (!resolved) {
 999            resolved = true;
1000            clearTimeout(timer);
1001            resolve(false);
1002          }
1003        });
1004      });
1005    }
1006  
1007    categorizeProxy(proxy, ip) {
1008      if (proxy.includes("tor") || proxy.includes("socks5")) {
1009        return "premium";
1010      }
1011  
1012      const datacenterRanges = [
1013        /^\d+\.\d+\.\d+\.(1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)/,
1014        /^10\./,
1015        /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
1016        /^192\.168\./,
1017      ];
1018  
1019      const isDatacenter = datacenterRanges.some(range => range.test(ip));
1020  
1021      if (isDatacenter) {
1022        return "datacenter";
1023      }
1024  
1025      return "residential";
1026    }
1027  
1028    getRandomUserAgent() {
1029      return getRandomUserAgent();
1030    }
1031  
1032    generateSessionId() {
1033      return crypto.randomBytes(16).toString("hex");
1034    }
1035  
1036    async sendTorControlCommand(command) {
1037      return new Promise((resolve, reject) => {
1038        const socket = net.connect({ host: "127.0.0.1", port: 9051 }, () => {
1039          socket.write(`AUTHENTICATE ""\r\n`);
1040        });
1041  
1042        let authenticated = false;
1043        let buffer = "";
1044  
1045        socket.on("data", (data) => {
1046          buffer += data.toString();
1047          const lines = buffer.split("\r\n");
1048          buffer = lines.pop();
1049  
1050          for (const line of lines) {
1051            if (line.startsWith("250")) {
1052              if (!authenticated) {
1053                authenticated = true;
1054                socket.write(`${command}\r\n`);
1055              } else {
1056                socket.end();
1057                this.tor.controlAvailable = true;
1058                resolve(true);
1059                return;
1060              }
1061            } else if (line.startsWith("515")) {
1062              socket.end();
1063              this.tor.controlAvailable = false;
1064              reject(new Error("Tor authentication failed"));
1065              return;
1066            } else if (line.startsWith("5")) {
1067              socket.end();
1068              this.tor.controlAvailable = false;
1069              reject(new Error(`Tor control error: ${line}`));
1070              return;
1071            }
1072          }
1073        });
1074  
1075        socket.on("error", (error) => {
1076          if (error.code === "ECONNREFUSED") {
1077            this.tor.controlAvailable = false;
1078          }
1079          reject(error);
1080        });
1081  
1082        socket.setTimeout(5000, () => {
1083          socket.destroy();
1084          this.tor.controlAvailable = false;
1085          reject(new Error("Tor control connection timeout"));
1086        });
1087      });
1088    }
1089  
1090    async refreshTorExit() {
1091      try {
1092        if (!this.tor.agent) {
1093          return;
1094        }
1095  
1096        if (!this.tor.controlAvailable && this.tor.controlAvailable !== undefined) {
1097          logger.debug("Tor control port not available, skipping exit node refresh");
1098          return;
1099        }
1100  
1101        try {
1102          await this.sendTorControlCommand("SIGNAL NEWNYM");
1103          this.tor.currentExit = null;
1104          logger.debug("Requested new Tor exit node");
1105  
1106          await new Promise(resolve => setTimeout(resolve, 5000));
1107  
1108          const testResponse = await fetch(this.ipCheckUrls[1], {
1109            agent: this.tor.agent,
1110            timeout: config.timeouts.short,
1111          });
1112  
1113          if (testResponse.ok) {
1114            const data = await testResponse.json();
1115            const newIp = data.ip;
1116  
1117            if (this.tor.maliciousExits.has(newIp)) {
1118              logger.warn(`New Tor exit ${newIp} is known to be malicious, refreshing again`);
1119              await this.refreshTorExit();
1120            } else {
1121              this.tor.currentExit = newIp;
1122              logger.debug(`Switched to new Tor exit: ${newIp}`);
1123            }
1124          }
1125        } catch (error) {
1126          if (error.code === "ECONNREFUSED" || error.message.includes("connection")) {
1127            this.tor.controlAvailable = false;
1128            logger.warn("Tor control port not available - exit node refresh disabled. Tor connections will still work but exit nodes won't be refreshed automatically.");
1129          } else {
1130            throw error;
1131          }
1132        }
1133      } catch (error) {
1134        logger.error("Failed to refresh Tor exit node:", error);
1135      }
1136    }
1137  
1138    async performHealthChecks() {
1139      if (Date.now() - this.lastHealthCheck < config.timeouts.veryLong) {
1140        return;
1141      }
1142  
1143      this.lastHealthCheck = Date.now();
1144      const sampleSize = Math.min(20, this.proxyPool.length);
1145      const sample = this.proxyPool
1146        .sort(() => Math.random() - 0.5)
1147        .slice(0, sampleSize);
1148  
1149      logger.info(`Performing health checks on ${sample.length} proxies`);
1150  
1151      for (const proxy of sample) {
1152        const isHealthy = await this.testProxy(proxy.proxy, 3000);
1153        if (!isHealthy) {
1154          proxy.failures++;
1155          logger.debug(`Proxy ${proxy.proxy} failed health check`);
1156        } else {
1157          proxy.lastHealthCheck = Date.now();
1158        }
1159  
1160        await new Promise(resolve => setTimeout(resolve, 200));
1161      }
1162  
1163      this.updatePoolHealth();
1164    }
1165  
1166    updatePoolHealth() {
1167      if (this.proxyPool.length === 0) {
1168        this.stats.poolHealth = 0;
1169        return;
1170      }
1171  
1172      const healthyCount = this.proxyPool.filter(p => {
1173        const stats = this.proxyStats.get(p.proxy);
1174        if (!stats) {
1175          return true;
1176        }
1177        return stats.totalRequests === 0 || (stats.successes / stats.totalRequests) >= 0.8;
1178      }).length;
1179  
1180      this.stats.poolHealth = (healthyCount / this.proxyPool.length) * 100;
1181    }
1182  
1183    trackProxyFailure(proxy, errorMessage) {
1184      if (!this.proxyFailureHistory.has(proxy)) {
1185        this.proxyFailureHistory.set(proxy, {
1186          failures: [],
1187          firstFailure: Date.now(),
1188          blacklisted: false,
1189        });
1190      }
1191  
1192      const history = this.proxyFailureHistory.get(proxy);
1193      history.failures.push({
1194        timestamp: Date.now(),
1195        error: errorMessage,
1196      });
1197  
1198      const oneHourAgo = Date.now() - (360000);
1199      history.failures = history.failures.filter(f => f.timestamp > oneHourAgo);
1200    }
1201  
1202    shouldBlacklistProxy(proxy) {
1203      const history = this.proxyFailureHistory.get(proxy);
1204      if (!history || history.blacklisted) {
1205        return false;
1206      }
1207  
1208      const recentFailures = history.failures.length;
1209      const timeSinceFirst = Date.now() - history.firstFailure;
1210      const oneHour = 360000;
1211  
1212      if (timeSinceFirst > oneHour) {
1213        history.firstFailure = Date.now();
1214        history.failures = history.failures.filter(f => Date.now() - f.timestamp < oneHour);
1215      }
1216  
1217      if (recentFailures >= 10) {
1218        return true;
1219      } else if (recentFailures >= 5 && timeSinceFirst < config.timeouts.extraLong) {
1220        return true;
1221      }
1222  
1223      const stats = this.proxyStats.get(proxy);
1224      if (stats && stats.totalRequests >= 20) {
1225        const successRate = stats.successes / stats.totalRequests;
1226        if (successRate < 0.3) {
1227          return true;
1228        }
1229      }
1230  
1231      const errorTypes = new Set(history.failures.map(f => f.error));
1232      if (errorTypes.has("Connection refused") || errorTypes.has("Connection timeout") || errorTypes.has("Socket timeout")) {
1233        if (recentFailures >= 3) {
1234          return true;
1235        }
1236      }
1237  
1238      return false;
1239    }
1240  
1241    blacklistProxy(proxy, reason = "Poor performance") {
1242      this.blacklistedProxies.add(proxy);
1243  
1244      const history = this.proxyFailureHistory.get(proxy);
1245      if (history) {
1246        history.blacklisted = true;
1247      }
1248  
1249      this.proxyPool = this.proxyPool.filter(p => p.proxy !== proxy);
1250      this.deadProxies.set(proxy, {
1251        markedAt: Date.now(),
1252        attempts: 3,
1253        lastError: reason,
1254        blacklisted: true,
1255      });
1256  
1257      this.proxyStats.delete(proxy);
1258      this.requestCounts.delete(proxy);
1259  
1260      logger.warn(`Proxy ${proxy} blacklisted: ${reason}`);
1261    }
1262  
1263    cleanupBlacklistedProxies() {
1264      const now = Date.now();
1265      const blacklistTimeout = this.blacklistTimeout;
1266  
1267      for (const proxy of this.blacklistedProxies) {
1268        const deadData = this.deadProxies.get(proxy);
1269        if (deadData && now - deadData.markedAt > blacklistTimeout) {
1270          this.blacklistedProxies.delete(proxy);
1271          this.deadProxies.delete(proxy);
1272          this.proxyFailureHistory.delete(proxy);
1273          logger.info(`Proxy ${proxy} removed from blacklist after timeout`);
1274        }
1275      }
1276    }
1277  
1278    logPoolHealth() {
1279      const stats = this.getStats();
1280  
1281      logger.info("Proxy Pool Health Report:", {
1282        totalProxies: stats.proxyPoolSize,
1283        healthyProxies: stats.healthyProxies,
1284        poolHealth: stats.poolHealth,
1285        torAvailable: stats.torAvailable,
1286        torFailures: stats.torFailures,
1287        avgLatency: stats.avgLatency,
1288        successRate: stats.successRate,
1289      });
1290  
1291      if (stats.poolHealth < 50) {
1292        logger.warn("Proxy pool health is below 50% - consider adding more sources");
1293      }
1294  
1295      if (this.tor.failures >= 3) {
1296        logger.error("Tor has failed multiple times - check Tor configuration");
1297      }
1298    }
1299  
1300    handleDomainRateLimit(domain) {
1301      const now = Date.now();
1302      if (!this.domainRateLimits.has(domain)) {
1303        this.domainRateLimits.set(domain, {
1304          firstHit: now,
1305          lastHit: now,
1306          hitCount: 1,
1307          backoffUntil: 0,
1308        });
1309      } else {
1310        const limit = this.domainRateLimits.get(domain);
1311        limit.lastHit = now;
1312        limit.hitCount++;
1313  
1314        const timeSinceFirst = now - limit.firstHit;
1315        if (timeSinceFirst < 60000) {
1316          limit.backoffUntil = now + Math.min(300000, Math.pow(2, limit.hitCount) * 1000);
1317          logger.warn(`Domain ${domain} hit rate limit ${limit.hitCount} times, backing off until ${new Date(limit.backoffUntil)}`);
1318        } else if (timeSinceFirst > 300000) {
1319          limit.hitCount = 1;
1320          limit.firstHit = now;
1321          limit.backoffUntil = 0;
1322        }
1323      }
1324    }
1325  
1326    getDomainRateLimitDelay(domain) {
1327      const limit = this.domainRateLimits.get(domain);
1328      if (!limit || limit.backoffUntil === 0) {
1329        return 0;
1330      }
1331  
1332      const now = Date.now();
1333      if (now < limit.backoffUntil) {
1334        return limit.backoffUntil - now;
1335      }
1336  
1337      return 0;
1338    }
1339  }
1340  
1341  export const ipRotatorService = new IpRotatorService();