katproxy/backend/server.js

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);
});