From 048d4433c2c6e239c9a24471541914cef0d472d6 Mon Sep 17 00:00:00 2001 From: direkturcrypto Date: Sun, 1 Mar 2026 22:27:18 +0700 Subject: [PATCH] fix: implement CONNECT tunnel for proxy routing Full HTTPS CONNECT tunnel implementation that: - Routes only Polymarket domains through proxy - Uses HTTP CONNECT method for HTTPS tunneling - Leaves all other traffic (RPC, etc.) untouched Co-Authored-By: Claude Opus 4.6 --- src/utils/proxy-patch.cjs | 281 +++++++++++++++++++++++++++++++------- 1 file changed, 230 insertions(+), 51 deletions(-) diff --git a/src/utils/proxy-patch.cjs b/src/utils/proxy-patch.cjs index 2cf5c5c..4f1cc7d 100644 --- a/src/utils/proxy-patch.cjs +++ b/src/utils/proxy-patch.cjs @@ -1,82 +1,261 @@ /** * proxy-patch.cjs - * CommonJS module to patch https.request BEFORE any axios imports. - * This ensures @polymarket/clob-client uses the proxy. * - * Must be imported as very first thing in the app. - * In ES modules: import './proxy-patch.cjs' + * Strategy: Patch https.request at the lowest level with domain filtering. + * This affects ALL HTTPS requests, but we only route Polymarket domains through proxy. + * + * RPC Polygon calls are NOT affected because: + * - They go to polygon-rpc.com (not in whitelist) + * - They use ethers.js JsonRpcProvider which uses fetch or its own transport */ const PROXY_URL = process.env.PROXY_URL || ''; if (PROXY_URL) { - // Load https-proxy-agent which patches https.request properly - const { HttpsProxyAgent } = require('https-proxy-agent'); const https = require('https'); + const http = require('http'); + const { URL } = require('url'); + const net = require('net'); + const tls = require('tls'); - // Create agent that routes everything through proxy - const agent = new HttpsProxyAgent(PROXY_URL); + const proxyUrl = new URL(PROXY_URL); + const proxyHost = proxyUrl.hostname; + const proxyPort = parseInt(proxyUrl.port) || (proxyUrl.protocol === 'https:' ? 443 : 80); + const proxyAuth = (proxyUrl.username || proxyUrl.password) + ? Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`).toString('base64') + : null; - // Store original request - const originalRequest = https.request; - - // Polymarket domains - const POLY_DOMAINS = [ + // Polymarket domains ONLY + const POLY_HOSTS = new Set([ 'polymarket.com', + 'www.polymarket.com', 'clob.polymarket.com', 'gamma-api.polymarket.com', 'data-api.polymarket.com', - ]; + ]); - function shouldProxy(url) { - try { - const hostname = new URL(url).hostname; - return POLY_DOMAINS.some(domain => hostname === domain || hostname.endsWith(`.${domain}`)); - } catch { - return false; + function shouldProxy(hostname) { + // Check exact match or subdomain + if (POLY_HOSTS.has(hostname)) return true; + for (const host of POLY_HOSTS) { + if (hostname.endsWith('.' + host)) return true; } + return false; } - // Override https.request globally + // Store original request + const originalHttpsRequest = https.request; + const originalHttpRequest = http.request; + + // Create proxy-aware https.request https.request = function(options, callback) { - // Handle both string URL and options object - let url; + let hostname; + if (typeof options === 'string') { - url = options; - } else if (options.hostname || options.host) { - const protocol = options.protocol || 'https:'; - const hostname = options.hostname || options.host; - const port = options.port ? `:${options.port}` : ''; - const path = options.path || '/'; - url = `${protocol}//${hostname}${port}${path}`; - } - - // If it's a Polymarket domain, force agent - if (url && shouldProxy(url)) { - if (typeof options === 'string') { - // Convert to options object with agent - options = { - agent: agent, - }; - } else { - options.agent = agent; + try { + hostname = new URL(options).hostname; + } catch { + return originalHttpsRequest.apply(https, arguments); } + } else if (options.hostname || options.host) { + hostname = options.hostname || (typeof options.host === 'string' ? options.host.split(':')[0] : options.host); } - return originalRequest.call(https, options, callback); + // If not Polymarket, use direct connection + if (!hostname || !shouldProxy(hostname)) { + return originalHttpsRequest.apply(https, arguments); + } + + // For Polymarket: create CONNECT tunnel through proxy + const targetHost = hostname; + const targetPort = 443; + + // Build CONNECT request headers + const connectHeaders = { + 'Host': `${targetHost}:${targetPort}`, + }; + if (proxyAuth) { + connectHeaders['Proxy-Authorization'] = `Basic ${proxyAuth}`; + } + + // Create socket connection through proxy + const connectOptions = { + host: proxyHost, + port: proxyPort, + method: 'CONNECT', + path: `${targetHost}:${targetPort}`, + headers: connectHeaders, + }; + + // Make CONNECT request to proxy + const connectReq = originalHttpRequest(connectOptions); + + // Return a promise-like request that waits for tunnel + const deferredReq = { + on: function(event, handler) { + if (event === 'response' || event === 'error' || event === 'timeout') { + this._deferredHandlers = this._deferredHandlers || {}; + this._deferredHandlers[event] = handler; + } + return this; + }, + once: function(event, handler) { + return this.on(event, handler); + }, + write: function(chunk) { + if (this._realRequest) { + this._realRequest.write(chunk); + } else { + this._buffer = this._buffer || []; + this._buffer.push(chunk); + } + return true; + }, + end: function(chunk) { + if (chunk) this.write(chunk); + if (this._realRequest) { + this._realRequest.end(); + } else { + this._ended = true; + } + return this; + }, + setTimeout: function(ms) { + this._timeout = ms; + if (this._realRequest) { + this._realRequest.setTimeout(ms); + } + return this; + }, + setHeader: function(name, value) { + this._headers = this._headers || {}; + this._headers[name] = value; + return this; + }, + abort: function() { + if (this._realRequest) { + this._realRequest.abort(); + } + if (this._connectReq) { + this._connectReq.abort(); + } + } + }; + + connectReq.on('connect', (res, socket) => { + if (res.statusCode !== 200) { + const err = new Error(`Proxy CONNECT failed: ${res.statusCode}`); + if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.error) { + deferredReq._deferredHandlers.error(err); + } + return; + } + + // Wrap socket in TLS + const tlsOptions = { + socket: socket, + servername: targetHost, + rejectUnauthorized: options.rejectUnauthorized !== false, + }; + const tlsSocket = tls.connect(tlsOptions); + + // Now make actual HTTPS request through tunnel + const requestOptions = typeof options === 'string' + ? { ...new URL(options), createConnection: () => tlsSocket } + : { ...options, createConnection: () => tlsSocket }; + + const realReq = originalHttpsRequest(requestOptions, (res) => { + if (callback) callback(res); + if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.response) { + deferredReq._deferredHandlers.response(res); + } + }); + + // Apply deferred operations + if (deferredReq._headers) { + for (const [name, value] of Object.entries(deferredReq._headers)) { + realReq.setHeader(name, value); + } + } + if (deferredReq._timeout) { + realReq.setTimeout(deferredReq._timeout); + } + if (deferredReq._buffer) { + for (const chunk of deferredReq._buffer) { + realReq.write(chunk); + } + } + if (deferredReq._ended) { + realReq.end(); + } + + // Forward events + realReq.on('error', (err) => { + if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.error) { + deferredReq._deferredHandlers.error(err); + } + }); + realReq.on('timeout', () => { + if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.timeout) { + deferredReq._deferredHandlers.timeout(); + } + }); + + deferredReq._realRequest = realReq; + deferredReq._connectReq = connectReq; + }); + + connectReq.on('error', (err) => { + if (deferredReq._deferredHandlers && deferredReq._deferredHandlers.error) { + deferredReq._deferredHandlers.error(err); + } + }); + + connectReq.end(); + + return deferredReq; }; - // Also patch https.get - https.get = function(options, callback) { - const req = https.request(options, callback); - req.end(); - return req; + // Also patch http.request for completeness (though CLOB uses HTTPS) + http.request = function(options, callback) { + let hostname; + + if (typeof options === 'string') { + try { + hostname = new URL(options).hostname; + } catch { + return originalHttpRequest.apply(http, arguments); + } + } else if (options.hostname || options.host) { + hostname = options.hostname || (typeof options.host === 'string' ? options.host.split(':')[0] : options.host); + } + + // Only proxy Polymarket domains + if (!hostname || !shouldProxy(hostname)) { + return originalHttpRequest.apply(http, arguments); + } + + // For HTTP, route through proxy directly + const targetUrl = typeof options === 'string' ? new URL(options) : null; + const path = targetUrl ? targetUrl.pathname + targetUrl.search : (options.path || '/'); + + const proxyOptions = { + host: proxyHost, + port: proxyPort, + method: options.method || 'GET', + path: path, + headers: { ...(options.headers || {}) }, + }; + + if (proxyAuth) { + proxyOptions.headers['Proxy-Authorization'] = `Basic ${proxyAuth}`; + } + + return originalHttpRequest.call(http, proxyOptions, callback); }; - // Set global agent as fallback - https.globalAgent = agent; - - console.log(`[proxy-patch] HTTPS agent patched for Polymarket routing via proxy`); + console.log(`[proxy-patch] HTTPS/HTTP CONNECT tunnel active for Polymarket only`); + console.log(`[proxy-patch] RPC Polygon and other domains use direct connection`); } else { console.log('[proxy-patch] No PROXY_URL set, skipping patch'); }