Commit 730ded05 authored by Vitaly Lipatov's avatar Vitaly Lipatov

web-api: unify speed test with gateway check, add JSON mode for /api/check

When file URL is given, speed test replaces gateway HTTP checks as the availability indicator — one unified section instead of two separate lists. Support dual response format: SSE for browsers (Accept: text/event-stream), JSON for curl/scripts (default). This prevents breaking existing API clients. Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 0f44225e
......@@ -323,10 +323,21 @@ def _check_speed(name, proxy, file_url):
def check_site(domain, file_url=None):
"""Check domain: resolve IPs, find in route lists, whois, test gateways."""
"""Check domain: resolve IPs, find in route lists, whois, test gateways.
When file_url is given, skip gateway HTTP checks — speed test replaces them.
"""
url = "https://%s/" % domain
# Check if domain has AAAA records
checks = {}
throttle = {}
whois_info = []
if file_url:
# File URL mode: only resolve, routes, whois (speed test is streamed separately)
whois_info = get_whois(domain)
else:
# Domain mode: full gateway checks + throttle detection
has_v6 = False
try:
socket.getaddrinfo(domain, None, socket.AF_INET6, socket.SOCK_STREAM)
......@@ -334,8 +345,6 @@ def check_site(domain, file_url=None):
except socket.gaierror:
pass
# Phase 1: gateway checks (IPv4 + IPv6), whois, and fetch assets in parallel
checks = {}
workers = len(CHECK_GATEWAYS) * 2 + 2
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
gw_futures = []
......@@ -344,7 +353,6 @@ def check_site(domain, file_url=None):
if has_v6 and proxy_v6:
gw_futures.append(pool.submit(_check_one, name, proxy_v6, url, "-6"))
whois_future = pool.submit(get_whois, domain)
# Fetch HTML and parse assets via first available gateway
assets_future = pool.submit(_find_assets, CHECK_GATEWAYS[0][1], url)
for future in concurrent.futures.as_completed(gw_futures):
......@@ -358,7 +366,6 @@ def check_site(domain, file_url=None):
checks[name]["status_v6"] = status
checks[name]["http_code_v6"] = code
# Fill NOIP for gateways without IPv6 result
for name, _pv4, _pv6 in CHECK_GATEWAYS:
if name not in checks:
checks[name] = {}
......@@ -369,8 +376,6 @@ def check_site(domain, file_url=None):
whois_info = whois_future.result()
assets = assets_future.result()
# Phase 2: throttle check — download first asset via all gateways in parallel
throttle = {}
if assets:
asset_url = assets[0]
with concurrent.futures.ThreadPoolExecutor(max_workers=len(CHECK_GATEWAYS)) as pool:
......@@ -387,8 +392,10 @@ def check_site(domain, file_url=None):
routes = find_in_routes(domain, ips)
result = {
"domain": domain, "ips": ips, "routes": routes,
"whois": whois_info, "checks": checks,
"whois": whois_info,
}
if checks:
result["checks"] = checks
if file_url:
result["file_url"] = file_url
if throttle:
......@@ -1130,15 +1137,16 @@ function renderCheck(data) {
}
container.appendChild(rsec);
// Gateways (IPv4 + IPv6)
// Gateways (IPv4 + IPv6) — only when checks data present (domain mode)
if (data.checks && Object.keys(data.checks).length) {
const thr = data.throttle || {};
const spd = data.speed || {};
// Find max speed for SLOW detection
let maxSpd = 0;
for (const v of Object.values(spd)) { if (v.speed > maxSpd) maxSpd = v.speed; }
const gsec = mkDiv('check-section');
gsec.appendChild(mkDiv('check-section-title', '\\u0414\\u043e\\u0441\\u0442\\u0443\\u043f\\u043d\\u043e\\u0441\\u0442\\u044c \\u0447\\u0435\\u0440\\u0435\\u0437 \\u0448\\u043b\\u044e\\u0437\\u044b'));
const gwrap = document.createElement('div');
gwrap.className = 'check-gateways';
const order = ['dgw', 'igw', 'warp', 'gre.hetzner', 'ikev2.hetzner', 'gre.vdska', 'gre.beget.ogw', 'ikev2.beget.ogw'];
for (const name of order) {
const info = data.checks[name];
......@@ -1184,6 +1192,7 @@ function renderCheck(data) {
}
container.appendChild(tsec);
}
}
// Whois
if (data.whois && data.whois.length) {
......@@ -1213,7 +1222,7 @@ async function checkDomain() {
try {
const resp = await fetch('/api/check', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
headers: {'Content-Type': 'application/json', 'Accept': 'text/event-stream'},
body: JSON.stringify({domain: v}),
});
const reader = resp.body.getReader();
......@@ -1221,6 +1230,7 @@ async function checkDomain() {
let buf = '';
let checkData = null;
let speedSection = null;
let speedWrap = null;
let speedResults = {};
while (true) {
const {done, value} = await reader.read();
......@@ -1241,51 +1251,44 @@ async function checkDomain() {
if (evType === 'check') {
checkData = parsed;
renderCheck(parsed);
// Prepare speed section if file_url present
// file_url mode: create gateway section for speed results
if (parsed.file_url) {
speedSection = mkDiv('check-section');
const fname = parsed.file_url.split('/').pop().split('?')[0];
speedSection.appendChild(mkDiv('check-section-title',
'\\u0421\\u043a\\u043e\\u0440\\u043e\\u0441\\u0442\\u044c \\u0441\\u043a\\u0430\\u0447\\u0438\\u0432\\u0430\\u043d\\u0438\\u044f (' + fname + ')'));
'\\u0414\\u043e\\u0441\\u0442\\u0443\\u043f\\u043d\\u043e\\u0441\\u0442\\u044c \\u0447\\u0435\\u0440\\u0435\\u0437 \\u0448\\u043b\\u044e\\u0437\\u044b'));
speedWrap = document.createElement('div');
speedWrap.className = 'check-gateways';
speedSection.appendChild(speedWrap);
$('check-gateways').appendChild(speedSection);
}
} else if (evType === 'speed' && speedSection) {
} else if (evType === 'speed' && speedWrap) {
const gw = parsed.gateway;
const info = parsed.result;
speedResults[gw] = info;
const el = mkDiv('check-route', '');
const el = document.createElement('span');
el.id = 'speed-' + gw;
if (info.error) {
el.className = 'check-gw proxy';
el.textContent = gw + ': ' + info.error;
el.style.color = '#999';
} else {
el.textContent = gw + ': ' + info.speed_str + ' (' + info.size + ' \\u0431\\u0430\\u0439\\u0442 \\u0437\\u0430 ' + info.time + '\\u0441)';
el.className = 'check-gw other';
el.textContent = gw + ': ' + info.speed_str;
}
el.id = 'speed-' + gw;
speedSection.appendChild(el);
speedWrap.appendChild(el);
} else if (evType === 'done') {
// Recolor speed rows based on max
// Recolor speed badges based on max speed
let maxSpd = 0;
for (const r of Object.values(speedResults)) { if (r.speed > maxSpd) maxSpd = r.speed; }
for (const [gw, r] of Object.entries(speedResults)) {
const el = document.getElementById('speed-' + gw);
if (!el || r.error) continue;
if (r.speed < maxSpd / 10) { el.style.background = '#f8d7da'; el.style.color = '#721c24'; }
else if (r.speed >= maxSpd * 0.5) { el.style.background = '#d4edda'; el.style.color = '#155724'; }
}
// Update gateway statuses with SLOW
if (checkData) {
const container = $('check-gateways');
const gwSpans = container.querySelectorAll('.check-gw');
gwSpans.forEach(span => {
const name = span.textContent.split(':')[0].trim();
const r = speedResults[name];
if (r && !r.error && maxSpd > 0 && r.speed < maxSpd / 10 && span.classList.contains('ok')) {
span.textContent = name + ': SLOW (' + r.speed_str + ')';
span.className = 'check-gw slow';
} else if (r && !r.error && span.classList.contains('ok')) {
span.textContent = name + ': OK (' + r.speed_str + ')';
if (maxSpd > 0 && r.speed < maxSpd / 10) {
el.className = 'check-gw slow';
el.textContent = gw + ': SLOW (' + r.speed_str + ')';
} else {
el.className = 'check-gw ok';
el.textContent = gw + ': OK (' + r.speed_str + ')';
}
});
}
}
}
......@@ -1734,7 +1737,9 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.log_message("CHECK %s%s", domain,
" file=%s" % file_url if file_url else "")
# SSE stream: first send check result, then speed events
accept = self.headers.get("Accept", "")
if "text/event-stream" in accept:
# SSE stream (browser)
self.send_response(200)
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
self.send_header("Cache-Control", "no-cache")
......@@ -1749,6 +1754,16 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
run_speed_test(file_url, on_speed)
self.send_sse("done", {})
else:
# JSON response (curl/scripts)
result = check_site(domain, file_url=file_url)
if file_url:
speed = {}
def on_speed(gw_name, res):
speed[gw_name] = res
run_speed_test(file_url, on_speed)
result["speed"] = speed
self.send_json(result)
finally:
_check_lock.release()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment