This commit is contained in:
2026-03-06 04:00:48 +05:00
parent 7444e7ddd7
commit ed1f3b3a75
5 changed files with 225 additions and 122 deletions

128
proxy.ts
View File

@@ -3,37 +3,47 @@
/**
* HTTP/HTTPS Proxy support for Deno fetch
*
* Reads proxy settings from environment variables:
* HTTPS_PROXY — proxy for HTTPS requests (e.g. http://127.0.0.1:8080)
* HTTP_PROXY — proxy for HTTP requests (e.g. http://127.0.0.1:8080)
* 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)
* Reads proxy settings from:
* 1. Runtime config set via setRuntimeProxy() / /api/proxy/set
* 2. Environment variables: HTTPS_PROXY, HTTP_PROXY, PROXY_URL
*
* Usage:
* import { proxyFetch } from "./proxy.ts";
* const res = await proxyFetch("https://music.youtube.com/...", { ... });
* NO_PROXY — comma-separated hosts to bypass (e.g. localhost,127.0.0.1)
*
* 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 {
const isHttps = targetUrl.startsWith("https://");
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);
const proxyUrl = getActiveProxyUrl();
if (!proxyUrl) return null;
// 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) {
try {
const { hostname } = new URL(targetUrl);
@@ -59,19 +69,16 @@ async function connectViaProxy(
const conn = await Deno.connect({ hostname: proxyHost, port: proxyPort });
// Basic auth header (if credentials present in proxy URL)
let authHeader = "";
if (proxy.username) {
const creds = btoa(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`);
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 encoder = new TextEncoder();
await conn.write(encoder.encode(connectReq));
// Read response
const buf = new Uint8Array(4096);
const n = await conn.read(buf);
if (n === null) throw new Error("Proxy closed connection unexpectedly");
@@ -91,8 +98,8 @@ async function connectViaProxy(
// ─── Proxy-aware fetch ────────────────────────────────────────────────────────
/**
* Drop-in replacement for global `fetch` that respects HTTP/HTTPS proxy
* environment variables. Falls back to native fetch when no proxy is configured.
* Drop-in replacement for global `fetch` that routes through proxy when configured.
* Falls back to native fetch when no proxy is configured.
*/
export async function proxyFetch(
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 proxyUrl = getProxyUrl(url);
// No proxy configured — use native fetch directly
// No proxy — use the NATIVE fetch (not globalThis.fetch to avoid recursion!)
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 {
const targetUrl = new URL(url);
const isHttps = targetUrl.protocol === "https:";
@@ -118,18 +120,13 @@ export async function proxyFetch(
const targetPort = parseInt(targetUrl.port || (isHttps ? "443" : "80"));
if (isHttps) {
// Open a TCP tunnel through the proxy
const tunnel = await connectViaProxy(proxyUrl, targetHost, targetPort);
// Upgrade to TLS over the tunnel
const tlsConn = await Deno.startTls(tunnel, { hostname: targetHost });
// Build raw HTTP/1.1 request
const method = (init?.method || "GET").toUpperCase();
const path = targetUrl.pathname + targetUrl.search;
const headers = new Headers(init?.headers as HeadersInit | undefined);
// Ensure required headers
if (!headers.has("Host")) headers.set("Host", targetHost);
if (!headers.has("User-Agent")) headers.set("User-Agent", "Mozilla/5.0");
if (!headers.has("Connection")) headers.set("Connection", "close");
@@ -152,7 +149,6 @@ export async function proxyFetch(
const encoder = new TextEncoder();
await tlsConn.write(encoder.encode(rawRequest));
// Read response
const chunks: Uint8Array[] = [];
const tmpBuf = new Uint8Array(16384);
while (true) {
@@ -187,7 +183,7 @@ export async function proxyFetch(
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 conn = await Deno.connect({ hostname: proxy.hostname, port: parseInt(proxy.port || "8080") });
@@ -207,7 +203,6 @@ export async function proxyFetch(
body = bodyText;
}
// Proxy-Authorization header for HTTP forward proxy
if (proxy.username && !headers.has("Proxy-Authorization")) {
const creds = btoa(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`);
headers.set("Proxy-Authorization", `Basic ${creds}`);
@@ -255,28 +250,55 @@ export async function proxyFetch(
return new Response(bodyBytes, { status: statusCode, statusText, headers: responseHeaders });
} catch (err) {
// On any proxy error — fall back to direct fetch and log the issue
console.warn(`[proxy] Error using proxy for ${url}: ${err}. Falling back to direct fetch.`);
return fetch(input, init);
// Proxy error — fall back to native fetch
console.warn(`[proxy] Error via proxy for ${url}: ${err}. Falling back to direct.`);
return _nativeFetch(input, init);
}
}
// ─── Patch global fetch ───────────────────────────────────────────────────────
// ─── Install ──────────────────────────────────────────────────────────────────
/**
* 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 {
const proxyUrl = 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");
// @ts-ignore — patching global fetch
globalThis.fetch = proxyFetch;
if (proxyUrl) {
console.log(`[proxy] HTTP proxy enabled: ${proxyUrl.replace(/:\/\/[^@]*@/, "://<credentials>@")}`);
// @ts-ignore — replacing global fetch
globalThis.fetch = proxyFetch;
const active = getActiveProxyUrl();
if (active) {
const masked = active.replace(/:\/\/[^@]*@/, "://<credentials>@");
console.log(`[proxy] HTTP proxy enabled: ${masked}`);
} else {
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);
}
}