feat: rewrite backend in rust (axum/tokio), remove settings modal from frontend
This commit is contained in:
parent
53743ca70f
commit
310cebbaab
19 changed files with 2895 additions and 1611 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,3 +5,5 @@ node_modules/
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
target/
|
||||
*.pid
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ curl -s "https://katproxy-api.oiupoyt.space/api/raw?protocol=socks5&alive=true"
|
|||
|
||||
## Architecture
|
||||
|
||||
- **Backend (`/backend`)**: Node.js service running on port `5050` with controlled memory allocation (`--max-old-space-size=256`). Includes background socket prober and auto-refresh worker.
|
||||
- **Backend (`/backend`)**: High-performance Rust (Axum + Tokio) service running on port `5050`. Features non-blocking TCP socket prober and automated refresh background worker.
|
||||
- **Frontend (`/public`)**: Vanilla JavaScript, CSS, and HTML with JetBrains Mono typography, auto-refresh countdown, and one-click copy actions.
|
||||
- **Tunnel**: Managed by `cloudflared` routing `katproxy-api.oiupoyt.space` to `localhost:5050`.
|
||||
|
||||
|
|
|
|||
2045
backend/Cargo.lock
generated
Normal file
2045
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
26
backend/Cargo.toml
Normal file
26
backend/Cargo.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "katproxy-backend"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
authors = ["oiupoyt"]
|
||||
description = "High-performance minimal Rust proxy aggregator and latency tester"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7", default-features = false, features = ["tokio", "http1", "http2", "json", "matched-path", "query"] }
|
||||
tokio = { version = "1.38", features = ["full"] }
|
||||
tower = { version = "0.4", features = ["util"] }
|
||||
tower-http = { version = "0.5", features = ["cors", "trace", "fs", "compression-full"] }
|
||||
futures = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
const net = require('net');
|
||||
|
||||
/**
|
||||
* Probe a single proxy host:port via TCP socket to check if open and measure round-trip latency (ms)
|
||||
*/
|
||||
function probeProxy(host, port, timeoutMs = 2000) {
|
||||
return new Promise((resolve) => {
|
||||
const startTime = Date.now();
|
||||
const socket = new net.Socket();
|
||||
|
||||
let resolved = false;
|
||||
const cleanup = () => {
|
||||
socket.removeAllListeners();
|
||||
socket.destroy();
|
||||
};
|
||||
|
||||
socket.setTimeout(timeoutMs);
|
||||
|
||||
socket.connect(port, host, () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
const latency = Date.now() - startTime;
|
||||
cleanup();
|
||||
resolve({ alive: true, latency });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
cleanup();
|
||||
resolve({ alive: false, latency: null, reason: 'timeout' });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
cleanup();
|
||||
resolve({ alive: false, latency: null, reason: err.code || 'error' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a batch of proxies with concurrency limit
|
||||
*/
|
||||
async function batchCheckProxies(proxies, concurrency = 50, timeoutMs = 2000, onProgress = null) {
|
||||
let index = 0;
|
||||
let active = 0;
|
||||
let completed = 0;
|
||||
const total = proxies.length;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (total === 0) return resolve();
|
||||
|
||||
function next() {
|
||||
while (active < concurrency && index < total) {
|
||||
const currentIndex = index++;
|
||||
const proxy = proxies[currentIndex];
|
||||
active++;
|
||||
|
||||
probeProxy(proxy.host, proxy.port, timeoutMs).then((res) => {
|
||||
proxy.alive = res.alive;
|
||||
if (res.alive) {
|
||||
proxy.latency = res.latency;
|
||||
}
|
||||
proxy.lastChecked = Date.now();
|
||||
active--;
|
||||
completed++;
|
||||
|
||||
if (onProgress && completed % 25 === 0) {
|
||||
onProgress(completed, total);
|
||||
}
|
||||
|
||||
if (completed === total) {
|
||||
resolve();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
probeProxy,
|
||||
batchCheckProxies
|
||||
};
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
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
|
||||
};
|
||||
863
backend/package-lock.json
generated
863
backend/package-lock.json
generated
|
|
@ -1,863 +0,0 @@
|
|||
{
|
||||
"name": "katproxy-backend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "katproxy-backend",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.8",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
|
||||
"integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.16.0",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.3",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
|
||||
"integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.5",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.13",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.16.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
|
||||
"integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
|
||||
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.1",
|
||||
"side-channel": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4",
|
||||
"side-channel-list": "^1.0.1",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"name": "katproxy-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "High-performance minimal proxy aggregator and latency checker backend",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"author": "oiupoyt",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
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);
|
||||
});
|
||||
61
backend/src/checker.rs
Normal file
61
backend/src/checker.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::timeout;
|
||||
use tracing::info;
|
||||
use crate::models::Proxy;
|
||||
|
||||
/// Probe a single proxy's host:port over TCP
|
||||
pub async fn probe_proxy(host: &str, port: u16, timeout_duration: Duration) -> (bool, Option<u32>) {
|
||||
let target = format!("{}:{}", host, port);
|
||||
let start = Instant::now();
|
||||
|
||||
match timeout(timeout_duration, TcpStream::connect(&target)).await {
|
||||
Ok(Ok(_stream)) => {
|
||||
let elapsed = start.elapsed().as_millis() as u32;
|
||||
(true, Some(elapsed))
|
||||
}
|
||||
_ => (false, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Concurrently probe a slice of proxies up to limit using a semaphore
|
||||
pub async fn probe_proxies_batch(proxies: &mut [Proxy], concurrency: usize, timeout_duration: Duration) {
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut handles = Vec::new();
|
||||
|
||||
let now_ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
|
||||
for (idx, p) in proxies.iter().enumerate() {
|
||||
let sem = semaphore.clone();
|
||||
let host = p.host.clone();
|
||||
let port = p.port;
|
||||
|
||||
handles.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
let (alive, latency) = probe_proxy(&host, port, timeout_duration).await;
|
||||
(idx, alive, latency)
|
||||
}));
|
||||
}
|
||||
|
||||
let mut completed = 0;
|
||||
let total = handles.len();
|
||||
|
||||
for handle in handles {
|
||||
if let Ok((idx, alive, latency)) = handle.await {
|
||||
proxies[idx].alive = Some(alive);
|
||||
if alive {
|
||||
proxies[idx].latency = latency;
|
||||
}
|
||||
proxies[idx].last_checked = Some(now_ts);
|
||||
completed += 1;
|
||||
if completed % 100 == 0 || completed == total {
|
||||
info!("[Checker] Latency probe progress: {}/{}", completed, total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
192
backend/src/fetcher.rs
Normal file
192
backend/src/fetcher.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use reqwest::Client;
|
||||
use tracing::{info, warn};
|
||||
use crate::models::{MonosansProxy, Proxy};
|
||||
|
||||
pub async fn fetch_all_proxies(client: &Client) -> Vec<Proxy> {
|
||||
info!("[Fetcher] Starting proxy list aggregation...");
|
||||
let mut proxy_map: HashMap<(String, u16), Proxy> = HashMap::new();
|
||||
|
||||
// Helper closure to insert or merge
|
||||
let mut add_proxy = |host: String, port: u16, protocol: String, country: String, country_name: String, city: String, org: String, latency: Option<u32>, alive: Option<bool>, source: String| {
|
||||
let clean_host = host.trim().to_string();
|
||||
if clean_host.is_empty() || port == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let key = (clean_host.clone(), port);
|
||||
let proto_clean = protocol.to_lowercase();
|
||||
|
||||
if let Some(existing) = proxy_map.get_mut(&key) {
|
||||
if !existing.protocols.contains(&proto_clean) {
|
||||
existing.protocols.push(proto_clean);
|
||||
}
|
||||
if existing.country == "UN" && country != "UN" {
|
||||
existing.country = country;
|
||||
if !country_name.is_empty() {
|
||||
existing.country_name = country_name;
|
||||
}
|
||||
}
|
||||
if existing.org.is_empty() && !org.is_empty() {
|
||||
existing.org = org;
|
||||
}
|
||||
if !existing.sources.contains(&source) {
|
||||
existing.sources.push(source);
|
||||
}
|
||||
} else {
|
||||
let id = format!("{}:{}", clean_host, port);
|
||||
proxy_map.insert(key, Proxy {
|
||||
id,
|
||||
host: clean_host,
|
||||
port,
|
||||
protocols: vec![proto_clean],
|
||||
country,
|
||||
country_name,
|
||||
city,
|
||||
org,
|
||||
latency,
|
||||
alive,
|
||||
last_checked: None,
|
||||
sources: vec![source],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Fetch monosans structured JSON
|
||||
let monosans_url = "https://raw.githubusercontent.com/monosans/proxy-list/main/proxies.json";
|
||||
match client.get(monosans_url).timeout(Duration::from_secs(15)).send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
if let Ok(items) = resp.json::<Vec<MonosansProxy>>().await {
|
||||
let count = items.len();
|
||||
for item in items {
|
||||
let country = item.geolocation.as_ref()
|
||||
.and_then(|g| g.country.as_ref())
|
||||
.and_then(|c| c.iso_code.clone())
|
||||
.unwrap_or_else(|| "UN".to_string())
|
||||
.to_uppercase();
|
||||
|
||||
let country_name = item.geolocation.as_ref()
|
||||
.and_then(|g| g.country.as_ref())
|
||||
.and_then(|c| c.names.as_ref())
|
||||
.and_then(|n| n.get("en").cloned())
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
|
||||
let city = item.geolocation.as_ref()
|
||||
.and_then(|g| g.city.as_ref())
|
||||
.and_then(|c| c.names.as_ref())
|
||||
.and_then(|n| n.get("en").cloned())
|
||||
.unwrap_or_default();
|
||||
|
||||
let org = item.asn.as_ref()
|
||||
.and_then(|a| a.autonomous_system_organization.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let latency = item.timeout.map(|t| (t * 1000.0).round() as u32);
|
||||
|
||||
add_proxy(
|
||||
item.host,
|
||||
item.port,
|
||||
item.protocol,
|
||||
country,
|
||||
country_name,
|
||||
city,
|
||||
org,
|
||||
latency,
|
||||
Some(true),
|
||||
"monosans".to_string(),
|
||||
);
|
||||
}
|
||||
info!("[Fetcher] monosans parsed: {} proxies", count);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("[Fetcher] monosans fetch error: {}", e),
|
||||
}
|
||||
|
||||
// 2. TheSpeedX lists (HTTP, SOCKS4, SOCKS5)
|
||||
let speedx_tasks = vec![
|
||||
("https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt", "http"),
|
||||
("https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks4.txt", "socks4"),
|
||||
("https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt", "socks5"),
|
||||
];
|
||||
|
||||
for (url, proto) in speedx_tasks {
|
||||
match client.get(url).timeout(Duration::from_secs(12)).send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
if let Ok(text) = resp.text().await {
|
||||
let mut count = 0;
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let Some((h, p)) = trimmed.split_once(':') {
|
||||
if let Ok(port) = p.parse::<u16>() {
|
||||
add_proxy(
|
||||
h.to_string(),
|
||||
port,
|
||||
proto.to_string(),
|
||||
"UN".to_string(),
|
||||
"Unknown".to_string(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
None,
|
||||
None,
|
||||
"thespeedx".to_string(),
|
||||
);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("[Fetcher] TheSpeedX {} parsed: {} proxies", proto, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("[Fetcher] TheSpeedX {} error: {}", proto, e),
|
||||
}
|
||||
}
|
||||
|
||||
// 3. hookzof SOCKS5 list
|
||||
let hookzof_url = "https://raw.githubusercontent.com/hookzof/socks5_list/master/proxy.txt";
|
||||
match client.get(hookzof_url).timeout(Duration::from_secs(12)).send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
if let Ok(text) = resp.text().await {
|
||||
let mut count = 0;
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let Some((h, p)) = trimmed.split_once(':') {
|
||||
if let Ok(port) = p.parse::<u16>() {
|
||||
add_proxy(
|
||||
h.to_string(),
|
||||
port,
|
||||
"socks5".to_string(),
|
||||
"UN".to_string(),
|
||||
"Unknown".to_string(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
None,
|
||||
None,
|
||||
"hookzof".to_string(),
|
||||
);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("[Fetcher] hookzof SOCKS5 parsed: {} proxies", count);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("[Fetcher] hookzof error: {}", e),
|
||||
}
|
||||
|
||||
let results: Vec<Proxy> = proxy_map.into_values().collect();
|
||||
info!("[Fetcher] Completed aggregation. Total unique proxies: {}", results.len());
|
||||
results
|
||||
}
|
||||
438
backend/src/main.rs
Normal file
438
backend/src/main.rs
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
mod models;
|
||||
mod fetcher;
|
||||
mod checker;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use axum::extract::{Query, State};
|
||||
use axum::http::{header, HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Json, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use reqwest::Client;
|
||||
use tokio::sync::RwLock;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use crate::models::{
|
||||
CountryCount, ProtocolStats, ProxiesQuery, ProxiesResponse, Proxy, RawQuery, StatsResponse,
|
||||
};
|
||||
|
||||
const REFRESH_INTERVAL_SECS: u64 = 15 * 60; // 15 mins
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub proxies: Arc<RwLock<Vec<Proxy>>>,
|
||||
pub last_updated: Arc<AtomicU64>,
|
||||
pub next_refresh_time: Arc<AtomicU64>,
|
||||
pub is_refreshing: Arc<AtomicBool>,
|
||||
pub is_checking: Arc<AtomicBool>,
|
||||
pub start_time: Instant,
|
||||
pub http_client: Client,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
|
||||
.with(tracing_subscriber::fmt::layer().compact())
|
||||
.init();
|
||||
|
||||
let client = Client::builder()
|
||||
.user_agent("katproxy-rs/1.0")
|
||||
.timeout(Duration::from_secs(15))
|
||||
.build()
|
||||
.expect("Failed to build HTTP client");
|
||||
|
||||
let state = AppState {
|
||||
proxies: Arc::new(RwLock::new(Vec::new())),
|
||||
last_updated: Arc::new(AtomicU64::new(0)),
|
||||
next_refresh_time: Arc::new(AtomicU64::new(0)),
|
||||
is_refreshing: Arc::new(AtomicBool::new(false)),
|
||||
is_checking: Arc::new(AtomicBool::new(false)),
|
||||
start_time: Instant::now(),
|
||||
http_client: client,
|
||||
};
|
||||
|
||||
// Spawn background refresh worker
|
||||
let worker_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
// Initial fetch immediately
|
||||
run_refresh_cycle(&worker_state).await;
|
||||
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(REFRESH_INTERVAL_SECS));
|
||||
// First tick completes immediately, skip it
|
||||
interval.tick().await;
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
run_refresh_cycle(&worker_state).await;
|
||||
}
|
||||
});
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
// Serve public directory if exists
|
||||
let public_dir = std::path::Path::new("public");
|
||||
let fallback_public_dir = std::path::Path::new("../public");
|
||||
let static_service = if public_dir.exists() {
|
||||
ServeDir::new(public_dir)
|
||||
} else {
|
||||
ServeDir::new(fallback_public_dir)
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/health", get(health_handler))
|
||||
.route("/api/stats", get(stats_handler))
|
||||
.route("/api/proxies", get(proxies_handler))
|
||||
.route("/api/raw", get(raw_handler))
|
||||
.route("/api/refresh", post(refresh_handler))
|
||||
.fallback_service(static_service)
|
||||
.layer(cors)
|
||||
.with_state(state);
|
||||
|
||||
let port: u16 = std::env::var("PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok())
|
||||
.unwrap_or(5050);
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
info!("[Server] katproxy-backend (Rust) listening on http://{}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr)
|
||||
.await
|
||||
.expect("Failed to bind TCP listener");
|
||||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("Server failed during execution");
|
||||
}
|
||||
|
||||
/// Run full refresh cycle: fetch upstreams, deduplicate, probe top batch
|
||||
async fn run_refresh_cycle(state: &AppState) {
|
||||
if state.is_refreshing.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
info!("[Server] Running background proxy refresh cycle...");
|
||||
let fetched = fetcher::fetch_all_proxies(&state.http_client).await;
|
||||
|
||||
if !fetched.is_empty() {
|
||||
let now_ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
|
||||
let next_refresh = now_ts + (REFRESH_INTERVAL_SECS * 1000);
|
||||
|
||||
{
|
||||
let mut lock = state.proxies.write().await;
|
||||
*lock = fetched;
|
||||
}
|
||||
|
||||
state.last_updated.store(now_ts, Ordering::SeqCst);
|
||||
state.next_refresh_time.store(next_refresh, Ordering::SeqCst);
|
||||
|
||||
// Run background TCP latency probe on top 350 proxies
|
||||
if !state.is_checking.swap(true, Ordering::SeqCst) {
|
||||
let probe_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
info!("[Server] Starting background TCP latency verification...");
|
||||
let mut proxies_clone = {
|
||||
let lock = probe_state.proxies.read().await;
|
||||
lock.clone()
|
||||
};
|
||||
|
||||
let target_len = proxies_clone.len().min(350);
|
||||
if target_len > 0 {
|
||||
checker::probe_proxies_batch(
|
||||
&mut proxies_clone[..target_len],
|
||||
40,
|
||||
Duration::from_millis(2000),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Update store with probed results
|
||||
let mut lock = probe_state.proxies.write().await;
|
||||
for (i, p) in proxies_clone.into_iter().take(target_len).enumerate() {
|
||||
if i < lock.len() {
|
||||
lock[i].alive = p.alive;
|
||||
lock[i].latency = p.latency;
|
||||
lock[i].last_checked = p.last_checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
probe_state.is_checking.store(false, Ordering::SeqCst);
|
||||
info!("[Checker] Latency probing completed.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
state.is_refreshing.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// Handlers
|
||||
|
||||
async fn health_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"service": "katproxy-backend-rs",
|
||||
"uptime": state.start_time.elapsed().as_secs(),
|
||||
"timestamp": now
|
||||
}))
|
||||
}
|
||||
|
||||
async fn stats_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let proxies = state.proxies.read().await;
|
||||
let total = proxies.len();
|
||||
|
||||
let mut alive_count = 0;
|
||||
let mut http_count = 0;
|
||||
let mut socks4_count = 0;
|
||||
let mut socks5_count = 0;
|
||||
let mut latencies = Vec::new();
|
||||
let mut country_map: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
for p in proxies.iter() {
|
||||
if p.alive == Some(true) {
|
||||
alive_count += 1;
|
||||
if let Some(lat) = p.latency {
|
||||
latencies.push(lat);
|
||||
}
|
||||
}
|
||||
|
||||
for proto in &p.protocols {
|
||||
match proto.as_str() {
|
||||
"http" => http_count += 1,
|
||||
"socks4" => socks4_count += 1,
|
||||
"socks5" => socks5_count += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let c = if p.country.is_empty() { "UN" } else { &p.country };
|
||||
*country_map.entry(c.to_string()).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
let mut top_countries: Vec<CountryCount> = country_map
|
||||
.into_iter()
|
||||
.map(|(country, count)| CountryCount { country, count })
|
||||
.collect();
|
||||
top_countries.sort_by(|a, b| b.count.cmp(&a.count));
|
||||
top_countries.truncate(10);
|
||||
|
||||
let avg_latency = if !latencies.is_empty() {
|
||||
(latencies.iter().map(|&v| v as u64).sum::<u64>() / latencies.len() as u64) as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let now_ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
|
||||
let next_refresh = state.next_refresh_time.load(Ordering::SeqCst);
|
||||
let remaining_seconds = if next_refresh > now_ts {
|
||||
(next_refresh - now_ts) / 1000
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let last_updated = state.last_updated.load(Ordering::SeqCst);
|
||||
|
||||
Json(StatsResponse {
|
||||
total,
|
||||
alive_count,
|
||||
protocols: ProtocolStats {
|
||||
http: http_count,
|
||||
socks4: socks4_count,
|
||||
socks5: socks5_count,
|
||||
},
|
||||
top_countries,
|
||||
avg_latency,
|
||||
last_updated: if last_updated > 0 { Some(last_updated) } else { None },
|
||||
next_refresh_time: if next_refresh > 0 { Some(next_refresh) } else { None },
|
||||
remaining_seconds,
|
||||
is_refreshing: state.is_refreshing.load(Ordering::SeqCst),
|
||||
is_checking: state.is_checking.load(Ordering::SeqCst),
|
||||
})
|
||||
}
|
||||
|
||||
async fn proxies_handler(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<ProxiesQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let limit = query.limit.unwrap_or(60).clamp(1, 1000);
|
||||
let page = query.page.unwrap_or(1).max(1);
|
||||
|
||||
let proxies = state.proxies.read().await;
|
||||
|
||||
// Filter
|
||||
let mut filtered: Vec<Proxy> = proxies
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
// Protocol filter
|
||||
if let Some(ref proto) = query.protocol {
|
||||
if proto != "all" && !p.protocols.contains(&proto.to_lowercase()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Country filter
|
||||
if let Some(ref c) = query.country {
|
||||
if c != "all" && p.country != c.to_uppercase() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Alive filter
|
||||
if let Some(ref alv) = query.alive {
|
||||
if alv == "true" && p.alive != Some(true) {
|
||||
return false;
|
||||
} else if alv == "false" && p.alive != Some(false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Max latency
|
||||
if let Some(max_lat) = query.max_latency {
|
||||
if let Some(lat) = p.latency {
|
||||
if lat > max_lat {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Search text
|
||||
if let Some(ref search) = query.search {
|
||||
let q = search.trim().to_lowercase();
|
||||
if !q.is_empty() {
|
||||
let port_str = p.port.to_string();
|
||||
let match_host = p.host.contains(&q);
|
||||
let match_port = port_str.contains(&q);
|
||||
let match_country = p.country.to_lowercase().contains(&q) || p.country_name.to_lowercase().contains(&q);
|
||||
let match_org = p.org.to_lowercase().contains(&q);
|
||||
let match_city = p.city.to_lowercase().contains(&q);
|
||||
|
||||
if !(match_host || match_port || match_country || match_org || match_city) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Sort
|
||||
let sort = query.sort.as_deref().unwrap_or("latency_asc");
|
||||
match sort {
|
||||
"latency_asc" => {
|
||||
filtered.sort_by_key(|p| p.latency.unwrap_or(u32::MAX));
|
||||
}
|
||||
"latency_desc" => {
|
||||
filtered.sort_by(|a, b| {
|
||||
let lat_a = a.latency.unwrap_or(0);
|
||||
let lat_b = b.latency.unwrap_or(0);
|
||||
lat_b.cmp(&lat_a)
|
||||
});
|
||||
}
|
||||
"country" => {
|
||||
filtered.sort_by(|a, b| a.country.cmp(&b.country));
|
||||
}
|
||||
"port" => {
|
||||
filtered.sort_by_key(|p| p.port);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let total = filtered.len();
|
||||
let total_pages = if total == 0 { 1 } else { (total + limit - 1) / limit };
|
||||
let start_idx = (page - 1) * limit;
|
||||
|
||||
let paginated = if start_idx >= total {
|
||||
Vec::new()
|
||||
} else {
|
||||
filtered
|
||||
.into_iter()
|
||||
.skip(start_idx)
|
||||
.take(limit)
|
||||
.collect()
|
||||
};
|
||||
|
||||
Json(ProxiesResponse {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
total_pages,
|
||||
proxies: paginated,
|
||||
})
|
||||
}
|
||||
|
||||
async fn raw_handler(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<RawQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let proxies = state.proxies.read().await;
|
||||
|
||||
let lines: Vec<String> = proxies
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
if let Some(ref proto) = query.protocol {
|
||||
if proto != "all" && !p.protocols.contains(&proto.to_lowercase()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(ref alv) = query.alive {
|
||||
if alv == "true" && p.alive != Some(true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.map(|p| {
|
||||
if query.format.as_deref() == Some("url") {
|
||||
let proto = p.protocols.first().map(|s| s.as_str()).unwrap_or("http");
|
||||
format!("{}://{}:{}", proto, p.host, p.port)
|
||||
} else {
|
||||
format!("{}:{}", p.host, p.port)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let output = lines.join("\n");
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::CONTENT_TYPE, "text/plain; charset=utf-8".parse().unwrap());
|
||||
|
||||
(headers, output)
|
||||
}
|
||||
|
||||
async fn refresh_handler(State(state): State<AppState>) -> Response {
|
||||
if state.is_refreshing.load(Ordering::SeqCst) {
|
||||
return (
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
Json(serde_json::json!({ "error": "Refresh already in progress" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let worker_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
run_refresh_cycle(&worker_state).await;
|
||||
});
|
||||
|
||||
Json(serde_json::json!({
|
||||
"success": true,
|
||||
"message": "Proxy aggregation started in background"
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
117
backend/src/models.rs
Normal file
117
backend/src/models.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Proxy {
|
||||
pub id: String,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub protocols: Vec<String>,
|
||||
pub country: String,
|
||||
#[serde(rename = "countryName")]
|
||||
pub country_name: String,
|
||||
pub city: String,
|
||||
pub org: String,
|
||||
pub latency: Option<u32>,
|
||||
pub alive: Option<bool>,
|
||||
#[serde(rename = "lastChecked")]
|
||||
pub last_checked: Option<u64>,
|
||||
pub sources: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StatsResponse {
|
||||
pub total: usize,
|
||||
#[serde(rename = "aliveCount")]
|
||||
pub alive_count: usize,
|
||||
pub protocols: ProtocolStats,
|
||||
#[serde(rename = "topCountries")]
|
||||
pub top_countries: Vec<CountryCount>,
|
||||
#[serde(rename = "avgLatency")]
|
||||
pub avg_latency: u32,
|
||||
#[serde(rename = "lastUpdated")]
|
||||
pub last_updated: Option<u64>,
|
||||
#[serde(rename = "nextRefreshTime")]
|
||||
pub next_refresh_time: Option<u64>,
|
||||
#[serde(rename = "remainingSeconds")]
|
||||
pub remaining_seconds: u64,
|
||||
#[serde(rename = "isRefreshing")]
|
||||
pub is_refreshing: bool,
|
||||
#[serde(rename = "isChecking")]
|
||||
pub is_checking: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProtocolStats {
|
||||
pub http: usize,
|
||||
pub socks4: usize,
|
||||
pub socks5: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CountryCount {
|
||||
pub country: String,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProxiesResponse {
|
||||
pub total: usize,
|
||||
pub page: usize,
|
||||
pub limit: usize,
|
||||
#[serde(rename = "totalPages")]
|
||||
pub total_pages: usize,
|
||||
pub proxies: Vec<Proxy>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ProxiesQuery {
|
||||
pub protocol: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub alive: Option<String>,
|
||||
#[serde(rename = "maxLatency")]
|
||||
pub max_latency: Option<u32>,
|
||||
pub search: Option<String>,
|
||||
pub sort: Option<String>,
|
||||
pub limit: Option<usize>,
|
||||
pub page: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct RawQuery {
|
||||
pub protocol: Option<String>,
|
||||
pub alive: Option<String>,
|
||||
pub format: Option<String>,
|
||||
}
|
||||
|
||||
// Struct for parsing monosans proxies.json
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct MonosansProxy {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub protocol: String,
|
||||
pub timeout: Option<f64>,
|
||||
pub asn: Option<MonosansAsn>,
|
||||
pub geolocation: Option<MonosansGeo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct MonosansAsn {
|
||||
pub autonomous_system_organization: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct MonosansGeo {
|
||||
pub country: Option<MonosansCountry>,
|
||||
pub city: Option<MonosansCity>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct MonosansCountry {
|
||||
pub iso_code: Option<String>,
|
||||
pub names: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct MonosansCity {
|
||||
pub names: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
|
|
@ -1,22 +1,18 @@
|
|||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Acquire Android partial wake-lock so CPU does not sleep when screen turns off
|
||||
/data/data/com.termux/files/usr/bin/termux-wake-lock 2>/dev/null || true
|
||||
|
||||
# Stop existing backend instance if running
|
||||
if [ -f katproxy.pid ]; then
|
||||
kill $(cat katproxy.pid) 2>/dev/null || true
|
||||
rm -f katproxy.pid
|
||||
fi
|
||||
sleep 0.5
|
||||
|
||||
# Rotate log file to save tablet storage
|
||||
if [ -f katproxy.log ]; then
|
||||
tail -n 200 katproxy.log > katproxy.log.tmp 2>/dev/null && mv katproxy.log.tmp katproxy.log 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Start backend with 256MB capped V8 heap for ARM Android stability
|
||||
PORT=5050 nohup node --max-old-space-size=256 server.js >> katproxy.log 2>&1 &
|
||||
PORT=5050 nohup ./target/release/katproxy-backend >> katproxy.log 2>&1 &
|
||||
echo $! > katproxy.pid
|
||||
echo "katproxy-backend running with PID $(cat katproxy.pid) on port 5050"
|
||||
echo "katproxy-backend (rust) running with PID $(cat katproxy.pid) on port 5050"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "=== KatProxy Backend Status ==="
|
||||
echo "=== KatProxy Backend (Rust) Status ==="
|
||||
if [ -f katproxy.pid ]; then
|
||||
PID=$(cat katproxy.pid)
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
|
|
|
|||
14
package.json
14
package.json
|
|
@ -3,15 +3,11 @@
|
|||
"version": "1.0.0",
|
||||
"description": "Minimal, sleek multi-source proxy list stream & active latency tester",
|
||||
"scripts": {
|
||||
"start": "node backend/server.js",
|
||||
"backend": "cd backend && npm start",
|
||||
"dev": "cd backend && npm run dev"
|
||||
"start": "cd backend && cargo run --release",
|
||||
"build": "cd backend && cargo build --release",
|
||||
"dev": "cd backend && cargo run"
|
||||
},
|
||||
"keywords": ["proxy", "socks5", "socks4", "http", "stream", "minimal"],
|
||||
"keywords": ["proxy", "socks5", "socks4", "http", "stream", "minimal", "rust", "axum"],
|
||||
"author": "oiupoyt",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2"
|
||||
}
|
||||
"license": "MIT"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@
|
|||
*/
|
||||
|
||||
// Configuration & State
|
||||
const DEFAULT_TUNNEL_URL = 'https://katproxy-api.oiupoyt.space';
|
||||
const DEFAULT_API_URL = 'https://katproxy-api.oiupoyt.space';
|
||||
const LOCAL_DEV_URL = 'http://localhost:5050';
|
||||
|
||||
let apiBase = localStorage.getItem('katproxy_api_url') || (
|
||||
window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||
const apiBase = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
|
||||
? LOCAL_DEV_URL
|
||||
: DEFAULT_TUNNEL_URL
|
||||
);
|
||||
: DEFAULT_API_URL;
|
||||
|
||||
const state = {
|
||||
protocol: 'all',
|
||||
|
|
@ -35,7 +33,6 @@ const el = {
|
|||
timerText: document.getElementById('timer-text'),
|
||||
progressCircle: document.getElementById('progress-circle'),
|
||||
btnManualRefresh: document.getElementById('btn-manual-refresh'),
|
||||
btnSettings: document.getElementById('btn-settings'),
|
||||
protocolFilters: document.getElementById('protocol-filters'),
|
||||
toggleAlive: document.getElementById('toggle-alive'),
|
||||
searchInput: document.getElementById('search-input'),
|
||||
|
|
@ -53,11 +50,6 @@ const el = {
|
|||
btnNext: document.getElementById('btn-next'),
|
||||
pageIndicator: document.getElementById('page-indicator'),
|
||||
linkRawApi: document.getElementById('link-raw-api'),
|
||||
settingsModal: document.getElementById('settings-modal'),
|
||||
modalClose: document.getElementById('modal-close'),
|
||||
modalCancel: document.getElementById('modal-cancel'),
|
||||
modalSave: document.getElementById('modal-save'),
|
||||
apiUrlInput: document.getElementById('api-url-input'),
|
||||
toastContainer: document.getElementById('toast-container')
|
||||
};
|
||||
|
||||
|
|
@ -469,35 +461,6 @@ function initEvents() {
|
|||
state.page++;
|
||||
loadProxies();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
// Settings Modal
|
||||
el.btnSettings.addEventListener('click', () => {
|
||||
el.apiUrlInput.value = apiBase;
|
||||
el.settingsModal.classList.remove('hidden');
|
||||
});
|
||||
|
||||
el.modalClose.addEventListener('click', () => el.settingsModal.classList.add('hidden'));
|
||||
el.modalCancel.addEventListener('click', () => el.settingsModal.classList.add('hidden'));
|
||||
|
||||
// Quick preset buttons in modal
|
||||
el.settingsModal.querySelectorAll('.quick-endpoints button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
el.apiUrlInput.value = btn.dataset.url;
|
||||
});
|
||||
});
|
||||
|
||||
el.modalSave.addEventListener('click', () => {
|
||||
const newUrl = el.apiUrlInput.value.trim().replace(/\/+$/, '');
|
||||
if (newUrl) {
|
||||
apiBase = newUrl;
|
||||
localStorage.setItem('katproxy_api_url', newUrl);
|
||||
showToast(`API endpoint updated to ${newUrl}`);
|
||||
el.settingsModal.classList.add('hidden');
|
||||
loadStats();
|
||||
loadProxies();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,12 +56,6 @@
|
|||
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-icon" id="btn-settings" title="API Settings">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -171,30 +165,7 @@
|
|||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div class="modal-overlay hidden" id="settings-modal">
|
||||
<div class="modal-card">
|
||||
<div class="modal-header">
|
||||
<h3>Backend API Configuration</h3>
|
||||
<button class="modal-close" id="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label for="api-url-input">API Base URL</label>
|
||||
<input type="text" id="api-url-input" class="modal-input" placeholder="https://katproxy-api.oiupoyt.space">
|
||||
<p class="modal-hint">Cloudflare Tunnel endpoint (public) or direct LAN address (e.g. <code>http://192.168.0.104:5050</code>).</p>
|
||||
|
||||
<div class="quick-endpoints">
|
||||
<button class="btn btn-xs" data-url="https://katproxy-api.oiupoyt.space">Cloudflare Tunnel</button>
|
||||
<button class="btn btn-xs" data-url="http://192.168.0.104:5050">Local LAN</button>
|
||||
<button class="btn btn-xs" data-url="http://localhost:5050">Localhost</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="modal-cancel">Cancel</button>
|
||||
<button class="btn btn-primary" id="modal-save">Save & Reconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
|
|
|||
105
public/style.css
105
public/style.css
|
|
@ -783,111 +783,6 @@ body {
|
|||
color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
background: #121216;
|
||||
border: 1px solid var(--border-hover);
|
||||
border-radius: var(--radius-lg);
|
||||
max-width: 440px;
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
||||
animation: modal-in 0.18s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modal-in {
|
||||
from { opacity: 0; transform: scale(0.96); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-body label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
width: 100%;
|
||||
background: #09090b;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 8px 12px;
|
||||
color: #fff;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-emerald);
|
||||
}
|
||||
|
||||
.modal-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quick-endpoints {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Toast System */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue