@rynn-k/proxy-agent
Version:
Efficient proxy rotation agent for Node.js with seamless axios integration
325 lines (280 loc) • 8.97 kB
JavaScript
const { HttpsProxyAgent } = require('https-proxy-agent');
const { HttpProxyAgent } = require('http-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
*/
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,
...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);
} 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);
}
}
});
}
}
/**
* 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.ip}:${selectedProxy.port}${selectedProxy.username ? ' (authenticated)' : ''}`);
}
return selectedProxy;
}
/**
* Get HTTPS proxy agent
* @returns {HttpsProxyAgent} HTTPS proxy agent
*/
https() {
const proxy = this.getNextProxy();
if (this.options.log) {
console.log(`🔗 Using HTTPS proxy: ${proxy.ip}:${proxy.port}${proxy.username ? ' (authenticated)' : ''}`);
}
return new HttpsProxyAgent(proxy.url);
}
/**
* Get HTTP proxy agent
* @returns {HttpProxyAgent} HTTP proxy agent
*/
http() {
const proxy = this.getNextProxy();
if (this.options.log) {
console.log(`🔗 Using HTTP proxy: ${proxy.ip}:${proxy.port}${proxy.username ? ' (authenticated)' : ''}`);
}
return new HttpProxyAgent(proxy.url);
}
/**
* 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.ip}:${proxy.port}${proxy.username ? ' (authenticated)' : ''}`);
}
return {
httpsAgent: new HttpsProxyAgent(proxy.url),
httpAgent: new HttpProxyAgent(proxy.url),
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;
return {
total: this.proxies.length,
auth: authCount,
noAuth: this.proxies.length - authCount,
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 response = await axios.get(testUrl, {
httpsAgent: new HttpsProxyAgent(proxy.url),
httpAgent: new HttpProxyAgent(proxy.url),
timeout,
proxy: false
});
const responseTime = Date.now() - startTime;
return {
success: true,
proxy: `${proxy.ip}:${proxy.port}`,
time: responseTime,
data: response.data
};
} catch (error) {
return {
success: false,
proxy: `${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;