277 lines
7.7 KiB
JavaScript
277 lines
7.7 KiB
JavaScript
const express = require('express');
|
|
const cors = require('cors');
|
|
const path = require('path');
|
|
const { fetchAllProxies } = require('./fetcher');
|
|
const { batchCheckProxies } = require('./checker');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 5050;
|
|
const REFRESH_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
|
|
|
// State
|
|
let proxyStore = [];
|
|
let lastUpdated = null;
|
|
let nextRefreshTime = null;
|
|
let isRefreshing = false;
|
|
let isChecking = false;
|
|
|
|
// Middleware
|
|
app.use(cors({
|
|
origin: '*',
|
|
methods: ['GET', 'POST', 'OPTIONS'],
|
|
allowedHeaders: ['Content-Type', 'Authorization']
|
|
}));
|
|
app.use(express.json());
|
|
|
|
// Optional: serve static frontend if public directory exists
|
|
const publicDir = path.join(__dirname, '../public');
|
|
app.use(express.static(publicDir));
|
|
|
|
/**
|
|
* Trigger full refresh cycle
|
|
*/
|
|
async function refreshCycle() {
|
|
if (isRefreshing) return;
|
|
isRefreshing = true;
|
|
console.log(`[Server] Starting auto-refresh at ${new Date().toISOString()}`);
|
|
|
|
try {
|
|
const rawList = await fetchAllProxies();
|
|
if (rawList && rawList.length > 0) {
|
|
proxyStore = rawList;
|
|
lastUpdated = Date.now();
|
|
nextRefreshTime = Date.now() + REFRESH_INTERVAL_MS;
|
|
console.log(`[Server] Aggregated ${proxyStore.length} proxies successfully.`);
|
|
|
|
// Launch background latency checker on the first batch of proxies
|
|
// Check high-priority proxies (e.g. up to 300) without hogging CPU/sockets
|
|
probeTopProxiesInBackground();
|
|
}
|
|
} catch (err) {
|
|
console.error('[Server] Refresh cycle error:', err);
|
|
} finally {
|
|
isRefreshing = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Background latency verification pool
|
|
*/
|
|
async function probeTopProxiesInBackground() {
|
|
if (isChecking || proxyStore.length === 0) return;
|
|
isChecking = true;
|
|
console.log('[Server] Starting background TCP latency verification...');
|
|
|
|
try {
|
|
// Select top 300 proxies to probe (or all if fewer)
|
|
const targetProxies = proxyStore.slice(0, 350);
|
|
await batchCheckProxies(targetProxies, 40, 2000, (completed, total) => {
|
|
if (completed % 100 === 0 || completed === total) {
|
|
console.log(`[Checker] Latency probe progress: ${completed}/${total}`);
|
|
}
|
|
});
|
|
console.log('[Checker] Latency probing completed.');
|
|
} catch (err) {
|
|
console.error('[Checker] Latency probing error:', err);
|
|
} finally {
|
|
isChecking = false;
|
|
}
|
|
}
|
|
|
|
// Routes
|
|
|
|
// 1. Health
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({
|
|
status: 'ok',
|
|
service: 'katproxy-backend',
|
|
uptime: Math.round(process.uptime()),
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
// 2. Stats
|
|
app.get('/api/stats', (req, res) => {
|
|
const total = proxyStore.length;
|
|
const aliveList = proxyStore.filter(p => p.alive === true);
|
|
|
|
const protocols = { http: 0, socks4: 0, socks5: 0 };
|
|
const countryCounts = {};
|
|
|
|
for (const p of proxyStore) {
|
|
for (const proto of p.protocols) {
|
|
if (protocols[proto] !== undefined) protocols[proto]++;
|
|
}
|
|
const c = p.country || 'UN';
|
|
countryCounts[c] = (countryCounts[c] || 0) + 1;
|
|
}
|
|
|
|
// Top 10 countries
|
|
const topCountries = Object.entries(countryCounts)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 10)
|
|
.map(([country, count]) => ({ country, count }));
|
|
|
|
// Average latency of alive proxies
|
|
const latencies = aliveList.filter(p => p.latency != null).map(p => p.latency);
|
|
const avgLatency = latencies.length > 0
|
|
? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length)
|
|
: 0;
|
|
|
|
res.json({
|
|
total,
|
|
aliveCount: aliveList.length,
|
|
protocols,
|
|
topCountries,
|
|
avgLatency,
|
|
lastUpdated,
|
|
nextRefreshTime,
|
|
remainingSeconds: nextRefreshTime ? Math.max(0, Math.round((nextRefreshTime - Date.now()) / 1000)) : 0,
|
|
isRefreshing,
|
|
isChecking
|
|
});
|
|
});
|
|
|
|
// 3. Proxies list with filter, search, sort, pagination
|
|
app.get('/api/proxies', (req, res) => {
|
|
let {
|
|
protocol,
|
|
country,
|
|
alive,
|
|
maxLatency,
|
|
search,
|
|
sort = 'latency_asc',
|
|
limit = 100,
|
|
page = 1
|
|
} = req.query;
|
|
|
|
limit = Math.min(Math.max(parseInt(limit, 10) || 100, 1), 1000);
|
|
page = Math.max(parseInt(page, 10) || 1, 1);
|
|
|
|
let results = proxyStore;
|
|
|
|
// Filter: protocol
|
|
if (protocol && protocol !== 'all') {
|
|
const protoLower = protocol.toLowerCase();
|
|
results = results.filter(p => p.protocols.includes(protoLower));
|
|
}
|
|
|
|
// Filter: country
|
|
if (country && country !== 'all') {
|
|
const countryUpper = country.toUpperCase();
|
|
results = results.filter(p => p.country === countryUpper);
|
|
}
|
|
|
|
// Filter: alive
|
|
if (alive === 'true') {
|
|
results = results.filter(p => p.alive === true);
|
|
} else if (alive === 'false') {
|
|
results = results.filter(p => p.alive === false);
|
|
}
|
|
|
|
// Filter: max latency
|
|
if (maxLatency) {
|
|
const maxMs = parseInt(maxLatency, 10);
|
|
if (!isNaN(maxMs)) {
|
|
results = results.filter(p => p.latency != null && p.latency <= maxMs);
|
|
}
|
|
}
|
|
|
|
// Filter: search query (IP, port, country name, org, city)
|
|
if (search && typeof search === 'string' && search.trim()) {
|
|
const q = search.trim().toLowerCase();
|
|
results = results.filter(p =>
|
|
p.host.includes(q) ||
|
|
String(p.port).includes(q) ||
|
|
p.country.toLowerCase().includes(q) ||
|
|
(p.countryName && p.countryName.toLowerCase().includes(q)) ||
|
|
(p.org && p.org.toLowerCase().includes(q)) ||
|
|
(p.city && p.city.toLowerCase().includes(q))
|
|
);
|
|
}
|
|
|
|
// Sort
|
|
results = results.slice().sort((a, b) => {
|
|
if (sort === 'latency_asc') {
|
|
const latA = a.latency != null ? a.latency : 999999;
|
|
const latB = b.latency != null ? b.latency : 999999;
|
|
return latA - latB;
|
|
}
|
|
if (sort === 'latency_desc') {
|
|
const latA = a.latency != null ? a.latency : -1;
|
|
const latB = b.latency != null ? b.latency : -1;
|
|
return latB - latA;
|
|
}
|
|
if (sort === 'country') {
|
|
return (a.country || '').localeCompare(b.country || '');
|
|
}
|
|
if (sort === 'port') {
|
|
return a.port - b.port;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
const totalMatches = results.length;
|
|
const startIndex = (page - 1) * limit;
|
|
const paginated = results.slice(startIndex, startIndex + limit);
|
|
|
|
res.json({
|
|
total: totalMatches,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(totalMatches / limit),
|
|
proxies: paginated
|
|
});
|
|
});
|
|
|
|
// 4. Manual Refresh Trigger
|
|
app.post('/api/refresh', async (req, res) => {
|
|
if (isRefreshing) {
|
|
return res.status(429).json({ error: 'Refresh already in progress' });
|
|
}
|
|
refreshCycle().catch(console.error);
|
|
res.json({ success: true, message: 'Proxy aggregation started in background' });
|
|
});
|
|
|
|
// 5. Raw Text Export (for curl / bash scripts)
|
|
app.get('/api/raw', (req, res) => {
|
|
const { protocol, alive, format } = req.query;
|
|
let results = proxyStore;
|
|
|
|
if (protocol && protocol !== 'all') {
|
|
results = results.filter(p => p.protocols.includes(protocol.toLowerCase()));
|
|
}
|
|
if (alive === 'true') {
|
|
results = results.filter(p => p.alive === true);
|
|
}
|
|
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
if (format === 'url') {
|
|
const lines = results.map(p => {
|
|
const proto = p.protocols[0] || 'http';
|
|
return `${proto}://${p.host}:${p.port}`;
|
|
});
|
|
return res.send(lines.join('\n'));
|
|
}
|
|
|
|
// Default: IP:PORT
|
|
const lines = results.map(p => `${p.host}:${p.port}`);
|
|
res.send(lines.join('\n'));
|
|
});
|
|
|
|
// Fallback for spa / static index if accessed directly
|
|
app.get('*', (req, res, next) => {
|
|
if (req.accepts('html')) {
|
|
return res.sendFile(path.join(publicDir, 'index.html'), (err) => {
|
|
if (err) res.status(404).send('KatProxy API Server is running.');
|
|
});
|
|
}
|
|
next();
|
|
});
|
|
|
|
// Start server and launch initial fetch
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`[Server] katproxy backend listening on http://0.0.0.0:${PORT}`);
|
|
refreshCycle();
|
|
setInterval(refreshCycle, REFRESH_INTERVAL_MS);
|
|
});
|