UNPKG

@rynn-k/proxy-agent

Version:

Efficient proxy rotation agent for Node.js with seamless axios integration

356 lines (307 loc) 10.1 kB
const { HttpsProxyAgent } = require('https-proxy-agent'); const { HttpProxyAgent } = require('http-proxy-agent'); const { SocksProxyAgent } = require('socks-proxy-agent'); const fs = require('fs'); const path = require('path'); const { validateProxy, parseProxyLine } = require('./validator'); /** * ProxyAgent - Efficient proxy rotation with axios integration * @class */ class ProxyAgent { /** * Create a ProxyAgent instance * @param {string} [proxyFilePath='proxies.txt'] - Path to proxy file * @param {Object} [options={}] - Configuration options * @param {boolean} [options.random=false] - Use random proxy selection * @param {boolean} [options.log=true] - Enable logging * @param {string} [options.encoding='utf-8'] - File encoding * @param {boolean} [options.autoReload=false] - Auto reload on file change * @param {string} [options.type='http'] - Default proxy type: 'http', 'https', 'socks4', 'socks5' */ constructor(proxyFilePath = 'proxies.txt', options = {}) { this.proxyFilePath = path.resolve(proxyFilePath); this.proxies = []; this.currentIndex = 0; this.currentProxy = null; this.lastUsedIndex = -1; this.options = { random: options.random || false, log: options.log !== false, encoding: options.encoding || 'utf-8', autoReload: options.autoReload || false, type: options.type || 'http', ...options }; this.loadProxies(); if (this.options.autoReload) { this.setupAutoReload(); } } /** * Load proxies from file * @private */ loadProxies() { try { if (!fs.existsSync(this.proxyFilePath)) { throw new Error(`Proxy file not found: ${this.proxyFilePath}`); } const proxyContent = fs.readFileSync(this.proxyFilePath, this.options.encoding); const proxyLines = proxyContent .split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); if (proxyLines.length === 0) { throw new Error('No valid proxy entries found in file'); } this.proxies = proxyLines .map((line, index) => { try { return parseProxyLine(line, index + 1, this.options.type); } catch (error) { if (this.options.log) { console.warn(`⚠️ Skipping invalid proxy at line ${index + 1}: ${error.message}`); } return null; } }) .filter(Boolean); if (this.proxies.length === 0) { throw new Error('No valid proxies could be parsed from file'); } this.proxies = this.proxies.filter((proxy, index) => { if (validateProxy(proxy)) { return true; } else { if (this.options.log) { console.warn(`⚠️ Skipping invalid proxy: ${proxy.ip}:${proxy.port}`); } return false; } }); if (this.options.log) { console.log(`✅ Loaded ${this.proxies.length} valid proxies from ${path.basename(this.proxyFilePath)}`); } this.currentIndex = 0; this.currentProxy = null; this.lastUsedIndex = -1; } catch (error) { throw new Error(`Failed to load proxies: ${error.message}`); } } /** * Setup auto-reload on file change * @private */ setupAutoReload() { if (fs.existsSync(this.proxyFilePath)) { fs.watchFile(this.proxyFilePath, { interval: 5000 }, () => { if (this.options.log) { console.log('🔄 Proxy file changed, reloading...'); } try { this.loadProxies(); } catch (error) { if (this.options.log) { console.error('❌ Failed to reload proxies:', error.message); } } }); } } /** * Create appropriate proxy agent based on protocol * @private * @param {Object} proxy - Proxy object * @param {boolean} isHttps - Whether to use HTTPS * @returns {HttpProxyAgent|HttpsProxyAgent|SocksProxyAgent} */ createProxyAgent(proxy, isHttps = true) { const protocol = proxy.protocol.toLowerCase(); if (protocol === 'socks4' || protocol === 'socks5') { return new SocksProxyAgent(proxy.url); } else if (isHttps) { return new HttpsProxyAgent(proxy.url); } else { return new HttpProxyAgent(proxy.url); } } /** * Get next proxy from the pool even if current is cached * @returns {Object} Proxy object */ getNextProxy() { if (this.proxies.length === 0) { throw new Error('No proxies available'); } let selectedProxy; let selectedIndex; if (this.options.random) { do { selectedIndex = Math.floor(Math.random() * this.proxies.length); } while (selectedIndex === this.lastUsedIndex && this.proxies.length > 1); selectedProxy = this.proxies[selectedIndex]; } else { selectedIndex = this.currentIndex; selectedProxy = this.proxies[this.currentIndex]; this.currentIndex = (this.currentIndex + 1) % this.proxies.length; if (this.currentIndex === 0 && this.lastUsedIndex !== -1) { if (this.options.log) { console.log('🔄 Reached end of proxy list, resetting to beginning'); } } } this.currentProxy = selectedProxy; this.lastUsedIndex = selectedIndex; if (this.options.log) { const cycleInfo = !this.options.random ? ` [${selectedIndex + 1}/${this.proxies.length}]` : ''; console.log(`🔄 Selected proxy${cycleInfo}: ${selectedProxy.protocol}://${selectedProxy.ip}:${selectedProxy.port}${selectedProxy.username ? ' (authenticated)' : ''}`); } return selectedProxy; } /** * Get HTTPS proxy agent * @returns {HttpsProxyAgent|SocksProxyAgent} HTTPS proxy agent */ https() { const proxy = this.getNextProxy(); if (this.options.log) { console.log(`🔗 Using HTTPS proxy: ${proxy.protocol}://${proxy.ip}:${proxy.port}${proxy.username ? ' (authenticated)' : ''}`); } return this.createProxyAgent(proxy, true); } /** * Get HTTP proxy agent * @returns {HttpProxyAgent|SocksProxyAgent} HTTP proxy agent */ http() { const proxy = this.getNextProxy(); if (this.options.log) { console.log(`🔗 Using HTTP proxy: ${proxy.protocol}://${proxy.ip}:${proxy.port}${proxy.username ? ' (authenticated)' : ''}`); } return this.createProxyAgent(proxy, false); } /** * Get axios config with proxy agents * @returns {Object} Axios config object */ config() { const proxy = this.getNextProxy(); if (this.options.log) { console.log(`🚀 Using proxy: ${proxy.protocol}://${proxy.ip}:${proxy.port}${proxy.username ? ' (authenticated)' : ''}`); } const agent = this.createProxyAgent(proxy, true); return { httpsAgent: agent, httpAgent: agent, proxy: false }; } /** * Get current proxy information * @returns {Object|null} Current proxy info */ getCurrentProxy() { return this.currentProxy ? { ip: this.currentProxy.ip, port: this.currentProxy.port, hasAuth: !!(this.currentProxy.username && this.currentProxy.password), protocol: this.currentProxy.protocol || 'http', index: this.lastUsedIndex } : null; } /** * Get all available proxies * @returns {Array} Array of proxy info objects */ list() { return this.proxies.map((proxy, index) => ({ index, ip: proxy.ip, port: proxy.port, hasAuth: !!(proxy.username && proxy.password), protocol: proxy.protocol || 'http', current: index === this.lastUsedIndex })); } /** * Get proxy statistics * @returns {Object} Statistics object */ stats() { const authCount = this.proxies.filter(p => p.username && p.password).length; const protocolCounts = this.proxies.reduce((acc, p) => { acc[p.protocol] = (acc[p.protocol] || 0) + 1; return acc; }, {}); return { total: this.proxies.length, auth: authCount, noAuth: this.proxies.length - authCount, protocols: protocolCounts, currentIndex: this.currentIndex, lastUsedIndex: this.lastUsedIndex, random: this.options.random, file: this.proxyFilePath, autoReload: this.options.autoReload, currentProxy: this.getCurrentProxy() }; } /** * Reload proxies from file */ reload() { this.loadProxies(); if (this.options.log) { console.log('🔄 Proxies reloaded successfully'); } } /** * Test proxy connectivity * @param {string} [testUrl='https://httpbin.org/ip'] - URL to test * @param {number} [timeout=10000] - Request timeout * @param {boolean} [useCurrentProxy=true] - Use current proxy or get next * @returns {Promise<Object>} Test results */ async test(testUrl = 'https://httpbin.org/ip', timeout = 10000, useCurrentProxy = true) { const axios = require('axios'); const proxy = useCurrentProxy ? this.getNextProxy() : this.getNextProxy(true); try { const startTime = Date.now(); const agent = this.createProxyAgent(proxy, true); const response = await axios.get(testUrl, { httpsAgent: agent, httpAgent: agent, timeout, proxy: false }); const responseTime = Date.now() - startTime; return { success: true, proxy: `${proxy.protocol}://${proxy.ip}:${proxy.port}`, time: responseTime, data: response.data }; } catch (error) { return { success: false, proxy: `${proxy.protocol}://${proxy.ip}:${proxy.port}`, error: error.message }; } } /** * Cleanup resources */ destroy() { if (this.options.autoReload && fs.existsSync(this.proxyFilePath)) { fs.unwatchFile(this.proxyFilePath); } this.currentProxy = null; if (this.options.log) { console.log('🧹 ProxyAgent resources cleaned up'); } } } module.exports = ProxyAgent;