Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 31 additions & 60 deletions open-sse/utils/proxyFetch.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,55 @@
import tlsClient from "./tlsClient.js";

const isCloud = typeof caches !== "undefined" && typeof caches === "object";

const originalFetch = globalThis.fetch;
let proxyAgent = null;
let socksAgent = null;

/**
* Get proxy URL from environment
* Check if URL should bypass proxy (NO_PROXY)
*/
function getProxyUrl(targetUrl) {
function shouldBypassProxy(targetUrl) {
const noProxy = process.env.NO_PROXY || process.env.no_proxy;

if (noProxy) {
const hostname = new URL(targetUrl).hostname.toLowerCase();
const patterns = noProxy.split(",").map(p => p.trim().toLowerCase());

const shouldBypass = patterns.some(pattern => {
if (pattern === "*") return true;
if (pattern.startsWith(".")) return hostname.endsWith(pattern) || hostname === pattern.slice(1);
return hostname === pattern || hostname.endsWith(`.${pattern}`);
});

if (shouldBypass) return null;
}
if (!noProxy) return false;

const protocol = new URL(targetUrl).protocol;

if (protocol === "https:") {
return process.env.HTTPS_PROXY || process.env.https_proxy ||
process.env.ALL_PROXY || process.env.all_proxy;
}

return process.env.HTTP_PROXY || process.env.http_proxy ||
process.env.ALL_PROXY || process.env.all_proxy;
}
const hostname = new URL(targetUrl).hostname.toLowerCase();
const patterns = noProxy.split(",").map(p => p.trim().toLowerCase());

/**
* Create proxy agent lazily
*/
async function getAgent(proxyUrl) {
const proxyProtocol = new URL(proxyUrl).protocol;

if (proxyProtocol === "socks:" || proxyProtocol === "socks5:" || proxyProtocol === "socks4:") {
if (!socksAgent) {
const { SocksProxyAgent } = await import("socks-proxy-agent");
socksAgent = new SocksProxyAgent(proxyUrl);
}
return socksAgent;
}

if (!proxyAgent) {
const { HttpsProxyAgent } = await import("https-proxy-agent");
proxyAgent = new HttpsProxyAgent(proxyUrl);
}
return proxyAgent;
return patterns.some(pattern => {
if (pattern === "*") return true;
if (pattern.startsWith(".")) return hostname.endsWith(pattern) || hostname === pattern.slice(1);
return hostname === pattern || hostname.endsWith(`.${pattern}`);
});
}

/**
* Patched fetch with proxy support and fallback to direct connection
* Patched fetch with TLS fingerprint spoofing (Chrome 124 via wreq-js).
*
* wreq-js natively handles both TLS fingerprinting AND proxy (HTTP/HTTPS/SOCKS).
* Proxy is configured at session level via env vars (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY).
*
* If NO_PROXY matches the target URL, bypasses the TLS client and uses native fetch.
* If TLS client fails, falls back to native fetch.
*/
async function patchedFetch(url, options = {}) {
const targetUrl = typeof url === "string" ? url : url.toString();
const proxyUrl = getProxyUrl(targetUrl);

if (proxyUrl) {
try {
const agent = await getAgent(proxyUrl);
return await originalFetch(url, { ...options, dispatcher: agent });
} catch (proxyError) {
// Fallback to direct connection if proxy fails
console.warn(`[ProxyFetch] Proxy failed, falling back to direct: ${proxyError.message}`);
return originalFetch(url, options);
}

// Bypass TLS client for NO_PROXY targets
if (shouldBypassProxy(targetUrl)) {
return originalFetch(url, options);
}

// Use TLS client (handles both TLS fingerprint + proxy)
try {
return await tlsClient.fetch(targetUrl, options);
} catch (tlsError) {
console.warn(`[ProxyFetch] TLS client failed, falling back to native fetch: ${tlsError.message}`);
return originalFetch(url, options);
}

return originalFetch(url, options);
}

if (!isCloud) {
globalThis.fetch = patchedFetch;
}

export default isCloud ? originalFetch : patchedFetch;

79 changes: 79 additions & 0 deletions open-sse/utils/tlsClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { createRequire } from "module";

const require = createRequire(import.meta.url);
const { createSession } = require("wreq-js");

/**
* Get proxy URL from environment variables.
* Priority: HTTPS_PROXY > HTTP_PROXY > ALL_PROXY
*/
function getProxyFromEnv() {
return process.env.HTTPS_PROXY || process.env.https_proxy ||
process.env.HTTP_PROXY || process.env.http_proxy ||
process.env.ALL_PROXY || process.env.all_proxy ||
undefined;
}

/**
* TLS Client — Chrome 124 TLS fingerprint spoofing via wreq-js
* Singleton instance used to disguise Node.js TLS handshake as Chrome browser.
*
* wreq-js natively supports proxy — TLS fingerprinting works through proxy.
* Proxy URL is read from environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY).
*/
class TlsClient {
constructor() {
this.session = null;
}

async getSession() {
if (this.session) return this.session;

const proxy = getProxyFromEnv();
const sessionOpts = {
browser: "chrome_124",
os: "macos",
};
if (proxy) {
sessionOpts.proxy = proxy;
console.log(`[TlsClient] Using proxy: ${proxy}`);
}

this.session = await createSession(sessionOpts);
console.log("[TlsClient] Session created (Chrome 124 TLS fingerprint)");
return this.session;
}

/**
* Fetch with Chrome 124 TLS fingerprint.
* wreq-js Response is already fetch-compatible (headers, text(), json(), clone(), body).
*/
async fetch(url, options = {}) {
const session = await this.getSession();
const method = (options.method || "GET").toUpperCase();

const wreqOptions = {
method,
headers: options.headers,
body: options.body,
redirect: options.redirect === "manual" ? "manual" : "follow",
};

// Pass signal through if available
if (options.signal) {
wreqOptions.signal = options.signal;
}

const response = await session.fetch(url, wreqOptions);
return response;
}

async exit() {
if (this.session) {
await this.session.close();
this.session = null;
}
}
}

export default new TlsClient();
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"socks-proxy-agent": "^8.0.5",
"undici": "^7.19.2",
"uuid": "^13.0.0",
"wreq-js": "^2.0.1",
"zustand": "^5.0.10"
},
"devDependencies": {
Expand Down