From c7e56ac022a372828a05006851d149b1c063e00d Mon Sep 17 00:00:00 2001 From: zx Date: Mon, 16 Feb 2026 14:28:31 +0800 Subject: [PATCH 1/2] feat: implement TLS fingerprint spoofing via wreq-js for Antigravity proxy * Add `TlsClient` utility to mimic Chrome 124 TLS handshake * Update `patchedFetch` to use TLS client when no proxy is configured * Add fallback mechanism to native fetch on TLS client failure * Add `wreq-js` dependency to package.json --- open-sse/utils/proxyFetch.js | 42 +++++++---- open-sse/utils/tlsClient.js | 139 +++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 open-sse/utils/tlsClient.js diff --git a/open-sse/utils/proxyFetch.js b/open-sse/utils/proxyFetch.js index 0e4acc41..2bca646f 100644 --- a/open-sse/utils/proxyFetch.js +++ b/open-sse/utils/proxyFetch.js @@ -1,3 +1,4 @@ +import tlsClient from "./tlsClient.js"; const isCloud = typeof caches !== "undefined" && typeof caches === "object"; @@ -10,29 +11,29 @@ let socksAgent = null; */ function getProxyUrl(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; } 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.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; + process.env.ALL_PROXY || process.env.all_proxy; } /** @@ -40,7 +41,7 @@ function getProxyUrl(targetUrl) { */ 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"); @@ -48,7 +49,7 @@ async function getAgent(proxyUrl) { } return socksAgent; } - + if (!proxyAgent) { const { HttpsProxyAgent } = await import("https-proxy-agent"); proxyAgent = new HttpsProxyAgent(proxyUrl); @@ -57,12 +58,18 @@ async function getAgent(proxyUrl) { } /** - * Patched fetch with proxy support and fallback to direct connection + * Patched fetch with TLS fingerprint spoofing (Chrome 124 via wreq-js) + * and proxy support with fallback to direct connection. + * + * Priority: + * 1. If proxy configured → use original fetch + proxy agent (wreq-js doesn't support proxy agents) + * 2. No proxy → use TLS client (wreq-js Chrome 124 fingerprint) + * 3. TLS client fails → fallback to original 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); @@ -73,8 +80,15 @@ async function patchedFetch(url, options = {}) { return originalFetch(url, options); } } - - return originalFetch(url, options); + + // No proxy — use TLS client for Chrome 124 fingerprint spoofing + try { + return await tlsClient.fetch(targetUrl, options); + } catch (tlsError) { + // Fallback to original fetch if TLS client fails + console.warn(`[ProxyFetch] TLS client failed, falling back to native fetch: ${tlsError.message}`); + return originalFetch(url, options); + } } if (!isCloud) { diff --git a/open-sse/utils/tlsClient.js b/open-sse/utils/tlsClient.js new file mode 100644 index 00000000..1f2556c8 --- /dev/null +++ b/open-sse/utils/tlsClient.js @@ -0,0 +1,139 @@ +import { createRequire } from "module"; +import { Readable } from "stream"; + +const require = createRequire(import.meta.url); +const { createSession } = require("wreq-js"); + +/** + * TLS Client — Chrome 124 TLS fingerprint spoofing via wreq-js + * Singleton instance used to disguise Node.js TLS handshake as Chrome browser. + */ +class TlsClient { + constructor() { + this.userAgent = "antigravity/1.104.0 darwin/arm64"; + this.session = null; + } + + async getSession() { + if (this.session) return this.session; + this.session = await createSession({ + browser: "chrome_124", + os: "macos", + userAgent: this.userAgent + }); + return this.session; + } + + 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", + }; + + try { + const response = await session.fetch(url, wreqOptions); + return new ResponseWrapper(response); + } catch (error) { + console.error("[TlsClient] wreq-js fetch failed:", error.message); + throw error; + } + } + + async exit() { + if (this.session) { + await this.session.close(); + this.session = null; + } + } +} + +/** + * Wraps wreq-js response to match standard fetch Response interface + */ +class ResponseWrapper { + constructor(wreqResponse) { + this.status = wreqResponse.status; + this.statusText = wreqResponse.statusText || (this.status === 200 ? "OK" : `Status ${this.status}`); + this.headers = new HeadersCompat(wreqResponse.headers); + this.url = wreqResponse.url; + this.ok = this.status >= 200 && this.status < 300; + + if (wreqResponse.body) { + if (typeof wreqResponse.body.getReader === "function") { + this.body = wreqResponse.body; + } else { + this.body = Readable.toWeb(wreqResponse.body); + } + } else { + this.body = null; + } + } + + async text() { + if (!this.body) return ""; + const reader = this.body.getReader(); + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(typeof value === "string" ? Buffer.from(value) : value); + } + return Buffer.concat(chunks).toString("utf8"); + } + + async json() { + const text = await this.text(); + return JSON.parse(text); + } + + clone() { + // Minimal clone — creates a new wrapper that shares status/headers + // but allows independent body consumption via stored text + const cloned = Object.create(ResponseWrapper.prototype); + cloned.status = this.status; + cloned.statusText = this.statusText; + cloned.headers = this.headers; + cloned.url = this.url; + cloned.ok = this.ok; + cloned.body = this.body; + + // Store original text() for clone usage + const originalText = this.text.bind(this); + let cachedText = null; + + const getText = async () => { + if (cachedText === null) cachedText = await originalText(); + return cachedText; + }; + + this.text = getText; + cloned.text = getText; + cloned.json = async () => JSON.parse(await getText()); + this.json = async () => JSON.parse(await getText()); + + return cloned; + } +} + +/** + * Minimal Headers compatibility class for wreq-js responses + */ +class HeadersCompat { + constructor(headersObj = {}) { + this.map = new Map(); + for (const [key, value] of Object.entries(headersObj)) { + this.map.set(key.toLowerCase(), Array.isArray(value) ? value.join(", ") : value); + } + } + + get(name) { return this.map.get(name.toLowerCase()) || null; } + has(name) { return this.map.has(name.toLowerCase()); } + forEach(callback) { this.map.forEach(callback); } +} + +export default new TlsClient(); diff --git a/package.json b/package.json index 1d765cb4..5bffa1ee 100644 --- a/package.json +++ b/package.json @@ -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": { From c3de453289deb8c4a86d3c0078929dbda762d3a7 Mon Sep 17 00:00:00 2001 From: zx Date: Mon, 16 Feb 2026 15:34:20 +0800 Subject: [PATCH 2/2] feat(proxy): delegate proxy handling to wreq-js and simplify fetch logic * Offload proxy configuration to `wreq-js` session in `tlsClient` * Remove manual proxy agent management and dependencies in `proxyFetch` * Remove redundant `ResponseWrapper` and `HeadersCompat` classes * Implement `NO_PROXY` support to bypass TLS client when needed --- open-sse/utils/proxyFetch.js | 85 ++++++----------------- open-sse/utils/tlsClient.js | 130 ++++++++++------------------------- 2 files changed, 56 insertions(+), 159 deletions(-) diff --git a/open-sse/utils/proxyFetch.js b/open-sse/utils/proxyFetch.js index 2bca646f..1f28a104 100644 --- a/open-sse/utils/proxyFetch.js +++ b/open-sse/utils/proxyFetch.js @@ -3,89 +3,45 @@ 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) return false; - if (noProxy) { - const hostname = new URL(targetUrl).hostname.toLowerCase(); - const patterns = noProxy.split(",").map(p => p.trim().toLowerCase()); + 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; - } - - 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; + 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}`); + }); } /** - * 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; -} - -/** - * Patched fetch with TLS fingerprint spoofing (Chrome 124 via wreq-js) - * and proxy support with 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). * - * Priority: - * 1. If proxy configured → use original fetch + proxy agent (wreq-js doesn't support proxy agents) - * 2. No proxy → use TLS client (wreq-js Chrome 124 fingerprint) - * 3. TLS client fails → fallback to original fetch + * 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); } - // No proxy — use TLS client for Chrome 124 fingerprint spoofing + // Use TLS client (handles both TLS fingerprint + proxy) try { return await tlsClient.fetch(targetUrl, options); } catch (tlsError) { - // Fallback to original fetch if TLS client fails console.warn(`[ProxyFetch] TLS client failed, falling back to native fetch: ${tlsError.message}`); return originalFetch(url, options); } @@ -96,3 +52,4 @@ if (!isCloud) { } export default isCloud ? originalFetch : patchedFetch; + diff --git a/open-sse/utils/tlsClient.js b/open-sse/utils/tlsClient.js index 1f2556c8..40becf30 100644 --- a/open-sse/utils/tlsClient.js +++ b/open-sse/utils/tlsClient.js @@ -1,29 +1,53 @@ import { createRequire } from "module"; -import { Readable } from "stream"; 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.userAgent = "antigravity/1.104.0 darwin/arm64"; this.session = null; } async getSession() { if (this.session) return this.session; - this.session = await createSession({ + + const proxy = getProxyFromEnv(); + const sessionOpts = { browser: "chrome_124", os: "macos", - userAgent: this.userAgent - }); + }; + 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(); @@ -35,13 +59,13 @@ class TlsClient { redirect: options.redirect === "manual" ? "manual" : "follow", }; - try { - const response = await session.fetch(url, wreqOptions); - return new ResponseWrapper(response); - } catch (error) { - console.error("[TlsClient] wreq-js fetch failed:", error.message); - throw error; + // Pass signal through if available + if (options.signal) { + wreqOptions.signal = options.signal; } + + const response = await session.fetch(url, wreqOptions); + return response; } async exit() { @@ -52,88 +76,4 @@ class TlsClient { } } -/** - * Wraps wreq-js response to match standard fetch Response interface - */ -class ResponseWrapper { - constructor(wreqResponse) { - this.status = wreqResponse.status; - this.statusText = wreqResponse.statusText || (this.status === 200 ? "OK" : `Status ${this.status}`); - this.headers = new HeadersCompat(wreqResponse.headers); - this.url = wreqResponse.url; - this.ok = this.status >= 200 && this.status < 300; - - if (wreqResponse.body) { - if (typeof wreqResponse.body.getReader === "function") { - this.body = wreqResponse.body; - } else { - this.body = Readable.toWeb(wreqResponse.body); - } - } else { - this.body = null; - } - } - - async text() { - if (!this.body) return ""; - const reader = this.body.getReader(); - const chunks = []; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(typeof value === "string" ? Buffer.from(value) : value); - } - return Buffer.concat(chunks).toString("utf8"); - } - - async json() { - const text = await this.text(); - return JSON.parse(text); - } - - clone() { - // Minimal clone — creates a new wrapper that shares status/headers - // but allows independent body consumption via stored text - const cloned = Object.create(ResponseWrapper.prototype); - cloned.status = this.status; - cloned.statusText = this.statusText; - cloned.headers = this.headers; - cloned.url = this.url; - cloned.ok = this.ok; - cloned.body = this.body; - - // Store original text() for clone usage - const originalText = this.text.bind(this); - let cachedText = null; - - const getText = async () => { - if (cachedText === null) cachedText = await originalText(); - return cachedText; - }; - - this.text = getText; - cloned.text = getText; - cloned.json = async () => JSON.parse(await getText()); - this.json = async () => JSON.parse(await getText()); - - return cloned; - } -} - -/** - * Minimal Headers compatibility class for wreq-js responses - */ -class HeadersCompat { - constructor(headersObj = {}) { - this.map = new Map(); - for (const [key, value] of Object.entries(headersObj)) { - this.map.set(key.toLowerCase(), Array.isArray(value) ? value.join(", ") : value); - } - } - - get(name) { return this.map.get(name.toLowerCase()) || null; } - has(name) { return this.map.has(name.toLowerCase()); } - forEach(callback) { this.map.forEach(callback); } -} - export default new TlsClient();