mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
fix: honor OS-level PAC configuration in system proxy mode (#7766)
This commit is contained in:
@@ -148,7 +148,7 @@ const getCertsAndProxyConfig = async ({
|
||||
} else if (globalProxySource === 'inherit') {
|
||||
proxyMode = 'system';
|
||||
const systemProxyConfig = await getCachedSystemProxy();
|
||||
proxyConfig = systemProxyConfig || { http_proxy: null, https_proxy: null, no_proxy: null, source: 'cache-miss' };
|
||||
proxyConfig = systemProxyConfig || { http_proxy: null, https_proxy: null, no_proxy: null, pac_url: null, source: 'cache-miss' };
|
||||
} else {
|
||||
// source === 'manual'
|
||||
proxyConfig = globalProxyConfigData;
|
||||
|
||||
@@ -12,6 +12,7 @@ const loadSystemProxy = async () => {
|
||||
http_proxy: null,
|
||||
https_proxy: null,
|
||||
no_proxy: null,
|
||||
pac_url: null,
|
||||
source: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
const parseUrl = require('url').parse;
|
||||
const https = require('node:https');
|
||||
const http = require('node:http');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
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 } = require('@usebruno/requests');
|
||||
const {
|
||||
getOrCreateHttpsAgent,
|
||||
getOrCreateHttpAgent,
|
||||
resolveAgentsFromPac,
|
||||
PatchedHttpsProxyAgent
|
||||
} = require('@usebruno/requests');
|
||||
const { preferencesUtil } = require('../store/preferences');
|
||||
const { getPacResolver } = require('@usebruno/requests');
|
||||
|
||||
const DEFAULT_PORTS = {
|
||||
ftp: 21,
|
||||
@@ -70,40 +73,6 @@ const shouldUseProxy = (url, proxyBypass) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Options that should be forwarded from the constructor to the target TLS upgrade.
|
||||
*/
|
||||
const TARGET_TLS_OPTIONS = ['cert', 'key', 'pfx', 'passphrase', 'rejectUnauthorized', 'secureContext'];
|
||||
|
||||
/**
|
||||
* Patched version of HttpsProxyAgent that correctly handles TLS options for
|
||||
* both the proxy connection and the target server connection.
|
||||
*
|
||||
* The upstream HttpsProxyAgent (https://github.com/TooTallNate/proxy-agents/issues/194)
|
||||
* ignores constructor options when upgrading the tunneled socket to TLS for the
|
||||
* target server. This patch forwards the relevant TLS options to the target upgrade.
|
||||
*/
|
||||
class PatchedHttpsProxyAgent extends HttpsProxyAgent {
|
||||
constructor(proxy, opts) {
|
||||
super(proxy, opts);
|
||||
this.constructorOpts = opts;
|
||||
}
|
||||
|
||||
async connect(req, opts) {
|
||||
const targetOpts = { ...opts };
|
||||
|
||||
if (this.constructorOpts) {
|
||||
for (const key of TARGET_TLS_OPTIONS) {
|
||||
if (key in this.constructorOpts) {
|
||||
targetOpts[key] = this.constructorOpts[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.connect(req, targetOpts);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupProxyAgents({
|
||||
requestConfig,
|
||||
proxyMode = 'off',
|
||||
@@ -184,40 +153,58 @@ async function setupProxyAgents({
|
||||
}
|
||||
}
|
||||
} else if (proxyMode === 'system') {
|
||||
const { http_proxy, https_proxy, no_proxy } = proxyConfig || {};
|
||||
const shouldUseSystemProxy = shouldUseProxy(requestConfig.url, no_proxy || '');
|
||||
if (shouldUseSystemProxy) {
|
||||
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 {
|
||||
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 });
|
||||
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 (error) {
|
||||
throw new Error(`Invalid system http_proxy "${http_proxy}": ${error.message}`);
|
||||
} catch (err) {
|
||||
if (timeline) { timeline.push({ timestamp: new Date(), type: 'error', message: `System PAC resolution failed: ${err.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}`
|
||||
});
|
||||
} 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 });
|
||||
}
|
||||
requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions, proxyUri: https_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}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid system https_proxy "${https_proxy}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
} else if (proxyMode === 'pac') {
|
||||
@@ -225,26 +212,11 @@ async function setupProxyAgents({
|
||||
if (pacSource) {
|
||||
if (timeline) timeline.push({ timestamp: new Date(), type: 'info', message: `Resolving PAC: ${pacSource}` });
|
||||
try {
|
||||
const resolver = await getPacResolver({ pacSource, httpsAgentRequestFields });
|
||||
const directives = await resolver.resolve(requestConfig.url);
|
||||
if (directives && directives.length) {
|
||||
const first = directives[0];
|
||||
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('; ')}` });
|
||||
if (/^(PROXY|HTTPS?)\s+/i.test(first)) {
|
||||
const parts = first.split(/\s+/);
|
||||
const keyword = parts[0].toUpperCase();
|
||||
const hostPort = parts[1];
|
||||
const scheme = keyword === 'HTTPS' ? 'https' : 'http';
|
||||
const proxyUri = `${scheme}://${hostPort}`;
|
||||
requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: HttpProxyAgent, options: { keepAlive: true }, proxyUri, timeline, disableCache, hostname });
|
||||
requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions, proxyUri, timeline, disableCache, hostname });
|
||||
} else if (/^SOCKS/i.test(first)) {
|
||||
const hostPort = first.split(/\s+/)[1];
|
||||
const proto = /^SOCKS4\s/i.test(first) ? 'socks4' : 'socks5';
|
||||
const proxyUri = `${proto}://${hostPort}`;
|
||||
requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: SocksProxyAgent, options: { keepAlive: true }, proxyUri, timeline, disableCache, hostname });
|
||||
requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: SocksProxyAgent, options: tlsOptions, proxyUri, timeline, disableCache, hostname });
|
||||
}
|
||||
} else {
|
||||
if (timeline) timeline.push({ timestamp: new Date(), type: 'info', message: 'PAC resolved: DIRECT (no proxy)' });
|
||||
}
|
||||
|
||||
@@ -12,16 +12,51 @@ const setupMocks = ({ pacDirectives = ['PROXY p.example:8080'] } = {}) => {
|
||||
}
|
||||
}));
|
||||
|
||||
// @usebruno/requests — agent factories + pac resolver
|
||||
jest.doMock('@usebruno/requests', () => ({
|
||||
getOrCreateHttpsAgent: jest.fn(() => ({ type: 'https-agent' })),
|
||||
getOrCreateHttpAgent: jest.fn(() => ({ type: 'http-agent' })),
|
||||
getPacResolver: jest.fn(async () => ({
|
||||
// @usebruno/requests — agent factories + pac resolver + shared resolveAgentsFromPac
|
||||
jest.doMock('@usebruno/requests', () => {
|
||||
const getOrCreateHttpsAgent = jest.fn(() => ({ type: 'https-agent' }));
|
||||
const getOrCreateHttpAgent = jest.fn(() => ({ type: 'http-agent' }));
|
||||
const getPacResolver = jest.fn(async () => ({
|
||||
resolve: async () => pacDirectives,
|
||||
dispose: () => {}
|
||||
})),
|
||||
clearPacCache: jest.fn()
|
||||
}));
|
||||
}));
|
||||
// Inline mock of resolveAgentsFromPac that wires through the mocked factories
|
||||
// so existing assertions on getOrCreateHttp(s)Agent call args still hold.
|
||||
const resolveAgentsFromPac = jest.fn(async ({ pacSource, requestUrl, tlsOptions, httpsAgentRequestFields, timeline, disableCache, hostname }) => {
|
||||
const resolver = await getPacResolver({ pacSource, httpsAgentRequestFields });
|
||||
const directives = await resolver.resolve(requestUrl);
|
||||
if (!directives || !directives.length) return { directives: null };
|
||||
const first = directives[0];
|
||||
if (/^(PROXY|HTTPS?)\s+/i.test(first)) {
|
||||
const parts = first.split(/\s+/);
|
||||
const scheme = parts[0].toUpperCase() === 'HTTPS' ? 'https' : 'http';
|
||||
const proxyUri = `${scheme}://${parts[1]}`;
|
||||
return {
|
||||
directives,
|
||||
httpAgent: getOrCreateHttpAgent({ proxyUri, options: { keepAlive: true }, timeline, disableCache, hostname }),
|
||||
httpsAgent: getOrCreateHttpsAgent({ proxyUri, options: tlsOptions, timeline, disableCache, hostname })
|
||||
};
|
||||
}
|
||||
if (/^SOCKS/i.test(first)) {
|
||||
const proto = /^SOCKS4\s/i.test(first) ? 'socks4' : 'socks5';
|
||||
const proxyUri = `${proto}://${first.split(/\s+/)[1]}`;
|
||||
return {
|
||||
directives,
|
||||
httpAgent: getOrCreateHttpAgent({ proxyUri, options: { keepAlive: true }, timeline, disableCache, hostname }),
|
||||
httpsAgent: getOrCreateHttpsAgent({ proxyUri, options: tlsOptions, timeline, disableCache, hostname })
|
||||
};
|
||||
}
|
||||
return { directives };
|
||||
});
|
||||
return {
|
||||
getOrCreateHttpsAgent,
|
||||
getOrCreateHttpAgent,
|
||||
getPacResolver,
|
||||
resolveAgentsFromPac,
|
||||
PatchedHttpsProxyAgent: class {},
|
||||
clearPacCache: jest.fn()
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
describe('proxy-util', () => {
|
||||
@@ -120,6 +155,8 @@ describe('proxy-util', () => {
|
||||
getOrCreateHttpsAgent: jest.fn(() => ({ type: 'https-agent' })),
|
||||
getOrCreateHttpAgent: jest.fn(() => ({ type: 'http-agent' })),
|
||||
getPacResolver: jest.fn(async () => { throw new Error('PAC fetch timeout'); }),
|
||||
resolveAgentsFromPac: jest.fn(async () => { throw new Error('PAC fetch timeout'); }),
|
||||
PatchedHttpsProxyAgent: class {},
|
||||
clearPacCache: jest.fn()
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user