diff --git a/package-lock.json b/package-lock.json
index d5768d9b7..629cf368d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35669,4 +35669,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/ProxySettings/StyledWrapper.js
index 69eecfb12..966a67c24 100644
--- a/packages/bruno-app/src/components/Preferences/ProxySettings/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Preferences/ProxySettings/StyledWrapper.js
@@ -25,6 +25,40 @@ const StyledWrapper = styled.div`
label {
color: ${(props) => props.theme.colors.text.yellow};
}
+
+ .system-proxy-title {
+ color: ${(props) => props.theme.text};
+ }
+
+ .system-proxy-description {
+ color: ${(props) => props.theme.colors.text.muted};
+ }
+
+ .system-proxy-error-container {
+ background: ${(props) => props.theme.status.danger.background};
+ border: 1px solid ${(props) => props.theme.status.danger.border};
+ }
+
+ .system-proxy-error-text {
+ color: ${(props) => props.theme.status.danger.text};
+ }
+
+ .system-proxy-source-label {
+ color: ${(props) => props.theme.colors.text.muted};
+ }
+
+ .system-proxy-source-value {
+ color: ${(props) => props.theme.text};
+ }
+
+ .system-proxy-info-text {
+ color: ${(props) => props.theme.colors.text.muted};
+ }
+
+ .system-proxy-value {
+ color: ${(props) => props.theme.colors.text.purple};
+ opacity: 0.8;
+ }
}
`;
diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/SystemProxy/index.js b/packages/bruno-app/src/components/Preferences/ProxySettings/SystemProxy/index.js
new file mode 100644
index 000000000..0d21aebbd
--- /dev/null
+++ b/packages/bruno-app/src/components/Preferences/ProxySettings/SystemProxy/index.js
@@ -0,0 +1,83 @@
+import { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { IconLoader2 } from '@tabler/icons';
+import { getSystemProxyVariables } from 'providers/ReduxStore/slices/app';
+import StyledWrapper from '../StyledWrapper';
+
+const SystemProxy = () => {
+ const dispatch = useDispatch();
+ const systemProxyVariables = useSelector((state) => state.app.systemProxyVariables);
+ const { source, http_proxy, https_proxy, no_proxy } = systemProxyVariables || {};
+ const [isFetching, setIsFetching] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ dispatch(getSystemProxyVariables())
+ .then(() => setError(null))
+ .catch((err) => setError(err.message || String(err)))
+ .finally(() => setIsFetching(false));
+ }, [dispatch]);
+
+ return (
+
+
+
+
+
+
+ System Proxy {isFetching ? : null}
+
+
+ Below values are sourced from your system proxy settings.
+
+
+
+
+ {error && (
+
+
+ Error loading system proxy settings: {error}
+
+
+ )}
+ {source && (
+
+
+
+ Proxy source:
+
+
+ {source}
+
+
+
+ )}
+
+ These values cannot be directly updated in Bruno. Please refer to your OS documentation to update these.
+
+
+
+
+ http_proxy
+
+
{http_proxy || '-'}
+
+
+
+ https_proxy
+
+
{https_proxy || '-'}
+
+
+
+ no_proxy
+
+
{no_proxy || '-'}
+
+
+
+
+ );
+};
+
+export default SystemProxy;
diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js
index 2601093ca..241c7a7cb 100644
--- a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js
+++ b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js
@@ -9,13 +9,11 @@ import StyledWrapper from './StyledWrapper';
import { useDispatch, useSelector } from 'react-redux';
import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react';
+import SystemProxy from './SystemProxy';
const ProxySettings = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences);
- const systemProxyEnvVariables = useSelector((state) => state.app.systemProxyEnvVariables);
- const { http_proxy, https_proxy, no_proxy } = systemProxyEnvVariables || {};
const dispatch = useDispatch();
- console.log(preferences);
const proxySchema = Yup.object({
disabled: Yup.boolean().optional(),
@@ -167,30 +165,7 @@ const ProxySettings = ({ close }) => {
{formik.values.disabled === false && formik.values.inherit === true ? (
-
- Below values are sourced from your system environment variables and cannot be directly updated in Bruno.
- Please refer to your OS documentation to change these values.
-
-
-
-
- http_proxy
-
-
{http_proxy || '-'}
-
-
-
- https_proxy
-
-
{https_proxy || '-'}
-
-
-
- no_proxy
-
-
{no_proxy || '-'}
-
-
+
) : null}
{formik.values.disabled === false && formik.values.inherit === false ? (
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index 57ad166b5..4b256555a 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -1,8 +1,7 @@
import { useEffect } from 'react';
import {
updateCookies,
- updatePreferences,
- updateSystemProxyEnvVariables
+ updatePreferences
} from 'providers/ReduxStore/slices/app';
import {
addTab
@@ -271,10 +270,6 @@ const useIpcEvents = () => {
dispatch(updatePreferences(val));
});
- const removeSystemProxyEnvUpdatesListener = ipcRenderer.on('main:load-system-proxy-env', (val) => {
- dispatch(updateSystemProxyEnvVariables(val));
- });
-
const removeCookieUpdateListener = ipcRenderer.on('main:cookies-update', (val) => {
dispatch(updateCookies(val));
});
@@ -331,7 +326,6 @@ const useIpcEvents = () => {
removeShowPreferencesListener();
removePreferencesUpdatesListener();
removeCookieUpdateListener();
- removeSystemProxyEnvUpdatesListener();
removeGlobalEnvironmentsUpdatesListener();
removeSnapshotHydrationListener();
removeCollectionOauth2CredentialsUpdatesListener();
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
index 32bf792d5..d70f6c0a1 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
@@ -48,10 +48,10 @@ const initialState = {
},
cookies: [],
taskQueue: [],
- systemProxyEnvVariables: {},
clipboard: {
hasCopiedItems: false // Whether clipboard has Bruno data (for UI)
- }
+ },
+ systemProxyVariables: {}
};
export const appSlice = createSlice({
@@ -111,8 +111,8 @@ export const appSlice = createSlice({
removeAllTasksFromQueue: (state) => {
state.taskQueue = [];
},
- updateSystemProxyEnvVariables: (state, action) => {
- state.systemProxyEnvVariables = action.payload;
+ updateSystemProxyVariables: (state, action) => {
+ state.systemProxyVariables = action.payload;
},
updateGenerateCode: (state, action) => {
state.generateCode = {
@@ -161,7 +161,7 @@ export const {
insertTaskIntoQueue,
removeTaskFromQueue,
removeAllTasksFromQueue,
- updateSystemProxyEnvVariables,
+ updateSystemProxyVariables,
updateGenerateCode,
toggleSidebarCollapse,
setClipboard
@@ -236,4 +236,16 @@ export const copyRequest = (item) => (dispatch, getState) => {
return Promise.resolve();
};
+export const getSystemProxyVariables = () => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const { ipcRenderer } = window;
+ ipcRenderer.invoke('renderer:get-system-proxy-variables')
+ .then((variables) => {
+ dispatch(updateSystemProxyVariables(variables));
+ return variables;
+ })
+ .then(resolve).catch(reject);
+ });
+};
+
export default appSlice.reducer;
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index 5fb8de3a7..f5dc104ee 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -14,7 +14,8 @@ const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('../utils/axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
-const { shouldUseProxy, PatchedHttpsProxyAgent, getSystemProxyEnvVariables } = require('../utils/proxy-util');
+const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
+const { getSystemProxy } = require('@usebruno/requests');
const path = require('path');
const { parseDataFromResponse } = require('../utils/common');
const { getCookieStringForUrl, saveCookies } = require('../utils/cookies');
@@ -302,7 +303,8 @@ const runSingleRequest = async function (
proxyMode = 'on';
} else if (!collectionProxyDisabled && collectionProxyInherit) {
// Inherit from system proxy
- const { http_proxy, https_proxy } = getSystemProxyEnvVariables();
+ const systemProxy = await getSystemProxy();
+ const { http_proxy, https_proxy } = systemProxy;
if (http_proxy?.length || https_proxy?.length) {
proxyMode = 'system';
}
@@ -347,33 +349,40 @@ const runSingleRequest = async function (
});
}
} else if (proxyMode === 'system') {
- const { http_proxy, https_proxy, no_proxy } = getSystemProxyEnvVariables();
- const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || '');
- if (shouldUseSystemProxy) {
- try {
- if (http_proxy?.length) {
- new URL(http_proxy);
- request.httpAgent = new HttpProxyAgent(http_proxy);
+ try {
+ const systemProxy = await getSystemProxy();
+ const { http_proxy, https_proxy, no_proxy } = systemProxy;
+ const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || '');
+ const parsedUrl = new URL(request.url);
+ const isHttpsRequest = parsedUrl.protocol === 'https:';
+ if (shouldUseSystemProxy) {
+ try {
+ if (http_proxy?.length && !isHttpsRequest) {
+ new URL(http_proxy);
+ request.httpAgent = new HttpProxyAgent(http_proxy);
+ }
+ } catch (error) {
+ throw new Error('Invalid system http_proxy');
}
- } catch (error) {
- throw new Error('Invalid system http_proxy');
- }
- try {
- if (https_proxy?.length) {
- new URL(https_proxy);
- request.httpsAgent = new PatchedHttpsProxyAgent(
- https_proxy,
- Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
- );
- } else {
- request.httpsAgent = new https.Agent({
- ...httpsAgentRequestFields
- });
+ try {
+ if (https_proxy?.length && isHttpsRequest) {
+ new URL(https_proxy);
+ request.httpsAgent = new PatchedHttpsProxyAgent(https_proxy,
+ Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined);
+ } else {
+ request.httpsAgent = new https.Agent({
+ ...httpsAgentRequestFields
+ });
+ }
+ } catch (error) {
+ throw new Error('Invalid system https_proxy');
}
- } catch (error) {
- throw new Error('Invalid system https_proxy');
+ } else {
+ request.httpsAgent = new https.Agent({
+ ...httpsAgentRequestFields
+ });
}
- } else {
+ } catch (error) {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
@@ -488,6 +497,7 @@ const runSingleRequest = async function (
const proxyConfig = get(brunoConfig, 'proxy');
const interpolatedClientCertificates = clientCertificates ? interpolateObject(clientCertificates, oauth2InterpolationOptions) : undefined;
const interpolatedProxyConfig = proxyConfig ? interpolateObject(proxyConfig, oauth2InterpolationOptions) : undefined;
+ const systemProxyConfig = await getSystemProxy();
const { httpAgent: oauth2HttpAgent, httpsAgent: oauth2HttpsAgent } = await getHttpHttpsAgents({
requestUrl: oauth2RequestUrl,
@@ -495,7 +505,7 @@ const runSingleRequest = async function (
options: tlsOptions,
clientCertificates: interpolatedClientCertificates,
collectionLevelProxy: interpolatedProxyConfig,
- systemProxyConfig: getSystemProxyEnvVariables()
+ systemProxyConfig
});
const oauth2AxiosInstance = makeAxiosInstanceForOauth2({
diff --git a/packages/bruno-cli/src/utils/proxy-util.js b/packages/bruno-cli/src/utils/proxy-util.js
index 3d3be5045..729e03356 100644
--- a/packages/bruno-cli/src/utils/proxy-util.js
+++ b/packages/bruno-cli/src/utils/proxy-util.js
@@ -79,17 +79,7 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent {
}
}
-const getSystemProxyEnvVariables = () => {
- const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY } = process.env;
- return {
- http_proxy: http_proxy || HTTP_PROXY,
- https_proxy: https_proxy || HTTPS_PROXY,
- no_proxy: no_proxy || NO_PROXY
- };
-};
-
module.exports = {
shouldUseProxy,
- PatchedHttpsProxyAgent,
- getSystemProxyEnvVariables
+ PatchedHttpsProxyAgent
};
diff --git a/packages/bruno-electron/src/ipc/network/cert-utils.js b/packages/bruno-electron/src/ipc/network/cert-utils.js
index e3cea3fb9..def250f14 100644
--- a/packages/bruno-electron/src/ipc/network/cert-utils.js
+++ b/packages/bruno-electron/src/ipc/network/cert-utils.js
@@ -1,7 +1,7 @@
const fs = require('node:fs');
const path = require('path');
const { get } = require('lodash');
-const { getCACertificates } = require('@usebruno/requests');
+const { getCACertificates, getSystemProxy } = require('@usebruno/requests');
const { preferencesUtil } = require('../../store/preferences');
const { getBrunoConfig } = require('../../store/bruno-config');
const { interpolateString } = require('./interpolate-string');
@@ -144,6 +144,8 @@ const getCertsAndProxyConfig = async ({
} else if (!globalDisabled && globalInherit) {
// Use system proxy
proxyMode = 'system';
+ const systemProxyConfig = await getSystemProxy();
+ proxyConfig = systemProxyConfig;
}
// else: global proxy is disabled, proxyMode stays 'off'
}
diff --git a/packages/bruno-electron/src/ipc/preferences.js b/packages/bruno-electron/src/ipc/preferences.js
index 4d8f7dea1..607eec6ad 100644
--- a/packages/bruno-electron/src/ipc/preferences.js
+++ b/packages/bruno-electron/src/ipc/preferences.js
@@ -1,18 +1,14 @@
const { ipcMain, nativeTheme } = require('electron');
-const { getPreferences, savePreferences, preferencesUtil } = require('../store/preferences');
+const { getPreferences, savePreferences } = require('../store/preferences');
const { globalEnvironmentsStore } = require('../store/global-environments');
+const { getSystemProxy } = require('@usebruno/requests');
-const registerPreferencesIpc = (mainWindow, watcher) => {
+const registerPreferencesIpc = (mainWindow) => {
ipcMain.handle('renderer:ready', async (event) => {
// load preferences
const preferences = getPreferences();
mainWindow.webContents.send('main:load-preferences', preferences);
- // load system proxy vars
- const systemProxyVars = preferencesUtil.getSystemProxyEnvVariables();
- const { http_proxy, https_proxy, no_proxy } = systemProxyVars || {};
- mainWindow.webContents.send('main:load-system-proxy-env', { http_proxy, https_proxy, no_proxy });
-
try {
// load global environments
const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();
@@ -42,6 +38,11 @@ const registerPreferencesIpc = (mainWindow, watcher) => {
ipcMain.on('renderer:theme-change', (event, theme) => {
nativeTheme.themeSource = theme;
});
+
+ ipcMain.handle('renderer:get-system-proxy-variables', async () => {
+ const systemProxyConfig = await getSystemProxy();
+ return systemProxyConfig;
+ });
};
module.exports = registerPreferencesIpc;
diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js
index b8a815f32..1fc04b5f3 100644
--- a/packages/bruno-electron/src/store/preferences.js
+++ b/packages/bruno-electron/src/store/preferences.js
@@ -124,9 +124,27 @@ class PreferencesStore {
getPreferences() {
let preferences = this.store.get('preferences', {});
- // Migrate proxy configuration from old formats to new format
- const proxyMigrated = get(preferences, '_migrations.proxyConfigFormat', false);
- if (!proxyMigrated && preferences?.proxy) {
+ // Handle existing users without proxy settings
+ // They should get disabled proxy by default, not inherit from system
+ // New users (empty preferences) will get defaultPreferences.proxy via merge
+ if (Object.keys(preferences).length > 0 && !preferences.proxy) {
+ preferences.proxy = {
+ inherit: false,
+ disabled: true,
+ config: {
+ protocol: 'http',
+ hostname: '',
+ port: null,
+ auth: {
+ username: '',
+ password: ''
+ },
+ bypassProxy: ''
+ }
+ };
+ }
+
+ if (preferences?.proxy) {
const proxy = preferences.proxy || {};
// Check if this is an old format that needs migration
@@ -181,15 +199,6 @@ class PreferencesStore {
}
preferences.proxy = newProxy;
-
- // Mark migration as complete // ?
- // if (!preferences._migrations) {
- // preferences._migrations = {};
- // }
- // preferences._migrations.proxyConfigFormat = true;
-
- // Save the migrated preferences back to the store
- // this.store.set('preferences', preferences);
}
}
@@ -260,7 +269,7 @@ const preferencesUtil = {
return get(getPreferences(), 'request.timeout', 0);
},
getGlobalProxyConfig: () => {
- return get(getPreferences(), 'proxy', {});
+ return get(getPreferences(), 'proxy', defaultPreferences.proxy);
},
shouldStoreCookies: () => {
return get(getPreferences(), 'request.storeCookies', true);
@@ -274,14 +283,6 @@ const preferencesUtil = {
getResponsePaneOrientation: () => {
return get(getPreferences(), 'layout.responsePaneOrientation', 'horizontal');
},
- getSystemProxyEnvVariables: () => {
- const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY } = process.env;
- return {
- http_proxy: http_proxy || HTTP_PROXY,
- https_proxy: https_proxy || HTTPS_PROXY,
- no_proxy: no_proxy || NO_PROXY
- };
- },
isBetaFeatureEnabled: (featureName) => {
return get(getPreferences(), `beta.${featureName}`, false);
},
diff --git a/packages/bruno-electron/src/utils/proxy-util.js b/packages/bruno-electron/src/utils/proxy-util.js
index 06e5c90d2..5730968ac 100644
--- a/packages/bruno-electron/src/utils/proxy-util.js
+++ b/packages/bruno-electron/src/utils/proxy-util.js
@@ -4,7 +4,6 @@ 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 { preferencesUtil } = require('../store/preferences');
const { isEmpty, get, isUndefined, isNull } = require('lodash');
const DEFAULT_PORTS = {
@@ -84,6 +83,29 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent {
}
}
+function createTimelineHttpAgentClass(BaseAgentClass) {
+ return class extends BaseAgentClass {
+ constructor(options, timeline) {
+ // For proxy agents, the first argument is the proxy URI and the second is options
+ const { proxy: proxyUri, httpProxyAgentOptions } = options || {};
+
+ if (!proxyUri) {
+ throw new Error('TimelineHttpProxyAgent requires options.proxy to be set');
+ }
+
+ super(proxyUri, httpProxyAgentOptions);
+
+ this.timeline = Array.isArray(timeline) ? timeline : [];
+ // Log the proxy details
+ this.timeline.push({
+ timestamp: new Date(),
+ type: 'info',
+ message: `Using proxy: ${proxyUri}`
+ });
+ }
+ };
+}
+
function createTimelineAgentClass(BaseAgentClass) {
return class extends BaseAgentClass {
constructor(options, timeline) {
@@ -312,6 +334,10 @@ function setupProxyAgents({
rejectUnauthorized: httpsAgentRequestFields.rejectUnauthorized !== undefined ? httpsAgentRequestFields.rejectUnauthorized : true
};
+ const httpProxyAgentOptions = {
+ keepAlive: true
+ };
+
if (proxyMode === 'on') {
const shouldProxy = shouldUseProxy(requestConfig.url, get(proxyConfig, 'bypassProxy', ''));
if (shouldProxy) {
@@ -337,8 +363,9 @@ function setupProxyAgents({
requestConfig.httpAgent = new TimelineSocksProxyAgent({ proxy: proxyUri }, timeline);
requestConfig.httpsAgent = new TimelineSocksProxyAgent({ proxy: proxyUri, ...tlsOptions }, timeline);
} else {
+ const TimelineHttpProxyAgent = createTimelineHttpAgentClass(HttpProxyAgent);
const TimelineHttpsProxyAgent = createTimelineAgentClass(PatchedHttpsProxyAgent);
- requestConfig.httpAgent = new HttpProxyAgent(proxyUri); // For http, no need for timeline
+ requestConfig.httpAgent = new TimelineHttpProxyAgent({ proxy: proxyUri, httpProxyAgentOptions }, timeline);
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
{ proxy: proxyUri, ...tlsOptions },
timeline
@@ -350,19 +377,22 @@ function setupProxyAgents({
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
}
} else if (proxyMode === 'system') {
- const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables();
+ const { http_proxy, https_proxy, no_proxy } = proxyConfig || {};
const shouldUseSystemProxy = shouldUseProxy(requestConfig.url, no_proxy || '');
+ const parsedUrl = parseUrl(requestConfig.url);
+ const isHttpsRequest = parsedUrl.protocol === 'https:';
if (shouldUseSystemProxy) {
try {
- if (http_proxy?.length) {
+ if (http_proxy?.length && !isHttpsRequest) {
new URL(http_proxy);
- requestConfig.httpAgent = new HttpProxyAgent(http_proxy);
+ const TimelineHttpProxyAgent = createTimelineHttpAgentClass(HttpProxyAgent);
+ requestConfig.httpAgent = new TimelineHttpProxyAgent({ proxy: http_proxy, httpProxyAgentOptions }, timeline);
}
} catch (error) {
- throw new Error('Invalid system http_proxy');
+ throw new Error(`Invalid system http_proxy "${http_proxy}": ${error.message}`);
}
try {
- if (https_proxy?.length) {
+ if (https_proxy?.length && isHttpsRequest) {
new URL(https_proxy);
const TimelineHttpsProxyAgent = createTimelineAgentClass(PatchedHttpsProxyAgent);
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
@@ -374,7 +404,7 @@ function setupProxyAgents({
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
}
} catch (error) {
- throw new Error('Invalid system https_proxy');
+ throw new Error(`Invalid system https_proxy "${https_proxy}": ${error.message}`);
}
} else {
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
diff --git a/packages/bruno-electron/tests/store/proxy-preferences.spec.js b/packages/bruno-electron/tests/store/proxy-preferences.spec.js
index cc202c9b3..927ce2032 100644
--- a/packages/bruno-electron/tests/store/proxy-preferences.spec.js
+++ b/packages/bruno-electron/tests/store/proxy-preferences.spec.js
@@ -19,6 +19,49 @@ describe('Proxy Preferences Migration', () => {
mockStoreData = {};
});
+ describe('Default Proxy Settings', () => {
+ it('should default to inherit: true for new users (empty preferences)', () => {
+ // New user - no preferences.json exists, store returns empty object
+ mockStoreData['preferences'] = {};
+
+ const preferences = getPreferences();
+
+ // New users get the default proxy settings with inherit: true
+ expect(preferences.proxy.inherit).toBe(true);
+ expect(preferences.proxy.disabled).toBeUndefined();
+ expect(preferences.proxy.config).toBeDefined();
+ expect(preferences.proxy.config.protocol).toBe('http');
+ expect(preferences.proxy.config.hostname).toBe('');
+ expect(preferences.proxy.config.port).toBeNull();
+ });
+
+ it('should default to disabled: true, inherit: false for existing users without proxy settings', () => {
+ // Existing user - has preferences but no proxy property
+ mockStoreData['preferences'] = {
+ request: {
+ sslVerification: true
+ },
+ font: {
+ codeFont: 'default',
+ codeFontSize: 13
+ }
+ };
+
+ const preferences = getPreferences();
+
+ // Existing users without proxy get disabled proxy by default
+ expect(preferences.proxy.disabled).toBe(true);
+ expect(preferences.proxy.inherit).toBe(false);
+ expect(preferences.proxy.config).toBeDefined();
+ expect(preferences.proxy.config.protocol).toBe('http');
+ expect(preferences.proxy.config.hostname).toBe('');
+ expect(preferences.proxy.config.port).toBeNull();
+ expect(preferences.proxy.config.auth.username).toBe('');
+ expect(preferences.proxy.config.auth.password).toBe('');
+ expect(preferences.proxy.config.bypassProxy).toBe('');
+ });
+ });
+
describe('New Format (no migration needed)', () => {
it('should handle new format with inherit: false', () => {
const newFormatProxy = {
diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts
index 1da7b035a..51f201545 100644
--- a/packages/bruno-requests/src/index.ts
+++ b/packages/bruno-requests/src/index.ts
@@ -11,4 +11,4 @@ export { getHttpHttpsAgents } from './utils/http-https-agents';
export * as scripting from './scripting';
-export { makeAxiosInstance } from './network/axios-instance';
+export { makeAxiosInstance, getSystemProxy } from './network';
diff --git a/packages/bruno-requests/src/network/index.ts b/packages/bruno-requests/src/network/index.ts
index 19548a4e5..374bef735 100644
--- a/packages/bruno-requests/src/network/index.ts
+++ b/packages/bruno-requests/src/network/index.ts
@@ -1 +1,3 @@
export { makeAxiosInstance } from './axios-instance';
+
+export { getSystemProxy } from './system-proxy';
diff --git a/packages/bruno-requests/src/network/system-proxy/index.spec.js b/packages/bruno-requests/src/network/system-proxy/index.spec.js
new file mode 100644
index 000000000..2438d566b
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/index.spec.js
@@ -0,0 +1,270 @@
+const { getSystemProxy, SystemProxyResolver } = require('./index');
+const os = require('node:os');
+
+// Mock dependencies
+jest.mock('node:os');
+jest.mock('./utils/windows');
+jest.mock('./utils/macos');
+jest.mock('./utils/linux');
+
+describe('SystemProxyResolver Integration', () => {
+ let detector;
+ let originalEnv;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ detector = new SystemProxyResolver();
+ originalEnv = { ...process.env };
+
+ // Clear environment variables
+ delete process.env.http_proxy;
+ delete process.env.HTTP_PROXY;
+ delete process.env.https_proxy;
+ delete process.env.HTTPS_PROXY;
+ delete process.env.no_proxy;
+ delete process.env.NO_PROXY;
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ jest.runOnlyPendingTimers();
+ // Clear any pending timers to prevent Jest open handles
+ jest.clearAllTimers();
+ jest.useRealTimers();
+ });
+
+ describe('Environment Variables', () => {
+ it('should prioritize lowercase over uppercase variables', () => {
+ process.env.http_proxy = 'http://proxy.usebruno.com:8080';
+ process.env.HTTP_PROXY = 'http://proxy.usebruno.com:8081';
+ process.env.https_proxy = 'https://proxy.usebruno.com:8082';
+ process.env.HTTPS_PROXY = 'https://proxy.usebruno.com:8083';
+
+ const result = detector.getEnvironmentVariables();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://proxy.usebruno.com:8082',
+ no_proxy: null,
+ source: 'environment'
+ });
+ });
+
+ it('should fall back to uppercase when lowercase is not set', () => {
+ process.env.HTTP_PROXY = 'http://proxy.usebruno.com:8081';
+ process.env.NO_PROXY = 'localhost,127.0.0.1';
+
+ const result = detector.getEnvironmentVariables();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8081',
+ https_proxy: null,
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'environment'
+ });
+ });
+
+ it('should return null values when no environment variables are set', () => {
+ const result = detector.getEnvironmentVariables();
+
+ expect(result).toEqual({
+ http_proxy: null,
+ https_proxy: null,
+ no_proxy: null,
+ source: 'environment'
+ });
+ });
+ });
+
+ describe('Platform Routing', () => {
+ it('should route to Windows detector on win32', async () => {
+ // Create a new detector instance after mocking the platform
+ os.platform.mockReturnValue('win32');
+ const testResolver = new SystemProxyResolver();
+ const { WindowsProxyResolver } = require('./utils/windows');
+ const mockDetect = jest.fn().mockResolvedValue({ source: 'windows-system' });
+ WindowsProxyResolver.mockImplementation(() => ({ detect: mockDetect }));
+
+ await testResolver.getSystemProxy();
+ expect(mockDetect).toHaveBeenCalled();
+ });
+
+ it('should route to macOS detector on darwin', async () => {
+ // Create a new detector instance after mocking the platform
+ os.platform.mockReturnValue('darwin');
+ const testResolver = new SystemProxyResolver();
+ const { MacOSProxyResolver } = require('./utils/macos');
+ const mockDetect = jest.fn().mockResolvedValue({ source: 'macos-system' });
+ MacOSProxyResolver.mockImplementation(() => ({ detect: mockDetect }));
+
+ await testResolver.getSystemProxy();
+ expect(mockDetect).toHaveBeenCalled();
+ });
+
+ it('should route to Linux detector on linux', async () => {
+ // Create a new detector instance after mocking the platform
+ os.platform.mockReturnValue('linux');
+ const testResolver = new SystemProxyResolver();
+ const { LinuxProxyResolver } = require('./utils/linux');
+ const mockDetect = jest.fn().mockResolvedValue({ source: 'linux-system' });
+ LinuxProxyResolver.mockImplementation(() => ({ detect: mockDetect }));
+
+ await testResolver.getSystemProxy();
+ expect(mockDetect).toHaveBeenCalled();
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should throw error when platform detection fails', async () => {
+ os.platform.mockReturnValue('win32');
+ const testResolver = new SystemProxyResolver();
+ const { WindowsProxyResolver } = require('./utils/windows');
+ WindowsProxyResolver.mockImplementation(() => ({
+ detect: jest.fn().mockRejectedValue(new Error('Detection failed'))
+ }));
+
+ await expect(testResolver.getSystemProxy()).rejects.toThrow('Detection failed');
+ });
+
+ it('should throw error on timeout', async () => {
+ os.platform.mockReturnValue('win32');
+ const testResolver = new SystemProxyResolver({ commandTimeoutMs: 100 });
+ const { WindowsProxyResolver } = require('./utils/windows');
+
+ // Mock a detector that throws a timeout error
+ WindowsProxyResolver.mockImplementation(() => ({
+ detect: jest.fn().mockRejectedValue(new Error('System proxy detection timeout'))
+ }));
+
+ await expect(testResolver.getSystemProxy()).rejects.toThrow('System proxy detection timeout');
+ });
+
+ it('should throw error for unsupported platform', async () => {
+ os.platform.mockReturnValue('freebsd');
+ const testResolver = new SystemProxyResolver();
+
+ await expect(testResolver.getSystemProxy()).rejects.toThrow('Unsupported platform: freebsd');
+ });
+ });
+
+ describe('getSystemProxy function', () => {
+ beforeEach(() => {
+ // Reset modules to ensure fresh imports for each test
+ jest.resetModules();
+ });
+
+ it('should merge environment variables with system proxy', async () => {
+ // Mock os.platform before requiring the module
+ jest.doMock('node:os', () => ({
+ platform: jest.fn().mockReturnValue('win32')
+ }));
+
+ const { WindowsProxyResolver } = require('./utils/windows');
+ WindowsProxyResolver.mockImplementation(() => ({
+ detect: jest.fn().mockResolvedValue({
+ http_proxy: 'http://system-proxy.usebruno.com:8080',
+ https_proxy: 'https://system-proxy.usebruno.com:8443',
+ no_proxy: 'localhost',
+ source: 'windows-system'
+ })
+ }));
+
+ process.env.http_proxy = 'http://env-proxy.usebruno.com:9090';
+
+ // Require the module after mocking
+ const { getSystemProxy: getSystemProxyFresh } = require('./index');
+ const result = await getSystemProxyFresh();
+
+ // Environment variables take priority
+ expect(result).toEqual({
+ http_proxy: 'http://env-proxy.usebruno.com:9090',
+ https_proxy: 'https://system-proxy.usebruno.com:8443',
+ no_proxy: 'localhost',
+ source: 'windows-system + environment'
+ });
+ });
+
+ it('should return only system proxy when no environment variables are set', async () => {
+ // Mock os.platform before requiring the module
+ jest.doMock('node:os', () => ({
+ platform: jest.fn().mockReturnValue('darwin')
+ }));
+
+ const { MacOSProxyResolver } = require('./utils/macos');
+ MacOSProxyResolver.mockImplementation(() => ({
+ detect: jest.fn().mockResolvedValue({
+ http_proxy: 'http://system-proxy.usebruno.com:8080',
+ https_proxy: 'https://system-proxy.usebruno.com:8443',
+ no_proxy: 'localhost',
+ source: 'macos-system'
+ })
+ }));
+
+ // Require the module after mocking
+ const { getSystemProxy: getSystemProxyFresh } = require('./index');
+ const result = await getSystemProxyFresh();
+
+ expect(result).toEqual({
+ http_proxy: 'http://system-proxy.usebruno.com:8080',
+ https_proxy: 'https://system-proxy.usebruno.com:8443',
+ no_proxy: 'localhost',
+ source: 'macos-system'
+ });
+ });
+
+ it('should fallback to environment variables when system detection fails', async () => {
+ // Mock os.platform before requiring the module
+ jest.doMock('node:os', () => ({
+ platform: jest.fn().mockReturnValue('linux')
+ }));
+
+ const { LinuxProxyResolver } = require('./utils/linux');
+ LinuxProxyResolver.mockImplementation(() => ({
+ detect: jest.fn().mockRejectedValue(new Error('Detection failed'))
+ }));
+
+ process.env.http_proxy = 'http://proxy.usebruno.com:8080';
+ process.env.https_proxy = 'https://proxy.usebruno.com:8443';
+
+ // Require the module after mocking
+ const { getSystemProxy: getSystemProxyFresh } = require('./index');
+ const result = await getSystemProxyFresh();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://proxy.usebruno.com:8443',
+ no_proxy: null,
+ source: 'environment'
+ });
+ });
+
+ it('should return null values when no proxy is configured', async () => {
+ // Mock os.platform before requiring the module
+ jest.doMock('node:os', () => ({
+ platform: jest.fn().mockReturnValue('darwin')
+ }));
+
+ const { MacOSProxyResolver } = require('./utils/macos');
+ MacOSProxyResolver.mockImplementation(() => ({
+ detect: jest.fn().mockResolvedValue({
+ http_proxy: null,
+ https_proxy: null,
+ no_proxy: null,
+ source: 'macos-system'
+ })
+ }));
+
+ // Require the module after mocking
+ const { getSystemProxy: getSystemProxyFresh } = require('./index');
+ const result = await getSystemProxyFresh();
+
+ expect(result).toEqual({
+ http_proxy: null,
+ https_proxy: null,
+ no_proxy: null,
+ source: 'macos-system'
+ });
+ });
+ });
+});
diff --git a/packages/bruno-requests/src/network/system-proxy/index.ts b/packages/bruno-requests/src/network/system-proxy/index.ts
new file mode 100644
index 000000000..07f9861cc
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/index.ts
@@ -0,0 +1,107 @@
+import { platform } from 'node:os';
+import { ProxyConfiguration, SystemProxyResolverOptions } from './types';
+import { WindowsProxyResolver } from './utils/windows';
+import { MacOSProxyResolver } from './utils/macos';
+import { LinuxProxyResolver } from './utils/linux';
+import { normalizeNoProxy, normalizeProxyUrl } from './utils/common';
+
+export class SystemProxyResolver {
+ private osPlatform: string;
+ private commandTimeoutMs: number = 10000;
+ private activeDetection: Promise | null = null;
+
+ constructor(options: SystemProxyResolverOptions = {}) {
+ this.osPlatform = platform();
+ if (options.commandTimeoutMs) {
+ this.commandTimeoutMs = options.commandTimeoutMs;
+ }
+ }
+
+ async getSystemProxy(): Promise {
+ // Return active detection if already in progress
+ if (this.activeDetection) {
+ return this.activeDetection;
+ }
+
+ // Start new detection
+ this.activeDetection = this.detectSystemProxy();
+
+ try {
+ const result = await this.activeDetection;
+ return result;
+ } finally {
+ this.activeDetection = null;
+ }
+ }
+
+ private async detectSystemProxy(): Promise {
+ const startTime = Date.now();
+
+ try {
+ const result = await this.detectByPlatform();
+
+ // Log slow detections
+ const detectionTime = Date.now() - startTime;
+ if (detectionTime > 5000) {
+ console.warn(`System proxy detection took ${detectionTime}ms`);
+ }
+
+ return result;
+ } catch (error) {
+ console.warn(`System proxy detection failed after ${Date.now() - startTime}ms:`, error instanceof Error ? error.message : String(error));
+ throw error;
+ }
+ }
+
+ private async detectByPlatform(): Promise {
+ switch (this.osPlatform) {
+ case 'win32':
+ return await new WindowsProxyResolver().detect({ timeoutMs: this.commandTimeoutMs });
+ case 'darwin':
+ return await new MacOSProxyResolver().detect({ timeoutMs: this.commandTimeoutMs });
+ case 'linux':
+ return await new LinuxProxyResolver().detect({ timeoutMs: this.commandTimeoutMs });
+ default:
+ throw new Error(`Unsupported platform: ${this.osPlatform}`);
+ }
+ }
+
+ getEnvironmentVariables(): ProxyConfiguration {
+ const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY, all_proxy, ALL_PROXY } = process.env;
+
+ const httpProxy = http_proxy || HTTP_PROXY || all_proxy || ALL_PROXY || '';
+ const httpsProxy = https_proxy || HTTPS_PROXY || all_proxy || ALL_PROXY || '';
+ const noProxy = no_proxy || NO_PROXY || '';
+
+ return {
+ http_proxy: httpProxy ? normalizeProxyUrl(httpProxy) : null,
+ https_proxy: httpsProxy ? normalizeProxyUrl(httpsProxy, 'https') : null,
+ no_proxy: noProxy ? normalizeNoProxy(noProxy) : null,
+ source: 'environment'
+ };
+ }
+}
+
+const systemProxyResolver = new SystemProxyResolver();
+
+export async function getSystemProxy(): Promise {
+ const proxyEnvironmentVariables = systemProxyResolver.getEnvironmentVariables();
+
+ const hasEnvironmentProxy = proxyEnvironmentVariables.http_proxy || proxyEnvironmentVariables.https_proxy;
+
+ try {
+ const systemProxyEnvironmentVariables = await systemProxyResolver.getSystemProxy();
+
+ return {
+ http_proxy: proxyEnvironmentVariables?.http_proxy || systemProxyEnvironmentVariables?.http_proxy,
+ https_proxy: proxyEnvironmentVariables?.https_proxy || systemProxyEnvironmentVariables?.https_proxy,
+ no_proxy: proxyEnvironmentVariables?.no_proxy || systemProxyEnvironmentVariables?.no_proxy,
+ source: hasEnvironmentProxy ? `${systemProxyEnvironmentVariables?.source} + environment` : systemProxyEnvironmentVariables?.source
+ };
+ } catch (error) {
+ console.error('Error getting system proxy:', error);
+ return proxyEnvironmentVariables;
+ }
+}
+
+export { ProxyConfiguration } from './types';
diff --git a/packages/bruno-requests/src/network/system-proxy/types.ts b/packages/bruno-requests/src/network/system-proxy/types.ts
new file mode 100644
index 000000000..a438691ae
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/types.ts
@@ -0,0 +1,14 @@
+export interface ProxyConfiguration {
+ http_proxy?: string | null;
+ https_proxy?: string | null;
+ no_proxy?: string | null;
+ source: string;
+};
+
+export interface ProxyResolver {
+ detect(opts?: { timeoutMs?: number }): Promise;
+}
+
+export interface SystemProxyResolverOptions {
+ commandTimeoutMs?: number;
+}
diff --git a/packages/bruno-requests/src/network/system-proxy/utils/common.spec.ts b/packages/bruno-requests/src/network/system-proxy/utils/common.spec.ts
new file mode 100644
index 000000000..a122ae4dd
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/utils/common.spec.ts
@@ -0,0 +1,64 @@
+import { normalizeProxyUrl, normalizeNoProxy } from './common';
+
+describe('normalizeProxyUrl', () => {
+ it('should add http protocol when missing', () => {
+ expect(normalizeProxyUrl('proxy.usebruno.com:8080')).toBe('http://proxy.usebruno.com:8080');
+ });
+
+ it('should not modify URL with existing protocol', () => {
+ expect(normalizeProxyUrl('http://proxy.usebruno.com:8080')).toBe('http://proxy.usebruno.com:8080');
+ expect(normalizeProxyUrl('https://proxy.usebruno.com:8443')).toBe('https://proxy.usebruno.com:8443');
+ });
+
+ it('should handle empty string', () => {
+ expect(normalizeProxyUrl('')).toBe('');
+ });
+
+ it('should handle various protocols', () => {
+ expect(normalizeProxyUrl('socks5://proxy.usebruno.com:1080')).toBe('socks5://proxy.usebruno.com:1080');
+ expect(normalizeProxyUrl('socks4://proxy.usebruno.com:1080')).toBe('socks4://proxy.usebruno.com:1080');
+ });
+
+ it('should handle URLs without port', () => {
+ expect(normalizeProxyUrl('proxy.usebruno.com')).toBe('http://proxy.usebruno.com');
+ });
+});
+
+describe('normalizeNoProxy', () => {
+ it('should normalize comma-separated list', () => {
+ expect(normalizeNoProxy('localhost,127.0.0.1')).toBe('localhost,127.0.0.1');
+ });
+
+ it('should convert semicolons to commas', () => {
+ expect(normalizeNoProxy('localhost;127.0.0.1')).toBe('localhost,127.0.0.1');
+ });
+
+ it('should handle mixed delimiters', () => {
+ expect(normalizeNoProxy('localhost;127.0.0.1,*.local')).toBe('localhost,127.0.0.1,*.local');
+ });
+
+ it('should trim whitespace', () => {
+ expect(normalizeNoProxy('localhost , 127.0.0.1 ; *.local')).toBe('localhost,127.0.0.1,*.local');
+ });
+
+ it('should remove empty entries', () => {
+ expect(normalizeNoProxy('localhost,,127.0.0.1')).toBe('localhost,127.0.0.1');
+ expect(normalizeNoProxy('localhost; ;127.0.0.1')).toBe('localhost,127.0.0.1');
+ });
+
+ it('should handle null input', () => {
+ expect(normalizeNoProxy(null)).toBeNull();
+ });
+
+ it('should handle empty string', () => {
+ expect(normalizeNoProxy('')).toBeNull();
+ });
+
+ it('should handle whitespace-only string', () => {
+ expect(normalizeNoProxy(' ')).toBeNull();
+ });
+
+ it('should handle complex patterns', () => {
+ expect(normalizeNoProxy('localhost;127.0.0.1;*.local;192.168.1.0/24;')).toBe('localhost,127.0.0.1,*.local,192.168.1.0/24,');
+ });
+});
diff --git a/packages/bruno-requests/src/network/system-proxy/utils/common.ts b/packages/bruno-requests/src/network/system-proxy/utils/common.ts
new file mode 100644
index 000000000..68594cc84
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/utils/common.ts
@@ -0,0 +1,63 @@
+import { execFile, ExecFileOptions } from 'node:child_process';
+import { promisify } from 'node:util';
+
+export const execFileAsync = promisify(execFile);
+
+/**
+ * Safely execute a command without shell interpretation.
+ * Returns stdout on success, null on error.
+ *
+ * @param bin - The binary/command to execute
+ * @param args - Array of arguments to pass to the command
+ * @param opts - ExecFileOptions (timeout, maxBuffer, etc.)
+ * @returns stdout trimmed on success, null on error
+ */
+export async function safeExec(bin: string, args: string[], opts: ExecFileOptions): Promise {
+ try {
+ const { stdout } = await execFileAsync(bin, args, opts);
+ return stdout.trim();
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Normalizes a proxy URL by ensuring it includes a protocol.
+ * @param proxy - The proxy URL to normalize (e.g., "proxy.usebruno.com:8080").
+ * @param defaultProtocol - The protocol to prepend if missing (default: "http").
+ * @returns The normalized proxy URL (e.g., "http://proxy.usebruno.com:8080").
+ *
+ * Notes:
+ * - If the URL already includes a protocol (e.g., "https://..."), it is returned unchanged.
+ * - When system proxy settings omit the protocol,
+ * this function cannot infer the original protocol and will apply the default ("http").
+ */
+
+export const normalizeProxyUrl = (proxy: string, defaultProtocol: string = 'http'): string => {
+ if (!proxy) return proxy;
+
+ // Check if proxy already has a protocol (must have :// after protocol name)
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(proxy)) {
+ return proxy;
+ }
+
+ // Add default protocol
+ return `${defaultProtocol}://${proxy}`;
+};
+
+/**
+ * Normalizes no_proxy list to comma-separated format
+ * @param noProxy - The no_proxy string (e.g., "localhost;127.0.0.1")
+ * @returns Normalized comma-separated no_proxy list (e.g., "localhost,127.0.0.1")
+ */
+export const normalizeNoProxy = (noProxy: string | null): string | null => {
+ if (!noProxy) return null;
+
+ const normalized = noProxy
+ .split(/[;,\s]+/)
+ .map((s) => s.trim())
+ .filter((s) => s.length > 0)
+ .join(',');
+
+ return normalized || null;
+};
diff --git a/packages/bruno-requests/src/network/system-proxy/utils/linux.spec.ts b/packages/bruno-requests/src/network/system-proxy/utils/linux.spec.ts
new file mode 100644
index 000000000..f2d2bcb53
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/utils/linux.spec.ts
@@ -0,0 +1,223 @@
+import { LinuxProxyResolver } from './linux';
+
+// Mock the entire child_process module
+jest.mock('node:child_process', () => ({
+ execFile: jest.fn()
+}));
+
+// Mock the fs/promises module
+jest.mock('node:fs/promises', () => ({
+ readFile: jest.fn()
+}));
+
+// Mock the fs module
+jest.mock('node:fs', () => ({
+ existsSync: jest.fn()
+}));
+
+// Mock the util module
+jest.mock('node:util', () => ({
+ promisify: jest.fn((fn) => fn)
+}));
+
+describe('LinuxProxyResolver', () => {
+ let detector: LinuxProxyResolver;
+ let mockExecFile: jest.MockedFunction;
+ let mockReadFile: jest.MockedFunction;
+ let mockExistsSync: jest.MockedFunction;
+
+ beforeEach(() => {
+ detector = new LinuxProxyResolver();
+ const { execFile } = require('node:child_process');
+ const { readFile } = require('node:fs/promises');
+ const { existsSync } = require('node:fs');
+ mockExecFile = execFile;
+ mockReadFile = readFile;
+ mockExistsSync = existsSync;
+ jest.clearAllMocks();
+ });
+
+ describe('gsettings proxy detection', () => {
+ it('should detect manual proxy configuration', async () => {
+ const modeOutput = '\'manual\'';
+ const httpHostOutput = '\'proxy.usebruno.com\'';
+ const httpPortOutput = '8080';
+ const httpsHostOutput = '\'secure-proxy.usebruno.com\'';
+ const httpsPortOutput = '8443';
+ const ignoreHostsOutput = '[\'localhost\', \'127.0.0.1\']';
+
+ mockExecFile
+ .mockResolvedValueOnce({ stdout: modeOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpHostOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpPortOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpsHostOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpsPortOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: ignoreHostsOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://secure-proxy.usebruno.com:8443',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'linux-system'
+ });
+ });
+
+ it('should detect identical HTTP and HTTPS proxies', async () => {
+ const modeOutput = '\'manual\'';
+ const httpHostOutput = '\'proxy.usebruno.com\'';
+ const httpPortOutput = '8080';
+ const httpsHostOutput = '\'proxy.usebruno.com\'';
+ const httpsPortOutput = '8080';
+ const ignoreHostsOutput = '[]';
+
+ mockExecFile
+ .mockResolvedValueOnce({ stdout: modeOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpHostOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpPortOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpsHostOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpsPortOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: ignoreHostsOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://proxy.usebruno.com:8080',
+ no_proxy: null,
+ source: 'linux-system'
+ });
+ });
+
+ it('should handle non-manual proxy mode', async () => {
+ const modeOutput = '\'auto\'';
+
+ mockExecFile.mockResolvedValueOnce({ stdout: modeOutput, stderr: '' });
+
+ await expect(detector.detect()).rejects.toThrow('Linux proxy detection failed');
+ });
+
+ it('should handle empty ignore hosts list', async () => {
+ const modeOutput = '\'manual\'';
+ const httpHostOutput = '\'proxy.usebruno.com\'';
+ const httpPortOutput = '8080';
+ const httpsHostOutput = '\'\'';
+ const httpsPortOutput = '';
+ const ignoreHostsOutput = '[]';
+
+ mockExecFile
+ .mockResolvedValueOnce({ stdout: modeOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpHostOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpPortOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpsHostOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: httpsPortOutput, stderr: '' })
+ .mockResolvedValueOnce({ stdout: ignoreHostsOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: null,
+ no_proxy: null,
+ source: 'linux-system'
+ });
+ });
+ });
+
+ describe('/etc/environment proxy detection', () => {
+ it('should detect proxy from /etc/environment', async () => {
+ // Mock gsettings to fail
+ mockExecFile.mockImplementation(() => {
+ throw new Error('gsettings not available');
+ });
+
+ // Mock /etc/environment file
+ mockExistsSync.mockReturnValueOnce(true);
+ mockReadFile.mockResolvedValueOnce(`
+http_proxy=http://proxy.usebruno.com:8080
+https_proxy=http://proxy.usebruno.com:8080
+no_proxy=localhost,127.0.0.1
+`);
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'linux-system'
+ });
+ });
+ });
+
+ describe('systemd proxy detection', () => {
+ it('should detect proxy from systemd configuration', async () => {
+ mockExecFile.mockImplementation(() => {
+ throw new Error('gsettings not available');
+ });
+
+ // Mock all previous methods to fail
+ mockExistsSync.mockReturnValue(false);
+
+ // Mock systemd conf directory to exist
+ mockExistsSync.mockReturnValueOnce(true);
+
+ // Mock systemd proxy file to exist
+ mockExistsSync.mockReturnValueOnce(true);
+ mockReadFile.mockResolvedValueOnce(`
+http_proxy=http://proxy.usebruno.com:8080
+https_proxy=http://proxy.usebruno.com:8080
+no_proxy=localhost,127.0.0.1
+`);
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'linux-system'
+ });
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should throw error when gsettings is not available', async () => {
+ const error = new Error('gsettings: command not found');
+
+ mockExecFile.mockImplementation(() => {
+ throw error;
+ });
+
+ // Mock all file-based methods to fail
+ mockExistsSync.mockReturnValue(false);
+
+ await expect(detector.detect()).rejects.toThrow('Linux proxy detection failed');
+ });
+
+ it('should throw error when gsettings schema is not installed', async () => {
+ const error = new Error('No such schema');
+
+ mockExecFile.mockImplementation(() => {
+ throw error;
+ });
+
+ // Mock all file-based methods to fail
+ mockExistsSync.mockReturnValue(false);
+
+ await expect(detector.detect()).rejects.toThrow('Linux proxy detection failed');
+ });
+
+ it('should throw error when no proxy configuration is found', async () => {
+ mockExecFile.mockImplementation(() => {
+ throw new Error('gsettings not available');
+ });
+
+ // Mock all file-based methods to fail
+ mockExistsSync.mockReturnValue(false);
+
+ await expect(detector.detect()).rejects.toThrow('Linux proxy detection failed');
+ });
+ });
+});
diff --git a/packages/bruno-requests/src/network/system-proxy/utils/linux.ts b/packages/bruno-requests/src/network/system-proxy/utils/linux.ts
new file mode 100644
index 000000000..deff4847e
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/utils/linux.ts
@@ -0,0 +1,227 @@
+import { ExecFileOptions } from 'node:child_process';
+import { readFile, readdir } from 'node:fs/promises';
+import { existsSync } from 'node:fs';
+import { ProxyConfiguration, ProxyResolver } from '../types';
+import { normalizeProxyUrl, normalizeNoProxy, safeExec } from './common';
+
+// Pre-compile patterns for proxy variable detection
+const PROXY_VAR_PATTERNS = ['http_proxy', 'https_proxy', 'no_proxy', 'all_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY']
+ .flatMap((varName) => [
+ { varName: varName.toLowerCase(), pattern: new RegExp(`^export\\s+${varName}\\s*=\\s*(.+)$`, 'i') },
+ { varName: varName.toLowerCase(), pattern: new RegExp(`^${varName}\\s*=\\s*(.+)$`, 'i') }
+ ]);
+
+export class LinuxProxyResolver implements ProxyResolver {
+ async detect(opts?: { timeoutMs?: number }): Promise {
+ const timeoutMs = opts?.timeoutMs ?? 10000;
+ const execOpts: ExecFileOptions = {
+ timeout: timeoutMs,
+ maxBuffer: 1024 * 1024
+ };
+
+ try {
+ // Try different proxy detection methods in order of preference
+ const detectionMethods = [
+ () => this.getGSettingsProxy(execOpts),
+ () => this.getKDEProxy(execOpts),
+ () => this.getEnvironmentFileProxy(),
+ () => this.getSystemdProxy()
+ ];
+
+ for (const method of detectionMethods) {
+ try {
+ const proxy = await method();
+ if (proxy) {
+ return proxy;
+ }
+ } catch (error) {
+ // Continue to next method if this one fails
+ continue;
+ }
+ }
+
+ throw new Error('No Linux proxy configuration found');
+ } catch (error) {
+ throw new Error(`Linux proxy detection failed: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ private async getGSettingsProxy(execOpts: ExecFileOptions): Promise {
+ try {
+ const mode = await safeExec('gsettings', ['get', 'org.gnome.system.proxy', 'mode'], execOpts);
+ if (mode !== '\'manual\'') {
+ return null;
+ }
+
+ const httpHost = await safeExec('gsettings', ['get', 'org.gnome.system.proxy.http', 'host'], execOpts);
+ const httpPort = await safeExec('gsettings', ['get', 'org.gnome.system.proxy.http', 'port'], execOpts);
+ const httpsHost = await safeExec('gsettings', ['get', 'org.gnome.system.proxy.https', 'host'], execOpts);
+ const httpsPort = await safeExec('gsettings', ['get', 'org.gnome.system.proxy.https', 'port'], execOpts);
+ const ignoreHosts = await safeExec('gsettings', ['get', 'org.gnome.system.proxy', 'ignore-hosts'], execOpts);
+
+ const cleanHttpHost = (httpHost || '').replace(/'/g, '');
+ const cleanHttpPort = httpPort || '';
+ const cleanHttpsHost = (httpsHost || '').replace(/'/g, '');
+ const cleanHttpsPort = httpsPort || '';
+ const cleanIgnoreHosts = ignoreHosts || '';
+
+ const http_proxy = cleanHttpHost && cleanHttpPort ? normalizeProxyUrl(`${cleanHttpHost}:${cleanHttpPort}`) : null;
+ const https_proxy = cleanHttpsHost && cleanHttpsPort ? normalizeProxyUrl(`${cleanHttpsHost}:${cleanHttpsPort}`, 'https') : null;
+
+ const rawNoProxy = cleanIgnoreHosts !== '[]' ? cleanIgnoreHosts.replace(/[\[\]']/g, '').replace(/,\s*/g, ',') : null;
+
+ return {
+ http_proxy,
+ https_proxy,
+ no_proxy: normalizeNoProxy(rawNoProxy),
+ source: 'linux-system'
+ };
+ } catch (error) {
+ return null;
+ }
+ }
+
+ private async getKDEProxy(execOpts: ExecFileOptions): Promise {
+ try {
+ // Check if kreadconfig5 is available and get proxy type
+ const proxyType = await safeExec('kreadconfig5', ['--group', 'Proxy Settings', '--key', 'ProxyType'], execOpts);
+
+ // ProxyType values:
+ // 0 = No proxy
+ // 1 = Manual proxy configuration
+ // 2 = Automatic proxy configuration via URL
+ // 3 = Automatic proxy detection
+ // 4 = Use system proxy configuration (environment variables)
+
+ if (proxyType !== '1') {
+ // Only handle manual proxy configuration for now
+ return null;
+ }
+
+ const httpProxy = await safeExec('kreadconfig5', ['--group', 'Proxy Settings', '--key', 'httpProxy'], execOpts);
+ const httpsProxy = await safeExec('kreadconfig5', ['--group', 'Proxy Settings', '--key', 'httpsProxy'], execOpts);
+ const noProxy = await safeExec('kreadconfig5', ['--group', 'Proxy Settings', '--key', 'NoProxyFor'], execOpts);
+
+ const cleanHttpProxy = httpProxy || '';
+ const cleanHttpsProxy = httpsProxy || '';
+ const cleanNoProxy = noProxy || '';
+
+ const http_proxy = cleanHttpProxy ? normalizeProxyUrl(cleanHttpProxy) : null;
+ const https_proxy = cleanHttpsProxy ? normalizeProxyUrl(cleanHttpsProxy, 'https') : null;
+
+ return {
+ http_proxy,
+ https_proxy,
+ no_proxy: normalizeNoProxy(cleanNoProxy || null),
+ source: 'linux-system'
+ };
+ } catch (error) {
+ return null;
+ }
+ }
+
+ private async getEnvironmentFileProxy(): Promise {
+ try {
+ if (!existsSync('/etc/environment')) {
+ return null;
+ }
+ const content = await readFile('/etc/environment', 'utf8');
+ return this.parseProxyFromContent(content);
+ } catch (error) {
+ return null;
+ }
+ }
+
+ private async getSystemdProxy(): Promise {
+ try {
+ const systemdConfDir = '/etc/systemd/system.conf.d';
+ if (!existsSync(systemdConfDir)) {
+ return null;
+ }
+
+ // Look for systemd proxy configuration files
+ const files = await readdir(systemdConfDir);
+ const systemdFiles = files.filter((f) => f.endsWith('.conf'));
+ let content = '';
+
+ for (const file of systemdFiles) {
+ const filePath = `${systemdConfDir}/${file}`;
+ if (existsSync(filePath)) {
+ const fileContent = await readFile(filePath, 'utf8');
+ content += fileContent + '\n';
+ }
+ }
+
+ if (!content) {
+ return null;
+ }
+
+ return this.parseProxyFromContent(content);
+ } catch (error) {
+ return null;
+ }
+ }
+
+ private parseProxyFromContent(content: string): ProxyConfiguration | null {
+ const proxyVars = ['http_proxy', 'https_proxy', 'no_proxy', 'all_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY'];
+ const proxies: Record = {};
+
+ const lines = content.split('\n');
+ for (const line of lines) {
+ const trimmedLine = line.trim();
+
+ // Skip comments and empty lines
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
+ continue;
+ }
+
+ // Handle systemd Environment= and DefaultEnvironment= directives
+ const systemdEnvMatch = trimmedLine.match(/^(Default)?Environment\s*=\s*(.+)$/i);
+ if (systemdEnvMatch) {
+ const envVars = systemdEnvMatch[2];
+ // Parse key=value pairs from the directive (handles quoted and unquoted values)
+ const kvPairs = envVars.match(/([A-Z_]+)=(?:"([^"]*)"|'([^']*)'|(\S+))/gi);
+ if (kvPairs) {
+ for (const pair of kvPairs) {
+ const [key, ...valueParts] = pair.split('=');
+ const value = valueParts.join('=').replace(/^["']|["']$/g, '');
+ if (proxyVars.some((v) => v.toLowerCase() === key.toLowerCase())) {
+ proxies[key.toLowerCase()] = value;
+ }
+ }
+ }
+ continue;
+ }
+
+ // Handle different formats: VAR=value, export VAR=value, VAR="value", etc.
+ for (const { varName, pattern } of PROXY_VAR_PATTERNS) {
+ const match = trimmedLine.match(pattern);
+ if (match) {
+ let value = match[1].trim();
+ // Remove surrounding quotes
+ value = value.replace(/^["']|["']$/g, '');
+ proxies[varName] = value;
+ break;
+ }
+ }
+ }
+
+ // Convert to ProxyConfiguration format with ALL_PROXY fallback
+ const httpProxy = proxies.http_proxy || proxies.all_proxy || null;
+ const httpsProxy = proxies.https_proxy || proxies.all_proxy || null;
+ const http_proxy = httpProxy ? normalizeProxyUrl(httpProxy) : null;
+ const https_proxy = httpsProxy ? normalizeProxyUrl(httpsProxy, 'https') : null;
+ const no_proxy = proxies.no_proxy || null;
+
+ if (http_proxy || https_proxy) {
+ return {
+ http_proxy,
+ https_proxy,
+ no_proxy: normalizeNoProxy(no_proxy),
+ source: 'linux-system'
+ };
+ }
+
+ return null;
+ }
+}
diff --git a/packages/bruno-requests/src/network/system-proxy/utils/macos.spec.ts b/packages/bruno-requests/src/network/system-proxy/utils/macos.spec.ts
new file mode 100644
index 000000000..ea0dc25b2
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/utils/macos.spec.ts
@@ -0,0 +1,243 @@
+import { MacOSProxyResolver } from './macos';
+
+// Mock the entire child_process module
+jest.mock('node:child_process', () => ({
+ execFile: jest.fn()
+}));
+
+// Mock the util module
+jest.mock('node:util', () => ({
+ promisify: jest.fn((fn) => fn)
+}));
+
+describe('MacOSProxyResolver', () => {
+ let detector: MacOSProxyResolver;
+ let mockExecFile: jest.MockedFunction;
+
+ beforeEach(() => {
+ detector = new MacOSProxyResolver();
+ const { execFile } = require('node:child_process');
+ mockExecFile = execFile;
+ jest.clearAllMocks();
+ });
+
+ describe('scutil proxy detection', () => {
+ it('should detect HTTP and HTTPS proxy settings', async () => {
+ const scutilOutput = ` {
+ HTTPEnable : 1
+ HTTPPort : 8080
+ HTTPProxy : proxy.usebruno.com
+ HTTPSEnable : 1
+ HTTPSPort : 8443
+ HTTPSProxy : secure-proxy.usebruno.com
+ ExceptionsList : {
+ 0 : localhost
+ 1 : 127.0.0.1
+ }
+ ExcludeSimpleHostnames : 1
+}`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://secure-proxy.usebruno.com:8443',
+ no_proxy: 'localhost,127.0.0.1,',
+ source: 'macos-system'
+ });
+ });
+
+ it('should handle disabled proxy settings', async () => {
+ const scutilOutput = ` {
+ HTTPEnable : 0
+ HTTPProxy : proxy.usebruno.com
+ HTTPSEnable : 0
+ HTTPSProxy : proxy.usebruno.com
+}`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: null,
+ https_proxy: null,
+ no_proxy: null,
+ source: 'macos-system'
+ });
+ });
+
+ it('should use default ports when not specified', async () => {
+ const scutilOutput = ` {
+ HTTPEnable : 1
+ HTTPProxy : proxy.usebruno.com
+ HTTPSEnable : 1
+ HTTPSProxy : secure-proxy.usebruno.com
+}`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result.http_proxy).toBe('http://proxy.usebruno.com:80');
+ expect(result.https_proxy).toBe('https://secure-proxy.usebruno.com:443');
+ });
+
+ it('should handle only HTTP proxy enabled', async () => {
+ const scutilOutput = ` {
+ HTTPEnable : 1
+ HTTPPort : 8080
+ HTTPProxy : proxy.usebruno.com
+ HTTPSEnable : 0
+ HTTPSProxy : secure-proxy.usebruno.com
+}`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: null,
+ no_proxy: null,
+ source: 'macos-system'
+ });
+ });
+
+ it('should handle only HTTPS proxy enabled', async () => {
+ const scutilOutput = ` {
+ HTTPEnable : 0
+ HTTPProxy : proxy.usebruno.com
+ HTTPSEnable : 1
+ HTTPSPort : 8443
+ HTTPSProxy : secure-proxy.usebruno.com
+}`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: null,
+ https_proxy: 'https://secure-proxy.usebruno.com:8443',
+ no_proxy: null,
+ source: 'macos-system'
+ });
+ });
+
+ it('should handle empty exceptions list', async () => {
+ const scutilOutput = ` {
+ HTTPEnable : 1
+ HTTPPort : 8080
+ HTTPProxy : proxy.usebruno.com
+ HTTPSEnable : 1
+ HTTPSPort : 8080
+ HTTPSProxy : proxy.usebruno.com
+ ExceptionsList : {
+ }
+ ExcludeSimpleHostnames : 0
+}`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://proxy.usebruno.com:8080',
+ no_proxy: null,
+ source: 'macos-system'
+ });
+ });
+
+ it('should handle ExcludeSimpleHostnames without exceptions', async () => {
+ const scutilOutput = ` {
+ HTTPEnable : 1
+ HTTPPort : 8080
+ HTTPProxy : proxy.usebruno.com
+ HTTPSEnable : 1
+ HTTPSPort : 8080
+ HTTPSProxy : proxy.usebruno.com
+ ExcludeSimpleHostnames : 1
+}`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://proxy.usebruno.com:8080',
+ no_proxy: '',
+ source: 'macos-system'
+ });
+ });
+
+ it('should handle complex exceptions list', async () => {
+ const scutilOutput = ` {
+ HTTPEnable : 1
+ HTTPPort : 8080
+ HTTPProxy : proxy.usebruno.com
+ HTTPSEnable : 1
+ HTTPSPort : 8080
+ HTTPSProxy : proxy.usebruno.com
+ ExceptionsList : {
+ 0 : localhost
+ 1 : 127.0.0.1
+ 2 : *.local
+ 3 : 192.168.1.0/24
+ }
+ ExcludeSimpleHostnames : 1
+}`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1,*.local,192.168.1.0/24,',
+ source: 'macos-system'
+ });
+ });
+
+ it('should handle malformed scutil output gracefully', async () => {
+ const scutilOutput = ` {
+ HTTPEnable : 1
+ HTTPPort : 8080
+ HTTPProxy : proxy.usebruno.com
+ HTTPSEnable : 1
+ HTTPSPort : 8080
+ HTTPSProxy proxy.usebruno.com
+}`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: null,
+ no_proxy: null,
+ source: 'macos-system'
+ });
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should throw error when scutil command fails', async () => {
+ mockExecFile.mockRejectedValueOnce(new Error('scutil command not found'));
+
+ await expect(detector.detect()).rejects.toThrow('macOS proxy detection failed');
+ });
+
+ it('should throw error for invalid scutil output', async () => {
+ mockExecFile.mockResolvedValueOnce({ stdout: 'Invalid output format', stderr: '' });
+
+ await expect(detector.detect()).rejects.toThrow('macOS proxy detection failed');
+ });
+ });
+});
diff --git a/packages/bruno-requests/src/network/system-proxy/utils/macos.ts b/packages/bruno-requests/src/network/system-proxy/utils/macos.ts
new file mode 100644
index 000000000..0360b84e5
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/utils/macos.ts
@@ -0,0 +1,115 @@
+import { ExecFileOptions } from 'node:child_process';
+import { ProxyConfiguration, ProxyResolver } from '../types';
+import { normalizeProxyUrl, normalizeNoProxy, execFileAsync } from './common';
+
+export class MacOSProxyResolver implements ProxyResolver {
+ async detect(opts?: { timeoutMs?: number }): Promise {
+ const timeoutMs = opts?.timeoutMs ?? 10000;
+ const execOpts: ExecFileOptions = {
+ timeout: timeoutMs,
+ maxBuffer: 1024 * 1024
+ };
+
+ try {
+ const { stdout } = await execFileAsync('scutil', ['--proxy'], execOpts);
+ return this.parseScutilOutput(stdout);
+ } catch (error) {
+ throw new Error(`macOS proxy detection failed: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ private parseScutilOutput(output: string): ProxyConfiguration {
+ if (typeof output !== 'string') {
+ throw new Error('Invalid scutil --proxy output');
+ }
+
+ const cleanLines = output.split('\n')
+ .map((line) => line.trim())
+ .filter((line) => line.length > 0);
+
+ const dictStart = cleanLines.findIndex((line) => line.includes(''));
+ if (dictStart === -1) {
+ throw new Error('Invalid scutil --proxy output format');
+ }
+ const config = this.parseConfiguration(cleanLines, dictStart);
+ return this.buildProxyConfiguration(config);
+ }
+
+ private parseConfiguration(lines: string[], startIndex: number): Record {
+ const config: Record = {};
+ let i = startIndex + 1;
+
+ while (i < lines.length && !lines[i].includes('}')) {
+ const line = lines[i];
+
+ if (!line.trim()) {
+ i++;
+ continue;
+ }
+
+ const keyValueMatch = line.match(/^([^:]+)\s*:\s*(.+)$/);
+ if (keyValueMatch) {
+ const key = keyValueMatch[1].trim();
+ const value = keyValueMatch[2].trim();
+
+ if (value === ' {') {
+ // Parse array
+ const array: string[] = [];
+ i++;
+ while (i < lines.length && !lines[i].includes('}')) {
+ const arrayLine = lines[i].trim();
+ const arrayMatch = arrayLine.match(/^\d+\s*:\s*(.+)$/);
+ if (arrayMatch) {
+ array.push(arrayMatch[1].trim());
+ }
+ i++;
+ }
+ config[key] = array;
+ } else if (value.match(/^\d+$/)) {
+ config[key] = parseInt(value, 10);
+ } else {
+ config[key] = value;
+ }
+ }
+
+ i++;
+ }
+
+ return config;
+ }
+
+ private buildProxyConfiguration(config: Record): ProxyConfiguration {
+ let http_proxy: string | null = null;
+ let https_proxy: string | null = null;
+ let no_proxy: string | null = null;
+
+ // Check HTTP proxy
+ if (config.HTTPEnable === 1 && config.HTTPProxy) {
+ const port = config.HTTPPort || 80;
+ http_proxy = normalizeProxyUrl(`${config.HTTPProxy}:${port}`);
+ }
+
+ // Check HTTPS proxy
+ if (config.HTTPSEnable === 1 && config.HTTPSProxy) {
+ const port = config.HTTPSPort || 443;
+ https_proxy = normalizeProxyUrl(`${config.HTTPSProxy}:${port}`, 'https');
+ }
+
+ // Check bypass list
+ if (config.ExceptionsList && Array.isArray(config.ExceptionsList) && config.ExceptionsList.length > 0) {
+ no_proxy = config.ExceptionsList.join(',');
+ }
+
+ // Handle "exclude simple hostnames" setting
+ if (config.ExcludeSimpleHostnames === 1) {
+ no_proxy = no_proxy ? `${no_proxy},` : '';
+ }
+
+ return {
+ http_proxy,
+ https_proxy,
+ no_proxy: normalizeNoProxy(no_proxy),
+ source: 'macos-system'
+ };
+ }
+}
diff --git a/packages/bruno-requests/src/network/system-proxy/utils/windows.spec.ts b/packages/bruno-requests/src/network/system-proxy/utils/windows.spec.ts
new file mode 100644
index 000000000..82c24ac88
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/utils/windows.spec.ts
@@ -0,0 +1,400 @@
+import { WindowsProxyResolver } from './windows';
+
+// Mock the entire child_process module
+jest.mock('node:child_process', () => ({
+ execFile: jest.fn()
+}));
+
+// Mock the util module
+jest.mock('node:util', () => ({
+ promisify: jest.fn((fn) => fn)
+}));
+
+describe('WindowsProxyResolver', () => {
+ let detector: WindowsProxyResolver;
+ let mockExecFile: jest.MockedFunction;
+
+ beforeEach(() => {
+ detector = new WindowsProxyResolver();
+ const { execFile } = require('node:child_process');
+ mockExecFile = execFile;
+ jest.clearAllMocks();
+ });
+
+ describe('Internet Options Registry Detection', () => {
+ it('should detect single proxy configuration', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 0x1
+ ProxyServer REG_SZ proxy.usebruno.com:8080
+ ProxyOverride REG_SZ localhost;127.0.0.1
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'windows-system'
+ });
+ });
+
+ it('should detect protocol-specific proxy configuration', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 0x1
+ ProxyServer REG_SZ http=proxy.usebruno.com:8080;https=proxy.usebruno.com:8443
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://proxy.usebruno.com:8443',
+ no_proxy: null,
+ source: 'windows-system'
+ });
+ });
+
+ it('should fallback to WinHTTP when registry fails', async () => {
+ const winhttpOutput = `
+Current WinHTTP proxy settings:
+ Proxy Server(s) : proxy.usebruno.com:8080
+ Bypass List : localhost
+`;
+
+ mockExecFile
+ .mockRejectedValueOnce(new Error('Registry access denied'))
+ .mockResolvedValueOnce({ stdout: winhttpOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost',
+ source: 'windows-system'
+ });
+ });
+ });
+
+ describe('WinHTTP Detection', () => {
+ it('should handle direct access configuration', async () => {
+ mockExecFile
+ .mockRejectedValueOnce(new Error('Registry not accessible'))
+ .mockResolvedValueOnce({ stdout: 'Direct access (no proxy server)', stderr: '' });
+
+ await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');
+ });
+
+ it('should detect single proxy from WinHTTP', async () => {
+ const winhttpOutput = `
+Current WinHTTP proxy settings:
+ Proxy Server(s) : proxy.usebruno.com:8080
+ Bypass List : localhost;127.0.0.1
+`;
+
+ mockExecFile
+ .mockRejectedValueOnce(new Error('Registry access denied'))
+ .mockResolvedValueOnce({ stdout: winhttpOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'windows-system'
+ });
+ });
+
+ it('should detect protocol-specific proxy from WinHTTP', async () => {
+ const winhttpOutput = `
+Current WinHTTP proxy settings:
+ Proxy Server(s) : http=proxy.usebruno.com:8080;https=proxy.usebruno.com:8443
+ Bypass List : localhost
+`;
+
+ mockExecFile
+ .mockRejectedValueOnce(new Error('Registry access denied'))
+ .mockResolvedValueOnce({ stdout: winhttpOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://proxy.usebruno.com:8443',
+ no_proxy: 'localhost',
+ source: 'windows-system'
+ });
+ });
+
+ it('should handle WinHTTP with no bypass list', async () => {
+ const winhttpOutput = `
+Current WinHTTP proxy settings:
+ Proxy Server(s) : proxy.usebruno.com:8080
+ Bypass List : (none)
+`;
+
+ mockExecFile
+ .mockRejectedValueOnce(new Error('Registry access denied'))
+ .mockResolvedValueOnce({ stdout: winhttpOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: null,
+ source: 'windows-system'
+ });
+ });
+ });
+
+ describe('System Proxy Environment Detection', () => {
+ it('should detect system-wide proxy environment variables', async () => {
+ const regOutput = `
+HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment
+ HTTP_PROXY REG_SZ http://proxy.usebruno.com:8080
+ HTTPS_PROXY REG_SZ http://proxy.usebruno.com:8080
+ NO_PROXY REG_SZ localhost,127.0.0.1
+`;
+
+ mockExecFile
+ .mockRejectedValueOnce(new Error('Internet Options not accessible'))
+ .mockRejectedValueOnce(new Error('WinHTTP not accessible'))
+ .mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'windows-system'
+ });
+ });
+
+ it('should handle only HTTP_PROXY in system environment', async () => {
+ const regOutput = `
+HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment
+ HTTP_PROXY REG_SZ http://proxy.usebruno.com:8080
+`;
+
+ mockExecFile
+ .mockRejectedValueOnce(new Error('Internet Options not accessible'))
+ .mockRejectedValueOnce(new Error('WinHTTP not accessible'))
+ .mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: null,
+ no_proxy: null,
+ source: 'windows-system'
+ });
+ });
+ });
+
+ describe('User Environment Proxy Detection', () => {
+ it('should detect proxy from HKCU\\Environment', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Environment
+ HTTP_PROXY REG_SZ http://proxy.usebruno.com:8080
+ HTTPS_PROXY REG_SZ http://proxy.usebruno.com:8080
+ NO_PROXY REG_SZ localhost,127.0.0.1
+`;
+
+ mockExecFile
+ .mockRejectedValueOnce(new Error('Internet Options not accessible'))
+ .mockRejectedValueOnce(new Error('WinHTTP not accessible'))
+ .mockRejectedValueOnce(new Error('System environment not accessible'))
+ .mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'windows-system'
+ });
+ });
+ });
+
+ describe('Edge Cases and Parsing', () => {
+ it('should handle proxy server with existing http:// prefix', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 0x1
+ ProxyServer REG_SZ http://proxy.usebruno.com:8080
+ ProxyOverride REG_SZ localhost;127.0.0.1
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'windows-system'
+ });
+ });
+
+ it('should handle protocol-specific proxies with existing prefixes', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 0x1
+ ProxyServer REG_SZ http=http://proxy.usebruno.com:8080;https=https://secure-proxy.usebruno.com:8443
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'https://secure-proxy.usebruno.com:8443',
+ no_proxy: null,
+ source: 'windows-system'
+ });
+ });
+
+ it('should handle empty proxy override', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 0x1
+ ProxyServer REG_SZ proxy.usebruno.com:8080
+ ProxyOverride REG_SZ
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: null,
+ source: 'windows-system'
+ });
+ });
+
+ it('should handle proxy disabled in registry', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 0x0
+ ProxyServer REG_SZ proxy.usebruno.com:8080
+ ProxyOverride REG_SZ localhost;127.0.0.1
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');
+ });
+
+ it('should handle decimal ProxyEnable value', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 1
+ ProxyServer REG_SZ proxy.usebruno.com:8080
+ ProxyOverride REG_SZ localhost;127.0.0.1
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'windows-system'
+ });
+ });
+
+ it('should handle decimal ProxyEnable disabled', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 0
+ ProxyServer REG_SZ proxy.usebruno.com:8080
+ ProxyOverride REG_SZ localhost;127.0.0.1
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');
+ });
+
+ it('should handle malformed registry output gracefully', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 0x1
+ ProxyServer REG_SZ proxy.usebruno.com:8080
+ ProxyOverride REG_SZ localhost;127.0.0.1
+ SomeOtherValue REG_SZ ignored
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ http_proxy: 'http://proxy.usebruno.com:8080',
+ https_proxy: 'http://proxy.usebruno.com:8080',
+ no_proxy: 'localhost,127.0.0.1',
+ source: 'windows-system'
+ });
+ });
+
+ it('should handle complex bypass list', async () => {
+ const regOutput = `
+HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings
+ ProxyEnable REG_DWORD 0x1
+ ProxyServer REG_SZ proxy.usebruno.com:8080
+ ProxyOverride REG_SZ localhost;127.0.0.1;*.local;192.168.1.0/24
+`;
+
+ mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });
+
+ const result = await detector.detect();
+
+ expect(result).toEqual({
+ 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',
+ source: 'windows-system'
+ });
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should throw error when no proxy configuration is found', async () => {
+ mockExecFile.mockRejectedValue(new Error('Command failed'));
+
+ await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');
+ });
+
+ it('should handle registry access denied gracefully', async () => {
+ mockExecFile.mockRejectedValue(new Error('Access is denied'));
+
+ await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');
+ });
+
+ it('should handle malformed WinHTTP output', async () => {
+ mockExecFile
+ .mockRejectedValueOnce(new Error('Registry not accessible'))
+ .mockResolvedValueOnce({ stdout: 'Malformed WinHTTP output', stderr: '' });
+
+ await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');
+ });
+ });
+});
diff --git a/packages/bruno-requests/src/network/system-proxy/utils/windows.ts b/packages/bruno-requests/src/network/system-proxy/utils/windows.ts
new file mode 100644
index 000000000..e0e6ff5cf
--- /dev/null
+++ b/packages/bruno-requests/src/network/system-proxy/utils/windows.ts
@@ -0,0 +1,213 @@
+import { ExecFileOptions } from 'node:child_process';
+import { ProxyConfiguration, ProxyResolver } from '../types';
+import { normalizeProxyUrl, normalizeNoProxy, safeExec } from './common';
+
+export class WindowsProxyResolver implements ProxyResolver {
+ async detect(opts?: { timeoutMs?: number }): Promise {
+ const timeoutMs = opts?.timeoutMs ?? 10000;
+ const execOpts: ExecFileOptions = {
+ timeout: timeoutMs,
+ windowsHide: true,
+ maxBuffer: 1024 * 1024
+ };
+
+ try {
+ // Try different detection methods in order of preference
+ const detectionMethods = [
+ () => this.getInternetOptions(execOpts),
+ () => this.getWinHttpProxy(execOpts),
+ () => this.getSystemProxyEnvironment(execOpts),
+ () => this.getUserEnvironmentProxy(execOpts)
+ ];
+
+ for (const method of detectionMethods) {
+ try {
+ const proxy = await method();
+ if (proxy) {
+ return proxy;
+ }
+ } catch (error) {
+ // Continue to next method if this one fails
+ continue;
+ }
+ }
+
+ throw new Error('No Windows proxy configuration found');
+ } catch (error) {
+ throw new Error(`Windows proxy detection failed: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ private async getInternetOptions(execOpts: ExecFileOptions): Promise {
+ const stdout = await safeExec('reg', ['query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'], execOpts);
+ if (!stdout) return null;
+
+ const lines = stdout.split('\n');
+ let proxyEnabled = false;
+ let proxyServer: string | null = null;
+ let proxyOverride: string | null = null;
+
+ for (const line of lines) {
+ const trimmedLine = line.trim();
+
+ if (trimmedLine.includes('ProxyEnable') && trimmedLine.includes('REG_DWORD')) {
+ // Extract the value after REG_DWORD
+ const match = trimmedLine.match(/ProxyEnable\s+REG_DWORD\s+(0x[0-9a-fA-F]+|\d+)/);
+ if (match) {
+ const value = match[1];
+ proxyEnabled = value === '0x1' || value === '1';
+ }
+ }
+
+ if (trimmedLine.includes('ProxyServer') && trimmedLine.includes('REG_SZ')) {
+ const match = trimmedLine.match(/ProxyServer\s+REG_SZ\s+(.+)/);
+ if (match) proxyServer = match[1].trim();
+ }
+
+ if (trimmedLine.includes('ProxyOverride') && trimmedLine.includes('REG_SZ')) {
+ const match = trimmedLine.match(/ProxyOverride\s+REG_SZ\s+(.+)/);
+ if (match) proxyOverride = match[1].trim();
+ }
+ }
+
+ if (proxyEnabled && proxyServer) {
+ return this.parseProxyString(proxyServer, proxyOverride);
+ }
+
+ return null;
+ }
+
+ private async getWinHttpProxy(execOpts: ExecFileOptions): Promise {
+ const stdout = await safeExec('netsh', ['winhttp', 'show', 'proxy'], execOpts);
+ if (!stdout) return null;
+
+ if (stdout.includes('Direct access (no proxy server)')) {
+ return null;
+ }
+
+ const proxyServerMatch = stdout.match(/Proxy Server\(s\)\s*:\s*(.+)/);
+ const bypassListMatch = stdout.match(/Bypass List\s*:\s*(.+)/);
+
+ if (proxyServerMatch) {
+ const proxyServer = proxyServerMatch[1].trim();
+ const bypassList = bypassListMatch ? bypassListMatch[1].trim() : '';
+
+ return this.parseProxyString(proxyServer, bypassList);
+ }
+
+ return null;
+ }
+
+ private async getSystemProxyEnvironment(execOpts: ExecFileOptions): Promise {
+ // Check for system-wide proxy environment variables
+ const stdout = await safeExec('reg', ['query', 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment'], execOpts);
+ if (!stdout) return null;
+
+ const lines = stdout.split('\n');
+ let http_proxy: string | null = null;
+ let https_proxy: string | null = null;
+ let no_proxy: string | null = null;
+
+ for (const line of lines) {
+ const trimmedLine = line.trim();
+
+ if (trimmedLine.toUpperCase().includes('HTTP_PROXY') && trimmedLine.includes('REG_SZ')) {
+ const match = trimmedLine.match(/HTTP_PROXY\s+REG_SZ\s+(.+)/i);
+ if (match) http_proxy = match[1].trim();
+ }
+
+ if (trimmedLine.toUpperCase().includes('HTTPS_PROXY') && trimmedLine.includes('REG_SZ')) {
+ const match = trimmedLine.match(/HTTPS_PROXY\s+REG_SZ\s+(.+)/i);
+ if (match) https_proxy = match[1].trim();
+ }
+
+ if (trimmedLine.toUpperCase().includes('NO_PROXY') && trimmedLine.includes('REG_SZ')) {
+ const match = trimmedLine.match(/NO_PROXY\s+REG_SZ\s+(.+)/i);
+ if (match) no_proxy = match[1].trim();
+ }
+ }
+
+ if (http_proxy || https_proxy) {
+ return {
+ http_proxy: http_proxy ? normalizeProxyUrl(http_proxy) : null,
+ https_proxy: https_proxy ? normalizeProxyUrl(https_proxy, 'https') : null,
+ no_proxy: no_proxy ? normalizeNoProxy(no_proxy) : null,
+ source: 'windows-system'
+ };
+ }
+
+ return null;
+ }
+
+ private async getUserEnvironmentProxy(execOpts: ExecFileOptions): Promise {
+ // Check for user-specific proxy environment variables in HKCU\Environment
+ const stdout = await safeExec('reg', ['query', 'HKCU\\Environment'], execOpts);
+ if (!stdout) return null;
+
+ const lines = stdout.split('\n');
+ let http_proxy: string | null = null;
+ let https_proxy: string | null = null;
+ let no_proxy: string | null = null;
+
+ for (const line of lines) {
+ const trimmedLine = line.trim();
+
+ if (trimmedLine.toUpperCase().includes('HTTP_PROXY') && trimmedLine.includes('REG_SZ')) {
+ const match = trimmedLine.match(/HTTP_PROXY\s+REG_SZ\s+(.+)/i);
+ if (match) http_proxy = match[1].trim();
+ }
+
+ if (trimmedLine.toUpperCase().includes('HTTPS_PROXY') && trimmedLine.includes('REG_SZ')) {
+ const match = trimmedLine.match(/HTTPS_PROXY\s+REG_SZ\s+(.+)/i);
+ if (match) https_proxy = match[1].trim();
+ }
+
+ if (trimmedLine.toUpperCase().includes('NO_PROXY') && trimmedLine.includes('REG_SZ')) {
+ const match = trimmedLine.match(/NO_PROXY\s+REG_SZ\s+(.+)/i);
+ if (match) no_proxy = match[1].trim();
+ }
+ }
+
+ if (http_proxy || https_proxy) {
+ return {
+ http_proxy: http_proxy ? normalizeProxyUrl(http_proxy) : null,
+ https_proxy: https_proxy ? normalizeProxyUrl(https_proxy, 'https') : null,
+ no_proxy: no_proxy ? normalizeNoProxy(no_proxy) : null,
+ source: 'windows-system'
+ };
+ }
+
+ return null;
+ }
+
+ private parseProxyString(proxyServer: string, bypassList: string | null): ProxyConfiguration {
+ let http_proxy: string | null = null;
+ let https_proxy: string | null = null;
+
+ if (proxyServer.includes('=')) {
+ // Protocol-specific format: "http=proxy1:8080;https=proxy2:8080"
+ const protocols = proxyServer.split(';');
+ for (const protocol of protocols) {
+ const [proto, server] = protocol.split('=');
+ if (!server || !proto) continue;
+ if (proto === 'http') {
+ http_proxy = normalizeProxyUrl(server);
+ } else if (proto === 'https') {
+ https_proxy = normalizeProxyUrl(server, 'https');
+ }
+ }
+ } else {
+ // Single proxy for all protocols: "proxy.example.com:8080"
+ const proxy = normalizeProxyUrl(proxyServer);
+ http_proxy = proxy;
+ https_proxy = proxy;
+ }
+
+ return {
+ http_proxy,
+ https_proxy,
+ no_proxy: bypassList && bypassList !== '(none)' ? normalizeNoProxy(bypassList) : null,
+ source: 'windows-system'
+ };
+ }
+}