katproxy/backend/fetcher.js

166 lines
5.8 KiB
JavaScript

const https = require('https');
const http = require('http');
/**
* Helper to fetch text or JSON over HTTPS with timeout
*/
function fetchUrl(url, timeoutMs = 12000) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
const req = client.get(url, { headers: { 'User-Agent': 'katproxy/1.0' } }, (res) => {
if (res.statusCode < 200 || res.statusCode >= 300) {
res.resume();
return reject(new Error(`Failed to fetch ${url}, status: ${res.statusCode}`));
}
let rawData = '';
res.setEncoding('utf8');
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => resolve(rawData));
});
req.on('error', (err) => reject(err));
req.setTimeout(timeoutMs, () => {
req.destroy();
reject(new Error(`Timeout fetching ${url}`));
});
});
}
/**
* Fetch and merge proxies from multiple premier GitHub repositories
*/
async function fetchAllProxies() {
console.log('[Fetcher] Starting proxy list aggregation...');
const proxyMap = new Map();
// Helper to add or merge a proxy
function addProxy(host, port, protocol, extra = {}) {
if (!host || !port) return;
const cleanHost = String(host).trim();
const cleanPort = parseInt(port, 10);
if (isNaN(cleanPort) || cleanPort <= 0 || cleanPort > 65535) return;
// Basic IP validation regex
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(cleanHost)) return;
const key = `${cleanHost}:${cleanPort}`;
const existing = proxyMap.get(key);
const protoClean = (protocol || 'http').toLowerCase();
if (!existing) {
proxyMap.set(key, {
id: key,
host: cleanHost,
port: cleanPort,
protocols: [protoClean],
country: (extra.country || 'UN').toUpperCase(),
countryName: extra.countryName || 'Unknown',
city: extra.city || '',
org: extra.org || '',
latency: extra.latency != null ? Math.round(extra.latency * 1000) : null,
alive: extra.alive !== undefined ? extra.alive : null,
lastChecked: extra.lastChecked || null,
sources: [extra.source || 'upstream']
});
} else {
if (!existing.protocols.includes(protoClean)) {
existing.protocols.push(protoClean);
}
if (existing.country === 'UN' && extra.country) {
existing.country = extra.country.toUpperCase();
existing.countryName = extra.countryName || existing.countryName;
}
if (!existing.org && extra.org) {
existing.org = extra.org;
}
if (extra.source && !existing.sources.includes(extra.source)) {
existing.sources.push(extra.source);
}
}
}
// 1. Fetch monosans structured JSON (provides rich metadata: country, org, timeout)
try {
console.log('[Fetcher] Fetching monosans/proxy-list JSON...');
const monosansData = await fetchUrl('https://raw.githubusercontent.com/monosans/proxy-list/main/proxies.json', 15000);
const parsed = JSON.parse(monosansData);
if (Array.isArray(parsed)) {
for (const item of parsed) {
const countryCode = item.geolocation?.country?.iso_code || 'UN';
const countryName = item.geolocation?.country?.names?.en || 'Unknown';
const city = item.geolocation?.city?.names?.en || '';
const org = item.asn?.autonomous_system_organization || '';
const latencyMs = item.timeout ? Math.round(item.timeout * 1000) : null;
addProxy(item.host, item.port, item.protocol, {
country: countryCode,
countryName,
city,
org,
latency: item.timeout != null ? item.timeout : null,
alive: true,
source: 'monosans'
});
}
console.log(`[Fetcher] monosans parsed: ${parsed.length} proxies`);
}
} catch (err) {
console.warn('[Fetcher] Warning: monosans JSON fetch failed:', err.message);
}
// 2. TheSpeedX lists (HTTP, SOCKS4, SOCKS5)
const speedXTasks = [
{ url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt', protocol: 'http' },
{ url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks4.txt', protocol: 'socks4' },
{ url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt', protocol: 'socks5' }
];
await Promise.allSettled(
speedXTasks.map(async (task) => {
try {
const text = await fetchUrl(task.url);
const lines = text.split('\n');
let count = 0;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const [host, port] = trimmed.split(':');
if (host && port) {
addProxy(host, port, task.protocol, { source: 'thespeedx' });
count++;
}
}
console.log(`[Fetcher] TheSpeedX ${task.protocol} parsed: ${count} proxies`);
} catch (err) {
console.warn(`[Fetcher] TheSpeedX ${task.protocol} error:`, err.message);
}
})
);
// 3. hookzof SOCKS5 list
try {
const hookzofText = await fetchUrl('https://raw.githubusercontent.com/hookzof/socks5_list/master/proxy.txt');
const lines = hookzofText.split('\n');
let count = 0;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const [host, port] = trimmed.split(':');
if (host && port) {
addProxy(host, port, 'socks5', { source: 'hookzof' });
count++;
}
}
console.log(`[Fetcher] hookzof SOCKS5 parsed: ${count} proxies`);
} catch (err) {
console.warn('[Fetcher] hookzof SOCKS5 error:', err.message);
}
const results = Array.from(proxyMap.values());
console.log(`[Fetcher] Completed aggregation. Total unique proxies: ${results.length}`);
return results;
}
module.exports = {
fetchAllProxies
};