256 lines
No EOL
9.8 KiB
TypeScript
256 lines
No EOL
9.8 KiB
TypeScript
import { Logger } from '@stock-bot/logger';
|
|
import createCache, { type CacheProvider } from '@stock-bot/cache';
|
|
import { HttpClient, ProxyInfo } from '@stock-bot/http';
|
|
import pLimit from 'p-limit';
|
|
|
|
export class ProxyService {
|
|
private logger = new Logger('proxy-service');
|
|
private cache: CacheProvider = createCache('hybrid');
|
|
private httpClient: HttpClient;
|
|
private readonly concurrencyLimit = pLimit(10);
|
|
private readonly CACHE_KEY = 'proxy';
|
|
private readonly CACHE_TTL = 86400; // 24 hours
|
|
private readonly CHECK_TIMEOUT = 5000;
|
|
private readonly CHECK_URL = 'https://proxy-detection.stare.gg/?api_key=bd406bf53ddc6abe1d9de5907830a955';
|
|
private readonly PROXY_SOURCES = [
|
|
// { url: 'https://raw.githubusercontent.com/BreakingTechFr/Proxy_Free/refs/heads/main/proxies/http.txt', protocol: 'http' },
|
|
// {url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt',protocol: 'http', },
|
|
// {url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt',protocol: 'http', },
|
|
// {url: 'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt',protocol: 'http', },
|
|
// {url: 'https://raw.githubusercontent.com/TuanMinPay/live-proxy/master/http.txt',protocol: 'http', },
|
|
// {url: 'https://raw.githubusercontent.com/casals-ar/proxy-list/main/http',protocol: 'http', },
|
|
// {url: 'https://raw.githubusercontent.com/prxchk/proxy-list/main/http.txt',protocol: 'http', },
|
|
// {url: 'https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt',protocol: 'http', },
|
|
// {url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/http.txt',protocol: 'http', },
|
|
// {url: 'https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/proxies.txt',protocol: 'http', },
|
|
// {url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt',protocol: 'https', },
|
|
{url: 'https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/socks4.txt',protocol: 'socks4', },
|
|
{url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks4.txt',protocol: 'socks4', },
|
|
{url: 'https://raw.githubusercontent.com/TuanMinPay/live-proxy/master/socks4.txt',protocol: 'socks4', },
|
|
{url: 'https://raw.githubusercontent.com/casals-ar/proxy-list/main/socks4',protocol: 'socks4', },
|
|
{url: 'https://raw.githubusercontent.com/prxchk/proxy-list/main/socks4.txt',protocol: 'socks4', },
|
|
{url: 'https://raw.githubusercontent.com/BreakingTechFr/Proxy_Free/refs/heads/main/proxies/socks4.txt',protocol: 'socks4', },
|
|
{url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/socks4.txt',protocol: 'socks4', },
|
|
{url: 'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/socks4.txt',protocol: 'socks4', },
|
|
{url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt',protocol: 'socks5', },
|
|
{url: 'https://raw.githubusercontent.com/hookzof/socks5_list/master/proxy.txt',protocol: 'socks5', },
|
|
{url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/socks5.txt',protocol: 'socks5', },
|
|
{url: 'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/socks5.txt',protocol: 'socks5', },
|
|
{url: 'https://raw.githubusercontent.com/TuanMinPay/live-proxy/master/socks5.txt',protocol: 'socks5', },
|
|
{url: 'https://raw.githubusercontent.com/casals-ar/proxy-list/main/socks5',protocol: 'socks5', },
|
|
{url: 'https://raw.githubusercontent.com/prxchk/proxy-list/main/socks5.txt',protocol: 'socks5', },
|
|
{url: 'https://raw.githubusercontent.com/hookzof/socks5_list/master/proxy.txt',protocol: 'socks5', },
|
|
{url: 'https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/socks5.txt',protocol: 'socks5', },
|
|
{url: 'https://raw.githubusercontent.com/BreakingTechFr/Proxy_Free/refs/heads/main/proxies/socks5.txt',protocol: 'socks5', },
|
|
]
|
|
|
|
constructor() {
|
|
this.httpClient = new HttpClient({
|
|
timeout: this.CHECK_TIMEOUT,
|
|
}, this.logger);
|
|
|
|
this.logger.info('ProxyService initialized');
|
|
}
|
|
|
|
|
|
async fetchProxiesFromSources() : Promise<boolean> {
|
|
const sources = this.PROXY_SOURCES.map(source =>
|
|
this.concurrencyLimit(() => this.fetchProxiesFromSource(source))
|
|
)
|
|
const result = await Promise.all(sources);
|
|
const allProxies: ProxyInfo[] = result.flat();
|
|
await this.checkProxies(this.removeDuplicateProxies(allProxies))
|
|
return true
|
|
}
|
|
|
|
private removeDuplicateProxies(proxies: ProxyInfo[]): ProxyInfo[] {
|
|
const seen = new Set<string>();
|
|
const unique: ProxyInfo[] = [];
|
|
|
|
for (const proxy of proxies) {
|
|
const key = `${proxy.protocol}://${proxy.host}:${proxy.port}`;
|
|
if (!seen.has(key)) {
|
|
seen.add(key);
|
|
unique.push(proxy);
|
|
}
|
|
}
|
|
|
|
return unique;
|
|
}
|
|
|
|
|
|
async fetchProxiesFromSource(source: { url: string; protocol: string }): Promise<ProxyInfo[]> {
|
|
const allProxies: ProxyInfo[] = [];
|
|
|
|
try {
|
|
this.logger.info(`Fetching proxies from ${source.url}`);
|
|
|
|
const response = await this.httpClient.get(source.url, {
|
|
timeout: 10000
|
|
});
|
|
|
|
if (response.status !== 200) {
|
|
this.logger.warn(`Failed to fetch from ${source.url}: ${response.status}`);
|
|
return []
|
|
}
|
|
|
|
const text = response.data;
|
|
const lines = text.split('\n').filter((line: string) => line.trim());
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
|
|
// Parse formats like "host:port" or "host:port:user:pass"
|
|
const parts = trimmed.split(':');
|
|
if (parts.length >= 2) {
|
|
const proxy: ProxyInfo = {
|
|
protocol: source.protocol as 'http' | 'https' | 'socks4' | 'socks5',
|
|
host: parts[0],
|
|
port: parseInt(parts[1])
|
|
};
|
|
|
|
if (!isNaN(proxy.port) && proxy.host) {
|
|
allProxies.push(proxy);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.logger.info(`Parsed ${allProxies.length} proxies from ${source.url}`);
|
|
|
|
} catch (error) {
|
|
this.logger.error(`Error fetching proxies from ${source.url}`, error);
|
|
return [];
|
|
}
|
|
|
|
this.logger.info(`Total proxies fetched: ${allProxies.length}`);
|
|
return allProxies;
|
|
}
|
|
|
|
/**
|
|
* Check if a proxy is working
|
|
*/
|
|
async checkProxy(proxy: ProxyInfo): Promise<ProxyInfo> {
|
|
console.log('Checking proxy:', `${proxy.protocol}://${proxy.host}:${proxy.port}`, this.concurrencyLimit.activeCount, this.concurrencyLimit.pendingCount);
|
|
try {
|
|
|
|
// Test the proxy
|
|
const response = await this.httpClient.get(this.CHECK_URL, {
|
|
proxy,
|
|
timeout: this.CHECK_TIMEOUT
|
|
});
|
|
|
|
const isWorking = response.status >= 200 && response.status < 300;
|
|
|
|
const result: ProxyInfo = {
|
|
...proxy,
|
|
isWorking,
|
|
checkedAt: new Date(),
|
|
responseTime: response.responseTime,
|
|
};
|
|
|
|
if (isWorking) {
|
|
await this.cache.set(`${this.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`, result, this.CACHE_TTL);
|
|
} else {
|
|
await this.cache.del(`${this.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`);
|
|
}
|
|
|
|
this.logger.debug('Proxy check completed', {
|
|
host: proxy.host,
|
|
port: proxy.port,
|
|
isWorking,
|
|
});
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
|
const result: ProxyInfo = {
|
|
...proxy,
|
|
isWorking: false,
|
|
error: errorMessage,
|
|
checkedAt: new Date()
|
|
};
|
|
|
|
// Cache failed result for shorter time
|
|
// await this.cache.set(cacheKey, result, 300); // 5 minutes
|
|
// await this.cache.del(`${this.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`);
|
|
|
|
this.logger.debug('Proxy check failed', {
|
|
host: proxy.host,
|
|
port: proxy.port,
|
|
error: errorMessage
|
|
});
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check multiple proxies concurrently
|
|
*/
|
|
async checkProxies(proxies: ProxyInfo[]): Promise<ProxyInfo[]> {
|
|
this.logger.info('Checking proxies', { count: proxies.length });
|
|
|
|
const checkPromises = proxies.map(proxy =>
|
|
this.concurrencyLimit(() => this.checkProxy(proxy))
|
|
);
|
|
|
|
const results = await Promise.all(checkPromises);
|
|
const workingCount = results.filter(r => r.isWorking).length;
|
|
|
|
this.logger.info('Proxy check completed', {
|
|
total: proxies.length,
|
|
working: workingCount,
|
|
failed: proxies.length - workingCount
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Get a random working proxy from cache
|
|
*/
|
|
async getWorkingProxy(): Promise<ProxyInfo | null> {
|
|
try {
|
|
// Note: This is a simplified implementation
|
|
// In production, you'd want to maintain a working proxies list
|
|
this.logger.warn('getWorkingProxy not fully implemented - requires proxy list management');
|
|
return null;
|
|
} catch (error) {
|
|
this.logger.error('Error getting working proxy', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add proxies to check and cache
|
|
*/
|
|
async addProxies(proxies: ProxyInfo[]): Promise<void> {
|
|
this.logger.info('Adding proxies for validation', { count: proxies.length });
|
|
|
|
// Start background validation
|
|
this.checkProxies(proxies).catch(error => {
|
|
this.logger.error('Error in background proxy validation', error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear proxy cache
|
|
*/
|
|
async clearCache(): Promise<void> {
|
|
this.logger.info('Clearing proxy cache');
|
|
// Note: Cache provider limitations - would need proper key tracking
|
|
}
|
|
|
|
/**
|
|
* Shutdown service
|
|
*/
|
|
async shutdown(): Promise<void> {
|
|
this.logger.info('Shutting down ProxyService');
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const proxyService = new ProxyService(); |