const parseUrl = require('url').parse; const https = require('node:https'); const http = require('node:http'); const { interpolateString } = require('../ipc/network/interpolate-string'); const { SocksProxyAgent } = require('socks-proxy-agent'); const { HttpProxyAgent } = require('http-proxy-agent'); const { isEmpty, get, isUndefined, isNull } = require('lodash'); const { getOrCreateHttpsAgent, getOrCreateHttpAgent, resolveAgentsFromPac, PatchedHttpsProxyAgent } = require('@usebruno/requests'); const { preferencesUtil } = require('../store/preferences'); const DEFAULT_PORTS = { ftp: 21, gopher: 70, http: 80, https: 443, ws: 80, wss: 443 }; /** * check for proxy bypass, copied form 'proxy-from-env' */ const shouldUseProxy = (url, proxyBypass) => { if (proxyBypass === '*') { return false; // Never proxy if wildcard is set. } // use proxy if no proxyBypass is set if (!proxyBypass || typeof proxyBypass !== 'string' || isEmpty(proxyBypass.trim())) { return true; } const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {}; let proto = parsedUrl.protocol; let hostname = parsedUrl.host; let port = parsedUrl.port; if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') { return false; // Don't proxy URLs without a valid scheme or host. } proto = proto.split(':', 1)[0]; // Stripping ports in this way instead of using parsedUrl.hostname to make // sure that the brackets around IPv6 addresses are kept. hostname = hostname.replace(/:\d*$/, ''); port = parseInt(port) || DEFAULT_PORTS[proto] || 0; return proxyBypass.split(/[,;\s]/).every(function (dontProxyFor) { if (!dontProxyFor) { return true; // Skip zero-length hosts. } const parsedProxy = dontProxyFor.match(/^(.+):(\d+)$/); let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor; const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0; if (parsedProxyPort && parsedProxyPort !== port) { return true; // Skip if ports don't match. } if (!/^[.*]/.test(parsedProxyHostname)) { // No wildcards, so stop proxying if there is an exact match. return hostname !== parsedProxyHostname; } if (parsedProxyHostname.charAt(0) === '*') { // Remove leading wildcard. parsedProxyHostname = parsedProxyHostname.slice(1); } // Stop proxying if the hostname ends with the no_proxy host. return !hostname.endsWith(parsedProxyHostname); }); }; async function setupProxyAgents({ requestConfig, proxyMode = 'off', proxyModeReason = '', proxyConfig, httpsAgentRequestFields, interpolationOptions, timeline }) { if (timeline) { let modeMsg = `Proxy mode: ${proxyMode}`; if (proxyMode === 'pac') modeMsg += ` | PAC URL: ${get(proxyConfig, 'pac.source') || '(empty)'}`; else if (proxyMode === 'on') modeMsg += ` | ${get(proxyConfig, 'protocol')}://${get(proxyConfig, 'hostname')}:${get(proxyConfig, 'port')}`; else if (proxyMode === 'off' && proxyModeReason) modeMsg += ` (${proxyModeReason})`; timeline.push({ timestamp: new Date(), type: 'info', message: modeMsg }); } // Clear stale agents so we always recreate them for the current URL // (handles protocol switches, host changes, and proxy-bypass rules on redirects). delete requestConfig.httpAgent; delete requestConfig.httpsAgent; const disableCache = !preferencesUtil.isSslSessionCachingEnabled(); // Ensure TLS options are properly set const tlsOptions = { ...httpsAgentRequestFields, // Enable all secure protocols by default secureProtocol: undefined, // Allow Node.js to choose the protocol minVersion: 'TLSv1', rejectUnauthorized: httpsAgentRequestFields.rejectUnauthorized !== undefined ? httpsAgentRequestFields.rejectUnauthorized : true, // Enable keepAlive for connection reuse keepAlive: true }; const parsedUrl = parseUrl(requestConfig.url); const isHttpsRequest = parsedUrl.protocol === 'https:'; const hostname = parsedUrl.hostname || null; if (proxyMode === 'on') { const shouldProxy = shouldUseProxy(requestConfig.url, get(proxyConfig, 'bypassProxy', '')); if (shouldProxy) { const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions); const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions); const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions); const proxyAuthEnabled = !get(proxyConfig, 'auth.disabled', false); const socksEnabled = proxyProtocol.includes('socks'); let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`; let proxyUri; if (proxyAuthEnabled) { const proxyAuthUsername = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions)); const proxyAuthPassword = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions)); proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`; } else { proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`; } // When the proxy itself uses HTTPS, the agent connecting to it needs TLS options // (e.g., ca certs) even for plain HTTP requests const isHttpsProxy = proxyProtocol === 'https'; const httpProxyAgentOptions = isHttpsProxy ? { keepAlive: true, ...tlsOptions } : { keepAlive: true }; // Only set the agent needed for the request protocol if (socksEnabled) { if (isHttpsRequest) { requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: SocksProxyAgent, options: tlsOptions, proxyUri, timeline, disableCache, hostname }); } else { requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: SocksProxyAgent, options: httpProxyAgentOptions, proxyUri, timeline, disableCache, hostname }); } } else { if (isHttpsRequest) { requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions, proxyUri, timeline, disableCache, hostname }); } else { requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: HttpProxyAgent, options: httpProxyAgentOptions, proxyUri, timeline, disableCache, hostname }); } } } } else if (proxyMode === 'system') { const { http_proxy, https_proxy, no_proxy, pac_url } = proxyConfig || {}; // If the OS is configured with a PAC URL, resolve it using the existing PAC infrastructure if (pac_url) { if (timeline) timeline.push({ timestamp: new Date(), type: 'info', message: `Resolving system PAC: ${pac_url}` }); try { const { directives, httpAgent, httpsAgent } = await resolveAgentsFromPac({ pacSource: pac_url, requestUrl: requestConfig.url, requestProtocol: isHttpsRequest ? 'https' : 'http', tlsOptions, httpsAgentRequestFields, timeline, disableCache, hostname }); if (httpAgent) requestConfig.httpAgent = httpAgent; if (httpsAgent) requestConfig.httpsAgent = httpsAgent; if (directives) { if (timeline) { timeline.push({ timestamp: new Date(), type: 'info', message: `PAC directives: ${directives.join('; ')}` }); } } else { if (timeline) { timeline.push({ timestamp: new Date(), type: 'info', message: 'System PAC resolved: DIRECT (no proxy)' }); } } } catch (err) { if (timeline) { timeline.push({ timestamp: new Date(), type: 'error', message: `System PAC resolution failed: ${err.message}` }); } } } else { const shouldUseSystemProxy = shouldUseProxy(requestConfig.url, no_proxy || ''); if (shouldUseSystemProxy) { try { if (http_proxy?.length && !isHttpsRequest) { const parsedHttpProxy = new URL(http_proxy); const isHttpsSystemProxy = parsedHttpProxy.protocol === 'https:'; const systemHttpProxyAgentOptions = isHttpsSystemProxy ? { keepAlive: true, ...tlsOptions } : { keepAlive: true }; if (timeline) { timeline.push({ timestamp: new Date(), type: 'info', message: `Using system proxy: ${http_proxy}` }); } requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: HttpProxyAgent, options: systemHttpProxyAgentOptions, proxyUri: http_proxy, timeline, disableCache, hostname }); } } catch (error) { throw new Error(`Invalid system http_proxy "${http_proxy}": ${error.message}`); } try { if (https_proxy?.length && isHttpsRequest) { new URL(https_proxy); if (timeline) { timeline.push({ timestamp: new Date(), type: 'info', message: `Using system proxy: ${https_proxy}` }); } requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions, proxyUri: https_proxy, timeline, disableCache, hostname }); } } catch (error) { throw new Error(`Invalid system https_proxy "${https_proxy}": ${error.message}`); } } } } else if (proxyMode === 'pac') { const pacSource = get(proxyConfig, 'pac.source'); if (pacSource) { if (timeline) timeline.push({ timestamp: new Date(), type: 'info', message: `Resolving PAC: ${pacSource}` }); try { const { directives, httpAgent, httpsAgent } = await resolveAgentsFromPac({ pacSource, requestUrl: requestConfig.url, requestProtocol: isHttpsRequest ? 'https' : 'http', tlsOptions, httpsAgentRequestFields, timeline, disableCache, hostname }); if (httpAgent) requestConfig.httpAgent = httpAgent; if (httpsAgent) requestConfig.httpsAgent = httpsAgent; if (directives) { if (timeline) timeline.push({ timestamp: new Date(), type: 'info', message: `PAC directives: ${directives.join('; ')}` }); } else { if (timeline) timeline.push({ timestamp: new Date(), type: 'info', message: 'PAC resolved: DIRECT (no proxy)' }); } } catch (err) { if (timeline) timeline.push({ timestamp: new Date(), type: 'error', message: `PAC resolution failed: ${err.message}` }); } } } if (!requestConfig.httpAgent && !requestConfig.httpsAgent) { if (isHttpsRequest) { requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: tlsOptions, proxyUri: null, timeline, disableCache, hostname }); } else { requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: http.Agent, options: { keepAlive: true }, proxyUri: null, timeline, disableCache, hostname }); } } } module.exports = { shouldUseProxy, PatchedHttpsProxyAgent, setupProxyAgents };