forked from github-mirror/Verome-API
fix
This commit is contained in:
@@ -32,4 +32,7 @@ ENV HTTPS_PROXY=""
|
|||||||
ENV HTTP_PROXY=""
|
ENV HTTP_PROXY=""
|
||||||
ENV NO_PROXY="localhost,127.0.0.1"
|
ENV NO_PROXY="localhost,127.0.0.1"
|
||||||
|
|
||||||
CMD ["run", "--allow-net", "--allow-env", "--allow-read", "mod.ts"]
|
# Volume for persistent proxy config (proxy.config.json)
|
||||||
|
VOLUME ["/app"]
|
||||||
|
|
||||||
|
CMD ["run", "--allow-net", "--allow-env", "--allow-read", "--allow-write", "mod.ts"]
|
||||||
|
|||||||
10
deno.json
10
deno.json
@@ -3,13 +3,13 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"start": "deno run --allow-net --allow-env --allow-read mod.ts",
|
"start": "deno run --allow-net --allow-env --allow-read --allow-write mod.ts",
|
||||||
"dev": "deno run --watch --allow-net --allow-env --allow-read mod.ts",
|
"dev": "deno run --watch --allow-net --allow-env --allow-read --allow-write mod.ts",
|
||||||
|
|
||||||
"start:proxy": "HTTPS_PROXY=http://127.0.0.1:8080 HTTP_PROXY=http://127.0.0.1:8080 deno run --allow-net --allow-env --allow-read mod.ts",
|
"start:proxy": "HTTPS_PROXY=http://127.0.0.1:8080 HTTP_PROXY=http://127.0.0.1:8080 deno run --allow-net --allow-env --allow-read --allow-write mod.ts",
|
||||||
"dev:proxy": "HTTPS_PROXY=http://127.0.0.1:8080 HTTP_PROXY=http://127.0.0.1:8080 deno run --watch --allow-net --allow-env --allow-read mod.ts",
|
"dev:proxy": "HTTPS_PROXY=http://127.0.0.1:8080 HTTP_PROXY=http://127.0.0.1:8080 deno run --watch --allow-net --allow-env --allow-read --allow-write mod.ts",
|
||||||
|
|
||||||
"start:proxy:auth": "HTTPS_PROXY=http://user:password@127.0.0.1:8080 HTTP_PROXY=http://user:password@127.0.0.1:8080 deno run --allow-net --allow-env --allow-read mod.ts"
|
"start:proxy:auth": "HTTPS_PROXY=http://user:password@127.0.0.1:8080 HTTP_PROXY=http://user:password@127.0.0.1:8080 deno run --allow-net --allow-env --allow-read --allow-write mod.ts"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"std/": "https://deno.land/std@0.208.0/"
|
"std/": "https://deno.land/std@0.208.0/"
|
||||||
|
|||||||
110
mod.ts
110
mod.ts
@@ -9,9 +9,10 @@
|
|||||||
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
|
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
|
||||||
import { YTMusic, YouTubeSearch, LastFM, fetchFromPiped, fetchFromInvidious, getLyrics, getTrendingMusic, getRadio, getTopArtists, getTopTracks, getArtistInfo, getTrackInfo, getSongComplete, getAlbumComplete, getArtistComplete, getFullChain } from "./lib.ts";
|
import { YTMusic, YouTubeSearch, LastFM, fetchFromPiped, fetchFromInvidious, getLyrics, getTrendingMusic, getRadio, getTopArtists, getTopTracks, getArtistInfo, getTrackInfo, getSongComplete, getAlbumComplete, getArtistComplete, getFullChain } from "./lib.ts";
|
||||||
import { html as uiHtml } from "./ui.ts";
|
import { html as uiHtml } from "./ui.ts";
|
||||||
import { installProxyFetch } from "./proxy.ts";
|
import { installProxyFetch, loadProxyConfig, saveProxyConfig, setRuntimeProxy, getActiveProxyUrl } from "./proxy.ts";
|
||||||
|
|
||||||
// ── Proxy must be installed BEFORE any fetch calls ──────────────────────────
|
// ── Proxy: load saved config → install (patch globalThis.fetch) ─────────────
|
||||||
|
await loadProxyConfig();
|
||||||
installProxyFetch();
|
installProxyFetch();
|
||||||
|
|
||||||
const PORT = parseInt(Deno.env.get("PORT") || "8000");
|
const PORT = parseInt(Deno.env.get("PORT") || "8000");
|
||||||
@@ -98,92 +99,83 @@ async function handler(req: Request): Promise<Response> {
|
|||||||
if (pathname === "/favicon.ico") return new Response(null, { status: 204 });
|
if (pathname === "/favicon.ico") return new Response(null, { status: 204 });
|
||||||
if (pathname === "/health") return json({ status: "ok" });
|
if (pathname === "/health") return json({ status: "ok" });
|
||||||
|
|
||||||
|
// ============ PROXY SET / CLEAR ============
|
||||||
|
|
||||||
|
if (pathname === "/api/proxy/set" && req.method === "POST") {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const newUrl: string | null = body.url || null;
|
||||||
|
if (newUrl) {
|
||||||
|
// Validate URL format
|
||||||
|
try { new URL(newUrl); } catch { return error("Invalid proxy URL format"); }
|
||||||
|
}
|
||||||
|
setRuntimeProxy(newUrl);
|
||||||
|
await saveProxyConfig(newUrl);
|
||||||
|
return json({ success: true, proxy_url: newUrl, message: newUrl ? "Proxy set and saved" : "Proxy cleared" });
|
||||||
|
} catch { return error("Invalid JSON body"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === "/api/proxy/clear" && (req.method === "POST" || req.method === "DELETE")) {
|
||||||
|
setRuntimeProxy(null);
|
||||||
|
await saveProxyConfig(null);
|
||||||
|
return json({ success: true, message: "Proxy cleared" });
|
||||||
|
}
|
||||||
|
|
||||||
// ============ PROXY STATUS ============
|
// ============ PROXY STATUS ============
|
||||||
|
|
||||||
if (pathname === "/api/proxy/status") {
|
if (pathname === "/api/proxy/status") {
|
||||||
const httpsProxy = Deno.env.get("HTTPS_PROXY") || Deno.env.get("https_proxy");
|
const mask = (u: string | null | undefined) =>
|
||||||
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(/:\/\/[^@]*@/, "://<hidden>@") : null;
|
u ? u.replace(/:\/\/[^@]*@/, "://<hidden>@") : null;
|
||||||
|
|
||||||
// 1. Определяем текущий внешний IP (через прокси, если настроен)
|
const activeProxy = getActiveProxyUrl();
|
||||||
|
const proxyActive = !!activeProxy;
|
||||||
|
|
||||||
|
// 1. IP через прокси (или прямой, если прокси не настроен)
|
||||||
let ip = "unknown";
|
let ip = "unknown";
|
||||||
let ipInfo: Record<string, string> = {};
|
let ipInfo: Record<string, string> = {};
|
||||||
let latencyMs = -1;
|
let latencyMs = -1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
const ipRes = await fetch("https://ipinfo.io/json", {
|
const ipRes = await fetch("https://ipinfo.io/json", {
|
||||||
headers: { "User-Agent": "curl/7.88.0", "Accept": "application/json" },
|
headers: { "User-Agent": "curl/7.88.0", "Accept": "application/json" },
|
||||||
signal: AbortSignal.timeout(8000),
|
|
||||||
});
|
});
|
||||||
latencyMs = Date.now() - t0;
|
latencyMs = Date.now() - t0;
|
||||||
if (ipRes.ok) {
|
if (ipRes.ok) { ipInfo = await ipRes.json(); ip = ipInfo.ip || "unknown"; }
|
||||||
ipInfo = await ipRes.json();
|
} catch { /* failed */ }
|
||||||
ip = ipInfo.ip || "unknown";
|
|
||||||
}
|
|
||||||
} catch { /* timeout or connection error */ }
|
|
||||||
|
|
||||||
// 2. Определяем IP без прокси (прямое соединение), чтобы сравнить
|
// 2. Прямой IP (нативный fetch, минуя прокси-патч)
|
||||||
let directIp = "unknown";
|
let directIp = ip; // если прокси не настроен — они совпадут
|
||||||
|
if (proxyActive) {
|
||||||
try {
|
try {
|
||||||
// Временно вызываем нативный fetch напрямую, минуя патч
|
// Импортируем нативный fetch через тот же модуль
|
||||||
const nativeFetch: typeof fetch = (globalThis as any).__nativeFetch || fetch;
|
const { proxyFetch: _pf, ...rest } = await import("./proxy.ts");
|
||||||
const res = await nativeFetch("https://api.ipify.org?format=json", {
|
void rest; void _pf;
|
||||||
signal: AbortSignal.timeout(5000),
|
// Используем глобальный нативный fetch напрямую через eval-трюк
|
||||||
});
|
const nf = (globalThis as any)._nativeFetchRef;
|
||||||
if (res.ok) {
|
if (nf) {
|
||||||
const data = await res.json();
|
const r = await nf("https://api.ipify.org?format=json");
|
||||||
directIp = data.ip || "unknown";
|
if (r.ok) { const d = await r.json(); directIp = d.ip || ip; }
|
||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
const proxyWorking = proxyActive && ip !== "unknown" && ip !== directIp;
|
const proxyWorking = proxyActive && ip !== "unknown" && ip !== directIp;
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
proxy_enabled: proxyActive,
|
proxy_enabled: proxyActive,
|
||||||
proxy_working: proxyWorking,
|
proxy_working: proxyWorking,
|
||||||
proxy_url: mask((httpsProxy || httpProxy || proxyUrl) || undefined),
|
proxy_url: mask(activeProxy),
|
||||||
current_ip: ip,
|
current_ip: ip,
|
||||||
direct_ip: directIp,
|
direct_ip: directIp,
|
||||||
ip_masked: directIp !== "unknown" && ip !== directIp,
|
ip_masked: ip !== directIp,
|
||||||
latency_ms: latencyMs,
|
latency_ms: latencyMs,
|
||||||
location: ipInfo.city
|
location: ipInfo.city ? `${ipInfo.city}, ${ipInfo.region}, ${ipInfo.country}` : (ipInfo.country || null),
|
||||||
? `${ipInfo.city}, ${ipInfo.region}, ${ipInfo.country}`
|
|
||||||
: (ipInfo.country || null),
|
|
||||||
org: ipInfo.org || null,
|
org: ipInfo.org || null,
|
||||||
timezone: ipInfo.timezone || null,
|
timezone: ipInfo.timezone || null,
|
||||||
status: proxyActive
|
status: !proxyActive ? "no_proxy"
|
||||||
? (proxyWorking ? "proxy_ok" : (ip === "unknown" ? "proxy_error" : "proxy_transparent"))
|
: ip === "unknown" ? "proxy_error"
|
||||||
: "no_proxy",
|
: proxyWorking ? "proxy_ok"
|
||||||
});
|
: "proxy_transparent",
|
||||||
}
|
|
||||||
|
|
||||||
// ============ PROXY CONFIG ============
|
|
||||||
|
|
||||||
if (pathname === "/api/proxy/config") {
|
|
||||||
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 noProxy = Deno.env.get("NO_PROXY") || Deno.env.get("no_proxy");
|
|
||||||
const active = !!(httpsProxy || httpProxy || proxyUrl);
|
|
||||||
|
|
||||||
// Mask credentials in proxy URL for display
|
|
||||||
const mask = (u: string | undefined) =>
|
|
||||||
u ? u.replace(/:\/\/[^@]*@/, "://<hidden>@") : null;
|
|
||||||
|
|
||||||
return json({
|
|
||||||
proxy_enabled: active,
|
|
||||||
https_proxy: mask(httpsProxy || undefined),
|
|
||||||
http_proxy: mask(httpProxy || undefined),
|
|
||||||
proxy_url: mask(proxyUrl || undefined),
|
|
||||||
no_proxy: noProxy || null,
|
|
||||||
note: active
|
|
||||||
? "All outbound requests are routed through the proxy"
|
|
||||||
: "No proxy configured — direct connections are used",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
128
proxy.ts
128
proxy.ts
@@ -3,37 +3,47 @@
|
|||||||
/**
|
/**
|
||||||
* HTTP/HTTPS Proxy support for Deno fetch
|
* HTTP/HTTPS Proxy support for Deno fetch
|
||||||
*
|
*
|
||||||
* Reads proxy settings from environment variables:
|
* Reads proxy settings from:
|
||||||
* HTTPS_PROXY — proxy for HTTPS requests (e.g. http://127.0.0.1:8080)
|
* 1. Runtime config set via setRuntimeProxy() / /api/proxy/set
|
||||||
* HTTP_PROXY — proxy for HTTP requests (e.g. http://127.0.0.1:8080)
|
* 2. Environment variables: HTTPS_PROXY, HTTP_PROXY, PROXY_URL
|
||||||
* PROXY_URL — fallback universal proxy (e.g. http://user:pass@host:port)
|
|
||||||
* NO_PROXY — comma-separated hosts to skip (e.g. localhost,127.0.0.1)
|
|
||||||
*
|
*
|
||||||
* Usage:
|
* NO_PROXY — comma-separated hosts to bypass (e.g. localhost,127.0.0.1)
|
||||||
* import { proxyFetch } from "./proxy.ts";
|
|
||||||
* const res = await proxyFetch("https://music.youtube.com/...", { ... });
|
|
||||||
*
|
*
|
||||||
* Run with:
|
* Run with:
|
||||||
* HTTPS_PROXY=http://127.0.0.1:8080 deno run --allow-net --allow-env --allow-read mod.ts
|
* HTTPS_PROXY=http://user:pass@host:port deno run --allow-net --allow-env --allow-read mod.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ─── Parse proxy URL ──────────────────────────────────────────────────────────
|
// ─── Native fetch reference (captured BEFORE any patching) ───────────────────
|
||||||
|
// Must be module-level so it's captured at import time.
|
||||||
|
const _nativeFetch: typeof fetch = globalThis.fetch.bind(globalThis);
|
||||||
|
|
||||||
|
// ─── Runtime proxy config (set via API / UI) ─────────────────────────────────
|
||||||
|
let _runtimeProxyUrl: string | null = null;
|
||||||
|
|
||||||
|
/** Set proxy URL at runtime (called from /api/proxy/set). Pass null to clear. */
|
||||||
|
export function setRuntimeProxy(url: string | null): void {
|
||||||
|
_runtimeProxyUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the active proxy URL (runtime config takes priority over env vars). */
|
||||||
|
export function getActiveProxyUrl(): string | null {
|
||||||
|
if (_runtimeProxyUrl) return _runtimeProxyUrl;
|
||||||
|
return (
|
||||||
|
Deno.env.get("HTTPS_PROXY") || Deno.env.get("https_proxy") ||
|
||||||
|
Deno.env.get("HTTP_PROXY") || Deno.env.get("http_proxy") ||
|
||||||
|
Deno.env.get("PROXY_URL") || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Parse proxy URL for a given target ──────────────────────────────────────
|
||||||
|
|
||||||
function getProxyUrl(targetUrl: string): string | null {
|
function getProxyUrl(targetUrl: string): string | null {
|
||||||
const isHttps = targetUrl.startsWith("https://");
|
const proxyUrl = getActiveProxyUrl();
|
||||||
|
|
||||||
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 universalProxy = Deno.env.get("PROXY_URL");
|
|
||||||
|
|
||||||
const proxyUrl = isHttps
|
|
||||||
? (httpsProxy || universalProxy || httpProxy || null)
|
|
||||||
: (httpProxy || universalProxy || null);
|
|
||||||
|
|
||||||
if (!proxyUrl) return null;
|
if (!proxyUrl) return null;
|
||||||
|
|
||||||
// Check NO_PROXY
|
// Check NO_PROXY
|
||||||
const noProxy = (Deno.env.get("NO_PROXY") || Deno.env.get("no_proxy") || "").split(",").map(s => s.trim()).filter(Boolean);
|
const noProxy = (Deno.env.get("NO_PROXY") || Deno.env.get("no_proxy") || "")
|
||||||
|
.split(",").map((s: string) => s.trim()).filter(Boolean);
|
||||||
if (noProxy.length) {
|
if (noProxy.length) {
|
||||||
try {
|
try {
|
||||||
const { hostname } = new URL(targetUrl);
|
const { hostname } = new URL(targetUrl);
|
||||||
@@ -59,19 +69,16 @@ async function connectViaProxy(
|
|||||||
|
|
||||||
const conn = await Deno.connect({ hostname: proxyHost, port: proxyPort });
|
const conn = await Deno.connect({ hostname: proxyHost, port: proxyPort });
|
||||||
|
|
||||||
// Basic auth header (if credentials present in proxy URL)
|
|
||||||
let authHeader = "";
|
let authHeader = "";
|
||||||
if (proxy.username) {
|
if (proxy.username) {
|
||||||
const creds = btoa(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`);
|
const creds = btoa(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`);
|
||||||
authHeader = `Proxy-Authorization: Basic ${creds}\r\n`;
|
authHeader = `Proxy-Authorization: Basic ${creds}\r\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send CONNECT request
|
|
||||||
const connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${authHeader}\r\n`;
|
const connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${authHeader}\r\n`;
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
await conn.write(encoder.encode(connectReq));
|
await conn.write(encoder.encode(connectReq));
|
||||||
|
|
||||||
// Read response
|
|
||||||
const buf = new Uint8Array(4096);
|
const buf = new Uint8Array(4096);
|
||||||
const n = await conn.read(buf);
|
const n = await conn.read(buf);
|
||||||
if (n === null) throw new Error("Proxy closed connection unexpectedly");
|
if (n === null) throw new Error("Proxy closed connection unexpectedly");
|
||||||
@@ -91,8 +98,8 @@ async function connectViaProxy(
|
|||||||
// ─── Proxy-aware fetch ────────────────────────────────────────────────────────
|
// ─── Proxy-aware fetch ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop-in replacement for global `fetch` that respects HTTP/HTTPS proxy
|
* Drop-in replacement for global `fetch` that routes through proxy when configured.
|
||||||
* environment variables. Falls back to native fetch when no proxy is configured.
|
* Falls back to native fetch when no proxy is configured.
|
||||||
*/
|
*/
|
||||||
export async function proxyFetch(
|
export async function proxyFetch(
|
||||||
input: string | URL | Request,
|
input: string | URL | Request,
|
||||||
@@ -101,16 +108,11 @@ export async function proxyFetch(
|
|||||||
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||||
const proxyUrl = getProxyUrl(url);
|
const proxyUrl = getProxyUrl(url);
|
||||||
|
|
||||||
// No proxy configured — use native fetch directly
|
// No proxy — use the NATIVE fetch (not globalThis.fetch to avoid recursion!)
|
||||||
if (!proxyUrl) {
|
if (!proxyUrl) {
|
||||||
return fetch(input, init);
|
return _nativeFetch(input, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For HTTPS targets we build a tunnel with HTTP CONNECT, then do the request
|
|
||||||
// over the tunnelled raw TCP connection using the Fetch API with the tunnel.
|
|
||||||
// Deno's built-in fetch also supports HTTPS_PROXY natively — but only when
|
|
||||||
// the flag --allow-env is present AND Deno is compiled with that support.
|
|
||||||
// To be safe, we also forward the env vars so Deno's own fetch can pick them up.
|
|
||||||
try {
|
try {
|
||||||
const targetUrl = new URL(url);
|
const targetUrl = new URL(url);
|
||||||
const isHttps = targetUrl.protocol === "https:";
|
const isHttps = targetUrl.protocol === "https:";
|
||||||
@@ -118,18 +120,13 @@ export async function proxyFetch(
|
|||||||
const targetPort = parseInt(targetUrl.port || (isHttps ? "443" : "80"));
|
const targetPort = parseInt(targetUrl.port || (isHttps ? "443" : "80"));
|
||||||
|
|
||||||
if (isHttps) {
|
if (isHttps) {
|
||||||
// Open a TCP tunnel through the proxy
|
|
||||||
const tunnel = await connectViaProxy(proxyUrl, targetHost, targetPort);
|
const tunnel = await connectViaProxy(proxyUrl, targetHost, targetPort);
|
||||||
|
|
||||||
// Upgrade to TLS over the tunnel
|
|
||||||
const tlsConn = await Deno.startTls(tunnel, { hostname: targetHost });
|
const tlsConn = await Deno.startTls(tunnel, { hostname: targetHost });
|
||||||
|
|
||||||
// Build raw HTTP/1.1 request
|
|
||||||
const method = (init?.method || "GET").toUpperCase();
|
const method = (init?.method || "GET").toUpperCase();
|
||||||
const path = targetUrl.pathname + targetUrl.search;
|
const path = targetUrl.pathname + targetUrl.search;
|
||||||
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
||||||
|
|
||||||
// Ensure required headers
|
|
||||||
if (!headers.has("Host")) headers.set("Host", targetHost);
|
if (!headers.has("Host")) headers.set("Host", targetHost);
|
||||||
if (!headers.has("User-Agent")) headers.set("User-Agent", "Mozilla/5.0");
|
if (!headers.has("User-Agent")) headers.set("User-Agent", "Mozilla/5.0");
|
||||||
if (!headers.has("Connection")) headers.set("Connection", "close");
|
if (!headers.has("Connection")) headers.set("Connection", "close");
|
||||||
@@ -152,7 +149,6 @@ export async function proxyFetch(
|
|||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
await tlsConn.write(encoder.encode(rawRequest));
|
await tlsConn.write(encoder.encode(rawRequest));
|
||||||
|
|
||||||
// Read response
|
|
||||||
const chunks: Uint8Array[] = [];
|
const chunks: Uint8Array[] = [];
|
||||||
const tmpBuf = new Uint8Array(16384);
|
const tmpBuf = new Uint8Array(16384);
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -187,7 +183,7 @@ export async function proxyFetch(
|
|||||||
return new Response(bodyBytes, { status: statusCode, statusText, headers: responseHeaders });
|
return new Response(bodyBytes, { status: statusCode, statusText, headers: responseHeaders });
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP (plain) — use proxy as a regular forward proxy
|
// HTTP plain — forward proxy
|
||||||
const proxy = new URL(proxyUrl);
|
const proxy = new URL(proxyUrl);
|
||||||
const conn = await Deno.connect({ hostname: proxy.hostname, port: parseInt(proxy.port || "8080") });
|
const conn = await Deno.connect({ hostname: proxy.hostname, port: parseInt(proxy.port || "8080") });
|
||||||
|
|
||||||
@@ -207,7 +203,6 @@ export async function proxyFetch(
|
|||||||
body = bodyText;
|
body = bodyText;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy-Authorization header for HTTP forward proxy
|
|
||||||
if (proxy.username && !headers.has("Proxy-Authorization")) {
|
if (proxy.username && !headers.has("Proxy-Authorization")) {
|
||||||
const creds = btoa(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`);
|
const creds = btoa(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`);
|
||||||
headers.set("Proxy-Authorization", `Basic ${creds}`);
|
headers.set("Proxy-Authorization", `Basic ${creds}`);
|
||||||
@@ -255,28 +250,55 @@ export async function proxyFetch(
|
|||||||
return new Response(bodyBytes, { status: statusCode, statusText, headers: responseHeaders });
|
return new Response(bodyBytes, { status: statusCode, statusText, headers: responseHeaders });
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// On any proxy error — fall back to direct fetch and log the issue
|
// Proxy error — fall back to native fetch
|
||||||
console.warn(`[proxy] Error using proxy for ${url}: ${err}. Falling back to direct fetch.`);
|
console.warn(`[proxy] Error via proxy for ${url}: ${err}. Falling back to direct.`);
|
||||||
return fetch(input, init);
|
return _nativeFetch(input, init);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Patch global fetch ───────────────────────────────────────────────────────
|
// ─── Install ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call once at startup to replace globalThis.fetch with proxyFetch.
|
* Call once at startup to replace globalThis.fetch with proxyFetch.
|
||||||
* After this, ALL fetch() calls in the process go through the proxy.
|
* The native fetch is already captured at module level, so no recursion issues.
|
||||||
*/
|
*/
|
||||||
export function installProxyFetch(): void {
|
export function installProxyFetch(): void {
|
||||||
const proxyUrl = Deno.env.get("HTTPS_PROXY") || Deno.env.get("https_proxy") ||
|
// @ts-ignore — patching global fetch
|
||||||
Deno.env.get("HTTP_PROXY") || Deno.env.get("http_proxy") ||
|
|
||||||
Deno.env.get("PROXY_URL");
|
|
||||||
|
|
||||||
if (proxyUrl) {
|
|
||||||
console.log(`[proxy] HTTP proxy enabled: ${proxyUrl.replace(/:\/\/[^@]*@/, "://<credentials>@")}`);
|
|
||||||
// @ts-ignore — replacing global fetch
|
|
||||||
globalThis.fetch = proxyFetch;
|
globalThis.fetch = proxyFetch;
|
||||||
|
|
||||||
|
const active = getActiveProxyUrl();
|
||||||
|
if (active) {
|
||||||
|
const masked = active.replace(/:\/\/[^@]*@/, "://<credentials>@");
|
||||||
|
console.log(`[proxy] HTTP proxy enabled: ${masked}`);
|
||||||
} else {
|
} else {
|
||||||
console.log("[proxy] No proxy configured — using direct connections");
|
console.log("[proxy] No proxy configured — using direct connections");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Config file persistence ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const CONFIG_FILE = "./proxy.config.json";
|
||||||
|
|
||||||
|
/** Load saved proxy URL from disk (called at startup). */
|
||||||
|
export async function loadProxyConfig(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const raw = await Deno.readTextFile(CONFIG_FILE);
|
||||||
|
const cfg = JSON.parse(raw);
|
||||||
|
if (cfg.url) {
|
||||||
|
_runtimeProxyUrl = cfg.url;
|
||||||
|
const masked = cfg.url.replace(/:\/\/[^@]*@/, "://<credentials>@");
|
||||||
|
console.log(`[proxy] Loaded saved proxy config: ${masked}`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist or invalid — ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save current runtime proxy URL to disk. */
|
||||||
|
export async function saveProxyConfig(url: string | null): Promise<void> {
|
||||||
|
try {
|
||||||
|
await Deno.writeTextFile(CONFIG_FILE, JSON.stringify({ url }, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[proxy] Failed to save proxy config:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
88
ui.ts
88
ui.ts
@@ -97,6 +97,20 @@ export const html = `<!DOCTYPE html>
|
|||||||
.proxy-tag.blue{color:#60a5fa;background:rgba(96,165,250,.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{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)}
|
.proxy-refresh:hover{color:var(--text)}
|
||||||
|
.proxy-settings{display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--border);width:100%}
|
||||||
|
.proxy-settings.open{display:block}
|
||||||
|
.proxy-form{display:flex;gap:8px;flex-wrap:wrap}
|
||||||
|
.proxy-input{flex:1;min-width:200px;background:var(--surface2);border:1px solid var(--border);padding:8px 12px;border-radius:8px;color:var(--text);font-size:.8rem;font-family:monospace}
|
||||||
|
.proxy-input:focus{outline:none;border-color:var(--accent)}
|
||||||
|
.proxy-input::placeholder{color:var(--dim)}
|
||||||
|
.proxy-save{background:var(--accent);color:#000;border:none;padding:8px 16px;border-radius:8px;font-size:.8rem;font-weight:600;cursor:pointer;font-family:inherit;transition:opacity .15s}
|
||||||
|
.proxy-save:hover{opacity:.85}
|
||||||
|
.proxy-save:disabled{opacity:.4}
|
||||||
|
.proxy-clear-btn{background:rgba(239,68,68,.15);color:#ef4444;border:1px solid rgba(239,68,68,.3);padding:8px 14px;border-radius:8px;font-size:.8rem;cursor:pointer;font-family:inherit;transition:all .15s}
|
||||||
|
.proxy-clear-btn:hover{background:rgba(239,68,68,.25)}
|
||||||
|
.proxy-msg{font-size:.75rem;margin-top:6px;padding:4px 8px;border-radius:4px;display:none}
|
||||||
|
.proxy-msg.ok{display:block;color:#10b981;background:rgba(16,185,129,.1)}
|
||||||
|
.proxy-msg.err{display:block;color:#ef4444;background:rgba(239,68,68,.1)}
|
||||||
@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}}
|
@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}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -109,14 +123,26 @@ export const html = `<!DOCTYPE html>
|
|||||||
<p class="subtitle">Music API for YouTube Music, Lyrics & Streaming</p>
|
<p class="subtitle">Music API for YouTube Music, Lyrics & Streaming</p>
|
||||||
|
|
||||||
<!-- Proxy Status Widget -->
|
<!-- Proxy Status Widget -->
|
||||||
<div class="proxy-widget loading" id="proxyWidget">
|
<div class="proxy-widget loading" id="proxyWidget" style="flex-wrap:wrap">
|
||||||
<div class="proxy-dot"></div>
|
<div class="proxy-dot"></div>
|
||||||
<div class="proxy-main">
|
<div class="proxy-main">
|
||||||
<div class="proxy-label" id="proxyLabel">Checking proxy...</div>
|
<div class="proxy-label" id="proxyLabel">Checking proxy...</div>
|
||||||
<div class="proxy-sub" id="proxySub"></div>
|
<div class="proxy-sub" id="proxySub"></div>
|
||||||
<div class="proxy-meta" id="proxyMeta"></div>
|
<div class="proxy-meta" id="proxyMeta"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="proxy-refresh" onclick="toggleProxySettings()" title="Settings" style="font-size:1rem">⚙</button>
|
||||||
<button class="proxy-refresh" id="proxyRefreshBtn" onclick="loadProxyStatus()" title="Refresh">↻</button>
|
<button class="proxy-refresh" id="proxyRefreshBtn" onclick="loadProxyStatus()" title="Refresh">↻</button>
|
||||||
|
|
||||||
|
<!-- Settings form (hidden by default) -->
|
||||||
|
<div class="proxy-settings" id="proxySettings">
|
||||||
|
<div style="font-size:.7rem;color:var(--muted);margin-bottom:6px">Proxy URL <span style="color:var(--dim)">(http://user:pass@host:port)</span></div>
|
||||||
|
<div class="proxy-form">
|
||||||
|
<input class="proxy-input" id="proxyUrlInput" type="text" placeholder="http://music:music@193.124.94.179:35467" spellcheck="false" autocomplete="off">
|
||||||
|
<button class="proxy-save" id="proxySaveBtn" onclick="saveProxy()">Test & Save</button>
|
||||||
|
<button class="proxy-clear-btn" onclick="clearProxy()">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div class="proxy-msg" id="proxyMsg"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -494,6 +520,66 @@ async function testApi(){
|
|||||||
}
|
}
|
||||||
updateInputs();
|
updateInputs();
|
||||||
|
|
||||||
|
// ── Proxy Settings Form ────────────────────────────────────────────────────
|
||||||
|
function toggleProxySettings(){
|
||||||
|
var s=document.getElementById('proxySettings');
|
||||||
|
s.classList.toggle('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showProxyMsg(text,isOk){
|
||||||
|
var m=document.getElementById('proxyMsg');
|
||||||
|
m.textContent=text;
|
||||||
|
m.className='proxy-msg '+(isOk?'ok':'err');
|
||||||
|
clearTimeout(m._t);
|
||||||
|
m._t=setTimeout(()=>m.className='proxy-msg',4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProxy(){
|
||||||
|
var input=document.getElementById('proxyUrlInput');
|
||||||
|
var btn=document.getElementById('proxySaveBtn');
|
||||||
|
var url=input.value.trim();
|
||||||
|
if(!url){showProxyMsg('Enter proxy URL first','');return;}
|
||||||
|
|
||||||
|
btn.disabled=true;
|
||||||
|
btn.textContent='Testing...';
|
||||||
|
|
||||||
|
try{
|
||||||
|
var res=await fetch('/api/proxy/set',{
|
||||||
|
method:'POST',
|
||||||
|
headers:{'Content-Type':'application/json'},
|
||||||
|
body:JSON.stringify({url:url})
|
||||||
|
});
|
||||||
|
var d=await res.json();
|
||||||
|
if(d.success){
|
||||||
|
showProxyMsg('✓ Saved. Checking status...',true);
|
||||||
|
// Collapse form and refresh status
|
||||||
|
setTimeout(()=>{
|
||||||
|
document.getElementById('proxySettings').classList.remove('open');
|
||||||
|
loadProxyStatus();
|
||||||
|
},600);
|
||||||
|
}else{
|
||||||
|
showProxyMsg('Error: '+(d.error||'Unknown'),false);
|
||||||
|
}
|
||||||
|
}catch(e){
|
||||||
|
showProxyMsg('Request failed: '+String(e),false);
|
||||||
|
}finally{
|
||||||
|
btn.disabled=false;
|
||||||
|
btn.textContent='Test & Save';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearProxy(){
|
||||||
|
try{
|
||||||
|
await fetch('/api/proxy/clear',{method:'POST'});
|
||||||
|
document.getElementById('proxyUrlInput').value='';
|
||||||
|
document.getElementById('proxySettings').classList.remove('open');
|
||||||
|
showProxyMsg('Proxy cleared',true);
|
||||||
|
setTimeout(loadProxyStatus,400);
|
||||||
|
}catch(e){
|
||||||
|
showProxyMsg('Failed: '+String(e),false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Proxy Status Widget ────────────────────────────────────────────────────
|
// ── Proxy Status Widget ────────────────────────────────────────────────────
|
||||||
async function loadProxyStatus(){
|
async function loadProxyStatus(){
|
||||||
var w=document.getElementById('proxyWidget');
|
var w=document.getElementById('proxyWidget');
|
||||||
|
|||||||
Reference in New Issue
Block a user