Commit c7c5569d authored by Vitaly Lipatov's avatar Vitaly Lipatov

web-api: separate /api/resolve endpoint for right column (IPs/routes/whois)

Browser fires two parallel requests: - POST /api/check (SSE) — streams gateway availability live - POST /api/resolve (JSON) — returns IPs, routes, whois independently No SSE dependency between columns — each loads on its own. Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c5855911
......@@ -1124,6 +1124,40 @@ function renderCheck(data) {
$('check-result').className = 'check-result visible';
}
function renderResolve(data) {
const col = document.querySelector('#check-gateways .check-col:last-child');
if (!col) return;
if (data.ips && data.ips.length) {
const sec = mkDiv('check-section');
sec.appendChild(mkDiv('check-section-title', 'IP-\\u0430\\u0434\\u0440\\u0435\\u0441\\u0430'));
sec.appendChild(mkDiv('check-ips', data.ips.join(', ')));
col.appendChild(sec);
}
const rsec = mkDiv('check-section');
rsec.appendChild(mkDiv('check-section-title', '\\u0412 \\u0441\\u043f\\u0438\\u0441\\u043a\\u0430\\u0445 \\u043c\\u0430\\u0440\\u0448\\u0440\\u0443\\u0442\\u043e\\u0432'));
if (data.routes && data.routes.length) {
const byList = {};
data.routes.forEach(r => {
const key = r.group + '/' + r.list;
if (!byList[key]) byList[key] = [];
byList[key].push(r.ip ? r.ip + ' \\u2208 ' + r.entry : r.entry);
});
for (const [key, items] of Object.entries(byList)) {
const unique = [...new Set(items)];
rsec.appendChild(mkDiv('check-route', key + ': ' + unique.join(', ')));
}
} else {
rsec.appendChild(mkDiv('check-no-route', '\\u041d\\u0435 \\u043d\\u0430\\u0439\\u0434\\u0435\\u043d\\u043e \\u0432 \\u0441\\u043f\\u0438\\u0441\\u043a\\u0430\\u0445 \\u043c\\u0430\\u0440\\u0448\\u0440\\u0443\\u0442\\u043e\\u0432'));
}
col.appendChild(rsec);
if (data.whois && data.whois.length) {
const wsec = mkDiv('check-section');
wsec.appendChild(mkDiv('check-section-title', 'WHOIS'));
wsec.appendChild(mkDiv('check-whois', data.whois.join('\\n')));
col.appendChild(wsec);
}
}
async function checkDomain() {
const inp = $('domain');
const v = inp.value.trim();
......@@ -1131,13 +1165,34 @@ async function checkDomain() {
const btn = $('btn-check');
btn.disabled = true;
btn.textContent = '\\u041f\\u0440\\u043e\\u0432\\u0435\\u0440\\u043a\\u0430\\u2026';
const hdr = $('check-domain');
hdr.textContent = v + ' ';
const sp = document.createElement('span');
sp.className = 'spinner';
hdr.appendChild(sp);
$('check-domain').textContent = v;
$('check-gateways').textContent = '';
$('check-result').className = 'check-result visible';
// Two-column layout
const container = $('check-gateways');
const cols = mkDiv('check-columns');
const colGw = mkDiv('check-col');
colGw.id = 'check-col-gw';
const colInfo = mkDiv('check-col');
cols.appendChild(colGw);
cols.appendChild(colInfo);
container.appendChild(cols);
// Gateway section in left column
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 gwWrap = document.createElement('div');
gwWrap.className = 'check-gateways';
gsec.appendChild(gwWrap);
colGw.appendChild(gsec);
// Two parallel requests: SSE for gateways, JSON for resolve
const resolveP = api('resolve', {domain: v}).then(renderResolve).catch(() => {});
const gwResults = {};
const speedResults = {};
try {
const resp = await fetch('/api/check', {
method: 'POST',
......@@ -1147,9 +1202,6 @@ async function checkDomain() {
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = '';
let gwWrap = null; // gateway badge container (left column)
let gwResults = {}; // {name: {status, status_v6, http_code, ...}}
let speedResults = {}; // file_url mode speed results
while (true) {
const {done, value} = await reader.read();
if (done) break;
......@@ -1166,17 +1218,9 @@ async function checkDomain() {
if (!evData) continue;
const parsed = JSON.parse(evData);
if (evType === 'check') {
renderCheck(parsed);
// Create gateway section in left column
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'));
gwWrap = document.createElement('div');
gwWrap.className = 'check-gateways';
gsec.appendChild(gwWrap);
$('check-col-gw').appendChild(gsec);
} else if (evType === 'gateway' && gwWrap) {
// Domain mode: individual gateway result
// file_url mode: update header
if (parsed.file_url) $('check-domain').textContent = parsed.file_url;
} else if (evType === 'gateway') {
const n = parsed.name;
if (!gwResults[n]) gwResults[n] = {};
if (parsed.ipver === '-4') {
......@@ -1186,7 +1230,6 @@ async function checkDomain() {
gwResults[n].status_v6 = parsed.status;
gwResults[n].http_code_v6 = parsed.http_code;
}
// Create or update badge
let el = document.getElementById('gw-' + n);
if (!el) {
el = document.createElement('span');
......@@ -1195,69 +1238,24 @@ async function checkDomain() {
}
const info = gwResults[n];
const st = info.status || '\\u2026';
const cls = st === 'OK' ? 'ok' : st === 'BLOCK' ? 'block' : st === 'PROXY?' ? 'proxy' : st === '\\u2026' ? 'other' : 'other';
const cls = st === 'OK' ? 'ok' : st === 'BLOCK' ? 'block' : st === 'PROXY?' ? 'proxy' : 'other';
el.className = 'check-gw ' + cls;
let txt = n + ': ' + st;
if (info.status_v6 && info.status_v6 !== 'PROXY?' && info.status_v6 !== 'NOIP') {
txt += ' v6: ' + info.status_v6;
}
el.textContent = txt;
} else if (evType === 'speed' && gwWrap) {
// File URL mode: speed test result per gateway
} else if (evType === 'speed') {
const gw = parsed.gateway;
const info = parsed.result;
speedResults[gw] = info;
const el = document.createElement('span');
el.id = 'gw-' + gw;
if (info.error) {
el.className = 'check-gw proxy';
el.textContent = gw + ': ' + info.error;
} else {
el.className = 'check-gw other';
el.textContent = gw + ': ' + info.speed_str;
}
el.className = info.error ? 'check-gw proxy' : 'check-gw other';
el.textContent = gw + ': ' + (info.error || info.speed_str);
gwWrap.appendChild(el);
} else if (evType === 'resolve') {
// Add IPs + routes to right column
const colInfo1 = document.querySelector('#check-gateways .check-col:last-child');
if (colInfo1) {
if (parsed.ips && parsed.ips.length) {
const sec = mkDiv('check-section');
sec.appendChild(mkDiv('check-section-title', 'IP-\\u0430\\u0434\\u0440\\u0435\\u0441\\u0430'));
sec.appendChild(mkDiv('check-ips', parsed.ips.join(', ')));
colInfo1.appendChild(sec);
}
const rsec = mkDiv('check-section');
rsec.appendChild(mkDiv('check-section-title', '\\u0412 \\u0441\\u043f\\u0438\\u0441\\u043a\\u0430\\u0445 \\u043c\\u0430\\u0440\\u0448\\u0440\\u0443\\u0442\\u043e\\u0432'));
if (parsed.routes && parsed.routes.length) {
const byList = {};
parsed.routes.forEach(r => {
const key = r.group + '/' + r.list;
if (!byList[key]) byList[key] = [];
byList[key].push(r.ip ? r.ip + ' \\u2208 ' + r.entry : r.entry);
});
for (const [key, items] of Object.entries(byList)) {
const unique = [...new Set(items)];
rsec.appendChild(mkDiv('check-route', key + ': ' + unique.join(', ')));
}
} else {
rsec.appendChild(mkDiv('check-no-route', '\\u041d\\u0435 \\u043d\\u0430\\u0439\\u0434\\u0435\\u043d\\u043e \\u0432 \\u0441\\u043f\\u0438\\u0441\\u043a\\u0430\\u0445 \\u043c\\u0430\\u0440\\u0448\\u0440\\u0443\\u0442\\u043e\\u0432'));
}
colInfo1.appendChild(rsec);
}
} else if (evType === 'whois') {
// Add whois to right column
const colInfo = document.querySelector('#check-gateways .check-col:last-child');
if (colInfo && parsed.whois && parsed.whois.length) {
const wsec = mkDiv('check-section');
wsec.appendChild(mkDiv('check-section-title', 'WHOIS'));
wsec.appendChild(mkDiv('check-whois', parsed.whois.join('\\n')));
colInfo.appendChild(wsec);
}
} else if (evType === 'throttle' && gwWrap) {
// Update gateway badge with SLOW
const gw = parsed.gateway;
const el = document.getElementById('gw-' + gw);
} else if (evType === 'throttle') {
const el = document.getElementById('gw-' + parsed.gateway);
if (el && el.classList.contains('ok')) {
const r = parsed.result;
el.className = 'check-gw slow';
......@@ -1265,7 +1263,6 @@ async function checkDomain() {
el.title = r.asset + ': ' + r.size + ' \\u0431\\u0430\\u0439\\u0442 \\u0437\\u0430 ' + r.time + '\\u0441';
}
} else if (evType === 'done') {
// File URL mode: recolor speed badges
if (Object.keys(speedResults).length) {
let maxSpd = 0;
for (const r of Object.values(speedResults)) { if (r.speed > maxSpd) maxSpd = r.speed; }
......@@ -1285,6 +1282,7 @@ async function checkDomain() {
}
}
} catch(e) { console.error(e); }
await resolveP;
btn.disabled = false;
btn.textContent = '\\u041f\\u0440\\u043e\\u0432\\u0435\\u0440\\u0438\\u0442\\u044c';
}
......@@ -1579,6 +1577,8 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.handle_move(data)
elif path == "/api/check":
self.handle_check(data)
elif path == "/api/resolve":
self.handle_resolve(data)
elif path == "/api/googlevideo":
self.handle_add_googlevideo(data)
else:
......@@ -1705,6 +1705,24 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.wfile.write(line.encode("utf-8"))
self.wfile.flush()
def handle_resolve(self, data):
"""Resolve domain: IPs, routes, whois."""
domain = extract_domain(data.get("domain", ""))
if not domain:
self.send_error_json(400, "Invalid domain or IP")
return
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
whois_future = pool.submit(get_whois, domain)
ips = resolve_domain(domain)
routes = find_in_routes(domain, ips)
whois_info = whois_future.result()
self.send_json({
"domain": domain, "ips": ips,
"routes": routes, "whois": whois_info,
})
def handle_check(self, data):
raw = data.get("domain", "").strip()
# Detect file URL: has path with extension (not just domain)
......@@ -1743,27 +1761,12 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
self.send_sse("check", base)
if file_url:
# File URL mode: resolve + whois + speed test
def _resolve_and_routes():
ips = resolve_domain(domain)
routes = find_in_routes(domain, ips)
return ips, routes
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
resolve_future = pool.submit(_resolve_and_routes)
whois_future = pool.submit(get_whois, domain)
ips, routes = resolve_future.result()
self.send_sse("resolve", {"ips": ips, "routes": routes})
whois_info = whois_future.result()
if whois_info:
self.send_sse("whois", {"whois": whois_info})
# File URL mode: speed test only
def on_speed(gw_name, res):
self.send_sse("speed", {"gateway": gw_name, "result": res})
run_speed_test(file_url, on_speed)
else:
# Domain mode: resolve + gateways + whois all in parallel
# Domain mode: gateways + throttle
url = "https://%s/" % domain
has_v6 = False
try:
......@@ -1772,14 +1775,7 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
except socket.gaierror:
pass
def _resolve_and_routes():
ips = resolve_domain(domain)
routes = find_in_routes(domain, ips)
return ips, routes
workers = len(CHECK_GATEWAYS) * 2 + 4
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
resolve_future = pool.submit(_resolve_and_routes)
with concurrent.futures.ThreadPoolExecutor(max_workers=len(CHECK_GATEWAYS) * 2 + 2) as pool:
gw_futures = {}
for name, proxy_v4, proxy_v6 in CHECK_GATEWAYS:
f4 = pool.submit(_check_one, name, proxy_v4, url, "-4")
......@@ -1787,13 +1783,8 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
if has_v6 and proxy_v6:
f6 = pool.submit(_check_one, name, proxy_v6, url, "-6")
gw_futures[f6] = True
whois_future = pool.submit(get_whois, domain)
assets_future = pool.submit(_find_assets, CHECK_GATEWAYS[0][1], url)
# Send resolve as soon as ready
ips, routes = resolve_future.result()
self.send_sse("resolve", {"ips": ips, "routes": routes})
for future in concurrent.futures.as_completed(gw_futures):
name, ipver, status, code = future.result()
self.send_sse("gateway", {
......@@ -1801,9 +1792,6 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
"status": status, "http_code": code,
})
whois_info = whois_future.result()
if whois_info:
self.send_sse("whois", {"whois": whois_info})
assets = assets_future.result()
# Phase 2: throttle detection
......
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