diff --git a/mod.ts b/mod.ts index 5c688f7..a6e7864 100644 --- a/mod.ts +++ b/mod.ts @@ -98,6 +98,70 @@ async function handler(req: Request): Promise { if (pathname === "/favicon.ico") return new Response(null, { status: 204 }); if (pathname === "/health") return json({ status: "ok" }); + // ============ PROXY STATUS ============ + + if (pathname === "/api/proxy/status") { + const httpsProxy = Deno.env.get("HTTPS_PROXY") || Deno.env.get("https_proxy"); + const httpProxy = Deno.env.get("HTTP_PROXY") || Deno.env.get("http_proxy"); + const proxyUrl = Deno.env.get("PROXY_URL"); + const proxyActive = !!(httpsProxy || httpProxy || proxyUrl); + + const mask = (u: string | undefined) => + u ? u.replace(/:\/\/[^@]*@/, "://@") : null; + + // 1. Определяем текущий внешний IP (через прокси, если настроен) + let ip = "unknown"; + let ipInfo: Record = {}; + let latencyMs = -1; + + try { + const t0 = Date.now(); + const ipRes = await fetch("https://ipinfo.io/json", { + headers: { "User-Agent": "curl/7.88.0", "Accept": "application/json" }, + signal: AbortSignal.timeout(8000), + }); + latencyMs = Date.now() - t0; + if (ipRes.ok) { + ipInfo = await ipRes.json(); + ip = ipInfo.ip || "unknown"; + } + } catch { /* timeout or connection error */ } + + // 2. Определяем IP без прокси (прямое соединение), чтобы сравнить + let directIp = "unknown"; + try { + // Временно вызываем нативный fetch напрямую, минуя патч + const nativeFetch: typeof fetch = (globalThis as any).__nativeFetch || fetch; + const res = await nativeFetch("https://api.ipify.org?format=json", { + signal: AbortSignal.timeout(5000), + }); + if (res.ok) { + const data = await res.json(); + directIp = data.ip || "unknown"; + } + } catch { /* ignore */ } + + const proxyWorking = proxyActive && ip !== "unknown" && ip !== directIp; + + return json({ + proxy_enabled: proxyActive, + proxy_working: proxyWorking, + proxy_url: mask((httpsProxy || httpProxy || proxyUrl) || undefined), + current_ip: ip, + direct_ip: directIp, + ip_masked: directIp !== "unknown" && ip !== directIp, + latency_ms: latencyMs, + location: ipInfo.city + ? `${ipInfo.city}, ${ipInfo.region}, ${ipInfo.country}` + : (ipInfo.country || null), + org: ipInfo.org || null, + timezone: ipInfo.timezone || null, + status: proxyActive + ? (proxyWorking ? "proxy_ok" : (ip === "unknown" ? "proxy_error" : "proxy_transparent")) + : "no_proxy", + }); + } + // ============ PROXY CONFIG ============ if (pathname === "/api/proxy/config") { diff --git a/ui.ts b/ui.ts index 9fd039d..72be0a0 100644 --- a/ui.ts +++ b/ui.ts @@ -77,7 +77,27 @@ export const html = ` .time{font-size:.7rem;color:var(--muted);min-width:40px;font-family:monospace} .bar{flex:1;height:4px;background:var(--surface2);border-radius:2px;cursor:pointer} .fill{height:100%;background:var(--accent);border-radius:2px;width:0%} - @media(max-width:600px){.container{padding:40px 16px 180px}.logo{width:80px;height:80px}.title{font-size:2rem}.desc{display:none}.search-row{flex-direction:column}} + /* ── Proxy Status Widget ─────────────────────────────── */ + .proxy-widget{display:flex;align-items:center;gap:12px;padding:10px 18px;margin:20px auto 0;max-width:520px;background:var(--surface);border:1px solid var(--border);border-radius:12px;font-size:.8rem;transition:border-color .3s} + .proxy-widget.ok{border-color:rgba(16,185,129,.4)} + .proxy-widget.error{border-color:rgba(239,68,68,.4)} + .proxy-widget.loading{border-color:var(--border)} + .proxy-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--dim)} + .proxy-widget.ok .proxy-dot{background:#10b981;box-shadow:0 0 6px rgba(16,185,129,.6)} + .proxy-widget.error .proxy-dot{background:#ef4444;box-shadow:0 0 6px rgba(239,68,68,.6)} + .proxy-widget.loading .proxy-dot{background:var(--dim);animation:blink 1s infinite} + @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}} + .proxy-main{flex:1;min-width:0} + .proxy-label{font-weight:600;color:var(--text)} + .proxy-sub{color:var(--muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .proxy-meta{display:flex;gap:10px;margin-top:4px;flex-wrap:wrap} + .proxy-tag{font-size:.7rem;padding:2px 8px;border-radius:4px;background:var(--surface2);color:var(--dim)} + .proxy-tag.green{color:#10b981;background:rgba(16,185,129,.1)} + .proxy-tag.red{color:#ef4444;background:rgba(239,68,68,.1)} + .proxy-tag.blue{color:#60a5fa;background:rgba(96,165,250,.1)} + .proxy-refresh{background:none;border:none;color:var(--dim);cursor:pointer;padding:4px;font-size:.9rem;transition:color .2s;flex-shrink:0} + .proxy-refresh:hover{color:var(--text)} + @media(max-width:600px){.container{padding:40px 16px 180px}.logo{width:80px;height:80px}.title{font-size:2rem}.desc{display:none}.search-row{flex-direction:column}.proxy-widget{flex-wrap:wrap}} @@ -87,6 +107,17 @@ export const html = `

Virome API

Music API for YouTube Music, Lyrics & Streaming

+ + +
+
+
+
Checking proxy...
+
+
+
+ +