Skip to content
Draft
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
2 changes: 1 addition & 1 deletion packages/bruno-electron/src/ipc/network/cert-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-electron/src/store/system-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const loadSystemProxy = async () => {
http_proxy: null,
https_proxy: null,
no_proxy: null,
pac_url: null,
source: 'error'
};
}
Expand Down
124 changes: 76 additions & 48 deletions packages/bruno-electron/src/utils/proxy-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,35 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent {
}
}

async function resolveAgentsFromPac({ pacSource, requestUrl, requestConfig, tlsOptions, httpsAgentRequestFields, timeline, disableCache, hostname }) {
const resolver = await getPacResolver({ pacSource, httpsAgentRequestFields });
const directives = await resolver.resolve(requestUrl);

if (!directives || !directives.length) {
return null;
}

const first = directives[0];

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 });
}

return directives;
}

async function setupProxyAgents({
requestConfig,
proxyMode = 'off',
Expand Down Expand Up @@ -184,67 +213,66 @@ 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 = await resolveAgentsFromPac({ pacSource: pac_url, requestUrl: requestConfig.url, requestConfig, tlsOptions, httpsAgentRequestFields, timeline, disableCache, hostname });
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 });
}
} 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 });
}
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') {
const pacSource = get(proxyConfig, 'pac.source');
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 = await resolveAgentsFromPac({ pacSource, requestUrl: requestConfig.url, requestConfig, tlsOptions, httpsAgentRequestFields, timeline, disableCache, hostname });
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)' });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ describe('SystemProxyResolver Integration', () => {
http_proxy: 'http://env-proxy.usebruno.com:9090',
https_proxy: 'https://system-proxy.usebruno.com:8443',
no_proxy: 'localhost',
pac_url: null,
source: 'windows-system + environment'
});
});
Expand Down Expand Up @@ -209,6 +210,7 @@ describe('SystemProxyResolver Integration', () => {
http_proxy: 'http://system-proxy.usebruno.com:8080',
https_proxy: 'https://system-proxy.usebruno.com:8443',
no_proxy: 'localhost',
pac_url: null,
source: 'macos-system'
});
});
Expand Down Expand Up @@ -263,6 +265,7 @@ describe('SystemProxyResolver Integration', () => {
http_proxy: null,
https_proxy: null,
no_proxy: null,
pac_url: null,
source: 'macos-system'
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export async function getSystemProxy(): Promise<ProxyConfiguration> {
http_proxy: proxyEnvironmentVariables?.http_proxy || systemProxyEnvironmentVariables?.http_proxy,
https_proxy: proxyEnvironmentVariables?.https_proxy || systemProxyEnvironmentVariables?.https_proxy,
no_proxy: proxyEnvironmentVariables?.no_proxy || systemProxyEnvironmentVariables?.no_proxy,
pac_url: systemProxyEnvironmentVariables?.pac_url || null,
source: hasEnvironmentProxy ? `${systemProxyEnvironmentVariables?.source} + environment` : systemProxyEnvironmentVariables?.source
};
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface ProxyConfiguration {
http_proxy?: string | null;
https_proxy?: string | null;
no_proxy?: string | null;
pac_url?: string | null;
source: string;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ export class LinuxProxyResolver implements ProxyResolver {
private async getGSettingsProxy(execOpts: ExecFileOptions): Promise<ProxyConfiguration | null> {
try {
const mode = await safeExec('gsettings', ['get', 'org.gnome.system.proxy', 'mode'], execOpts);

// Handle PAC (auto) mode
if (mode === '\'auto\'') {
const autoConfigUrl = await safeExec('gsettings', ['get', 'org.gnome.system.proxy', 'autoconfig-url'], execOpts);
const cleanUrl = (autoConfigUrl || '').replace(/'/g, '').trim();
if (cleanUrl) {
return {
http_proxy: null,
https_proxy: null,
no_proxy: null,
pac_url: cleanUrl,
source: 'linux-system'
};
}
return null;
}

if (mode !== '\'manual\'') {
return null;
}
Expand Down Expand Up @@ -93,8 +110,22 @@ export class LinuxProxyResolver implements ProxyResolver {
// 3 = Automatic proxy detection
// 4 = Use system proxy configuration (environment variables)

if (proxyType === '2') {
const pacUrl = await safeExec('kreadconfig5', ['--group', 'Proxy Settings', '--key', 'Proxy Config Script'], execOpts);
const cleanPacUrl = (pacUrl || '').trim();
if (cleanPacUrl) {
return {
http_proxy: null,
https_proxy: null,
no_proxy: null,
pac_url: cleanPacUrl,
source: 'linux-system'
};
}
return null;
}

if (proxyType !== '1') {
// Only handle manual proxy configuration for now
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('MacOSProxyResolver', () => {
http_proxy: 'http://proxy.usebruno.com:8080',
https_proxy: 'http://secure-proxy.usebruno.com:8443',
no_proxy: 'localhost,127.0.0.1,<local>',
pac_url: null,
source: 'macos-system'
Comment on lines +48 to 49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add one positive PAC-enabled test case.

Current updates assert pac_url: null, but the new PAC extraction path is not directly asserted with a non-null URL scenario.

Proposed test addition
 describe('scutil proxy detection', () => {
+  it('should detect PAC URL when PAC is enabled', async () => {
+    const scutilOutput = `<dictionary> {
+  ProxyAutoConfigEnable : 1
+  ProxyAutoConfigURLString : http://proxy.usebruno.com/proxy.pac
+  HTTPEnable : 0
+  HTTPSEnable : 0
+}`;
+
+    mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+    const result = await detector.detect();
+
+    expect(result).toEqual({
+      http_proxy: null,
+      https_proxy: null,
+      no_proxy: null,
+      pac_url: 'http://proxy.usebruno.com/proxy.pac',
+      source: 'macos-system'
+    });
+  });

As per coding guidelines **/*.{test,spec}.{js,jsx,ts,tsx}: Add tests for any new functionality or meaningful changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-requests/src/network/system-proxy/utils/macos.spec.ts` around
lines 48 - 49, Add a positive PAC-enabled test in macos.spec.ts that exercises
the new PAC extraction path: create a test case that stubs/mocks the macOS
system proxy output to include a PAC URL and assert that the parsed result (the
object under test that currently exposes pac_url) returns that non-null URL (not
null) and retains source: 'macos-system'; locate the spec in
packages/bruno-requests/src/network/system-proxy/utils/macos.spec.ts and add the
test alongside the existing cases that currently assert pac_url: null so it
covers the PAC-enabled branch of the parsing function that produces pac_url.

});
});
Expand All @@ -65,6 +66,7 @@ describe('MacOSProxyResolver', () => {
http_proxy: null,
https_proxy: null,
no_proxy: null,
pac_url: null,
source: 'macos-system'
});
});
Expand Down Expand Up @@ -102,6 +104,7 @@ describe('MacOSProxyResolver', () => {
http_proxy: 'http://proxy.usebruno.com:8080',
https_proxy: null,
no_proxy: null,
pac_url: null,
source: 'macos-system'
});
});
Expand All @@ -123,6 +126,7 @@ describe('MacOSProxyResolver', () => {
http_proxy: null,
https_proxy: 'http://secure-proxy.usebruno.com:8443',
no_proxy: null,
pac_url: null,
source: 'macos-system'
});
});
Expand All @@ -148,6 +152,7 @@ describe('MacOSProxyResolver', () => {
http_proxy: 'http://proxy.usebruno.com:8080',
https_proxy: 'http://proxy.usebruno.com:8080',
no_proxy: null,
pac_url: null,
source: 'macos-system'
});
});
Expand All @@ -171,6 +176,7 @@ describe('MacOSProxyResolver', () => {
http_proxy: 'http://proxy.usebruno.com:8080',
https_proxy: 'http://proxy.usebruno.com:8080',
no_proxy: '<local>',
pac_url: null,
source: 'macos-system'
});
});
Expand Down Expand Up @@ -200,6 +206,7 @@ describe('MacOSProxyResolver', () => {
http_proxy: 'http://proxy.usebruno.com:8080',
https_proxy: 'http://proxy.usebruno.com:8080',
no_proxy: 'localhost,127.0.0.1,*.local,192.168.1.0/24,<local>',
pac_url: null,
source: 'macos-system'
});
});
Expand All @@ -222,6 +229,7 @@ describe('MacOSProxyResolver', () => {
http_proxy: 'http://proxy.usebruno.com:8080',
https_proxy: null,
no_proxy: null,
pac_url: null,
source: 'macos-system'
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ export class MacOSProxyResolver implements ProxyResolver {
let http_proxy: string | null = null;
let https_proxy: string | null = null;
let no_proxy: string | null = null;
let pac_url: string | null = null;

// Check PAC (Proxy Auto-Configuration)
if (config.ProxyAutoConfigEnable === 1 && config.ProxyAutoConfigURLString) {
pac_url = config.ProxyAutoConfigURLString;
}

// Check HTTP proxy
if (config.HTTPEnable === 1 && config.HTTPProxy) {
Expand Down Expand Up @@ -109,6 +115,7 @@ export class MacOSProxyResolver implements ProxyResolver {
http_proxy,
https_proxy,
no_proxy: normalizeNoProxy(no_proxy),
pac_url,
source: 'macos-system'
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class WindowsProxyResolver implements ProxyResolver {
let proxyEnabled = false;
let proxyServer: string | null = null;
let proxyOverride: string | null = null;
let autoConfigURL: string | null = null;

for (const line of lines) {
const trimmedLine = line.trim();
Expand All @@ -68,6 +69,19 @@ export class WindowsProxyResolver implements ProxyResolver {
const match = trimmedLine.match(/ProxyOverride\s+REG_SZ\s+(.+)/);
if (match) proxyOverride = match[1].trim();
}

if (trimmedLine.includes('AutoConfigURL') && trimmedLine.includes('REG_SZ')) {
const match = trimmedLine.match(/AutoConfigURL\s+REG_SZ\s+(.+)/);
if (match) autoConfigURL = match[1].trim();
}
}

// PAC URL takes precedence — return it even without a manual proxy
if (autoConfigURL) {
const config = proxyEnabled && proxyServer
? this.parseProxyString(proxyServer, proxyOverride)
: { http_proxy: null, https_proxy: null, no_proxy: null, source: 'windows-system' };
return { ...config, pac_url: autoConfigURL };
}

if (proxyEnabled && proxyServer) {
Expand Down
Loading
Loading