diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 4022152e8..9c3c5fb9e 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -22,7 +22,7 @@ const { createFormData } = require('../utils/form-data'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const { NtlmClient } = require('axios-ntlm'); const { addDigestInterceptor } = require('@usebruno/requests'); -const { getCACertificates } = require('@usebruno/requests'); +const { getCACertificates, transformProxyConfig } = require('@usebruno/requests'); const { getOAuth2Token } = require('../utils/oauth2'); const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData } = require('@usebruno/common').utils; @@ -288,25 +288,27 @@ const runSingleRequest = async function ( let proxyMode = 'off'; let proxyConfig = {}; - const collectionProxyConfig = get(brunoConfig, 'proxy', {}); - const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', false); + const collectionProxyConfig = transformProxyConfig(get(brunoConfig, 'proxy', {})); + const collectionProxyDisabled = get(collectionProxyConfig, 'disabled', false); + const collectionProxyInherit = get(collectionProxyConfig, 'inherit', true); + const collectionProxyConfigData = get(collectionProxyConfig, 'config', {}); - if (noproxy) { - // If noproxy flag is set, don't use any proxy + if (noproxy || collectionProxyDisabled) { + // If noproxy flag is set or collection proxy is disabled, don't use any proxy proxyMode = 'off'; - } else if (collectionProxyEnabled === true) { - // If collection proxy is enabled, use it - proxyConfig = collectionProxyConfig; + } else if (!collectionProxyDisabled && !collectionProxyInherit) { + // Use collection-specific proxy + proxyConfig = collectionProxyConfigData; proxyMode = 'on'; - } else if (collectionProxyEnabled === 'global') { - // If collection proxy is set to 'global', use system proxy + } else if (!collectionProxyDisabled && collectionProxyInherit) { + // Inherit from system proxy const { http_proxy, https_proxy } = getSystemProxyEnvVariables(); if (http_proxy?.length || https_proxy?.length) { proxyMode = 'system'; } - } else { - proxyMode = 'off'; + // else: no system proxy available, proxyMode stays 'off' } + // else: collection proxy is disabled, proxyMode stays 'off' if (proxyMode === 'on') { const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', '')); @@ -314,7 +316,7 @@ const runSingleRequest = async function ( const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions); const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions); const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions); - const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false); + const proxyAuthEnabled = !get(proxyConfig, 'auth.disabled', false); const socksEnabled = proxyProtocol.includes('socks'); let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`; let proxyUri; diff --git a/packages/bruno-electron/src/utils/transformBrunoConfig.js b/packages/bruno-electron/src/utils/transformBrunoConfig.js index 43a5e21d3..60b59fd7f 100644 --- a/packages/bruno-electron/src/utils/transformBrunoConfig.js +++ b/packages/bruno-electron/src/utils/transformBrunoConfig.js @@ -1,6 +1,6 @@ const path = require('path'); const { isFile, isDirectory } = require('./filesystem'); -const { get } = require('lodash'); +const { transformProxyConfig } = require('@usebruno/requests'); function transformBrunoConfigBeforeSave(brunoConfig) { // remove exists from importPaths and protoFiles @@ -76,55 +76,7 @@ async function transformBrunoConfigAfterRead(brunoConfig, collectionPathname) { // Migrate proxy configuration from old format to new format if (brunoConfig.proxy) { - const proxy = brunoConfig.proxy || {}; - - // Check if this is an old format (has 'enabled' property) - if (proxy.hasOwnProperty('enabled')) { - const enabled = proxy.enabled; - - let newProxy = { - inherit: true, - config: { - protocol: proxy.protocol || 'http', - hostname: proxy.hostname || '', - port: proxy.port || null, - auth: { - username: get(proxy, 'auth.username', ''), - password: get(proxy, 'auth.password', '') - }, - bypassProxy: proxy.bypassProxy || '' - } - }; - - // Handle old format: enabled (true | false | 'global') - if (enabled === true) { - newProxy.disabled = false; - newProxy.inherit = false; - } else if (enabled === false) { - newProxy.disabled = true; - newProxy.inherit = false; - } else if (enabled === 'global') { - newProxy.disabled = false; - newProxy.inherit = true; - } - - // Migrate auth.enabled to auth.disabled - if (get(proxy, 'auth.enabled') === false) { - newProxy.config.auth.disabled = true; - } - // If auth.enabled is true or undefined, omit disabled (defaults to false) - - // Omit disabled: false at top level (optional field) - if (newProxy.disabled === false) { - delete newProxy.disabled; - } - // Omit auth.disabled: false (optional field) - if (newProxy.config.auth.disabled === false) { - delete newProxy.config.auth.disabled; - } - - brunoConfig.proxy = newProxy; - } + brunoConfig.proxy = transformProxyConfig(brunoConfig.proxy); } return brunoConfig; diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index 52ef41696..7fe1838a1 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -4,6 +4,7 @@ export { WsClient } from './ws/ws-client'; export { default as cookies } from './cookies'; export { getCACertificates } from './utils/ca-cert'; +export { transformProxyConfig } from './utils/proxy-util'; export { default as createVaultClient, VaultError } from './utils/node-vault'; export type { VaultClient, VaultConfig, VaultRequestOptions } from './utils/node-vault'; diff --git a/packages/bruno-requests/src/utils/proxy-util.spec.ts b/packages/bruno-requests/src/utils/proxy-util.spec.ts new file mode 100644 index 000000000..27050dac8 --- /dev/null +++ b/packages/bruno-requests/src/utils/proxy-util.spec.ts @@ -0,0 +1,336 @@ +import { transformProxyConfig } from './proxy-util'; + +describe('transformProxyConfig', () => { + describe('Migration from old to new format', () => { + describe('Old Format: enabled (true | false | "global")', () => { + test('should migrate enabled: true to disabled: false, inherit: false', () => { + const oldConfig = { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: 'localhost' + }; + + const result = transformProxyConfig(oldConfig); + + expect(result).toEqual({ + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + username: 'user', + password: 'pass' + }, + bypassProxy: 'localhost' + } + }); + expect((result as any).disabled).toBeUndefined(); // disabled: false is omitted + }); + + test('should migrate enabled: false to disabled: true, inherit: false', () => { + const oldConfig = { + enabled: false, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).disabled).toBe(true); + expect((result as any).inherit).toBe(false); + }); + + test('should migrate enabled: "global" to disabled: false, inherit: true', () => { + const oldConfig = { + enabled: 'global' as const, + protocol: 'http', + hostname: '', + port: null, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).disabled).toBeUndefined(); // disabled: false is omitted + expect((result as any).inherit).toBe(true); + }); + + test('should migrate auth.enabled: false to auth.disabled: true', () => { + const oldConfig = { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: false, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.auth.disabled).toBe(true); + expect((result as any).config.auth.username).toBe('user'); + expect((result as any).config.auth.password).toBe('pass'); + }); + + test('should omit auth.disabled when auth.enabled: true', () => { + const oldConfig = { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.auth.disabled).toBeUndefined(); + expect((result as any).config.auth.username).toBe('user'); + expect((result as any).config.auth.password).toBe('pass'); + }); + }); + + describe('New Format (no migration)', () => { + test('should not modify new format with inherit: false', () => { + const newConfig = { + inherit: false, + config: { + protocol: 'https', + hostname: 'proxy.example.com', + port: 8443, + auth: { + username: 'user', + password: 'pass' + }, + bypassProxy: '*.local' + } + }; + + const result = transformProxyConfig(newConfig); + + expect(result).toEqual(newConfig); + }); + + test('should not modify new format with inherit: true', () => { + const newConfig = { + inherit: true, + config: { + protocol: 'http', + hostname: '', + port: null, + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } + }; + + const result = transformProxyConfig(newConfig); + + expect(result).toEqual(newConfig); + }); + + test('should not modify new format with disabled: true', () => { + const newConfig = { + disabled: true, + inherit: false, + config: { + protocol: 'http', + hostname: '', + port: null, + auth: { + username: '', + password: '' + }, + bypassProxy: '' + } + }; + + const result = transformProxyConfig(newConfig); + + expect(result).toEqual(newConfig); + }); + + test('should not modify new format with auth.disabled: true', () => { + const newConfig = { + inherit: false, + config: { + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + disabled: true, + username: 'user', + password: 'pass' + }, + bypassProxy: '' + } + }; + + const result = transformProxyConfig(newConfig); + + expect(result).toEqual(newConfig); + }); + }); + + describe('Edge Cases', () => { + test('should handle missing/null/undefined proxy config', () => { + expect(transformProxyConfig(null)).toEqual({}); + expect(transformProxyConfig(undefined)).toEqual({}); + expect(transformProxyConfig({})).toEqual({}); + }); + + test('should handle null port values', () => { + const oldConfig = { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: null, + auth: { + enabled: false, + username: '', + password: '' + }, + bypassProxy: '' + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.port).toBeNull(); + }); + + test('should handle SOCKS protocols', () => { + const oldConfig = { + enabled: true, + protocol: 'socks5', + hostname: 'socks.example.com', + port: 1080, + auth: { + enabled: true, + username: 'socksuser', + password: 'sockspass' + }, + bypassProxy: '' + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.protocol).toBe('socks5'); + expect((result as any).config.hostname).toBe('socks.example.com'); + expect((result as any).config.port).toBe(1080); + }); + + test('should handle missing auth object', () => { + const oldConfig = { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + bypassProxy: '' + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.auth).toEqual({ + username: '', + password: '' + }); + }); + + test('should handle missing protocol (defaults to http)', () => { + const oldConfig = { + enabled: true, + hostname: 'proxy.example.com', + port: 8080 + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.protocol).toBe('http'); + }); + + test('should handle missing hostname (defaults to empty string)', () => { + const oldConfig = { + enabled: true, + protocol: 'http', + port: 8080 + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.hostname).toBe(''); + }); + + test('should handle missing port (defaults to null)', () => { + const oldConfig = { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com' + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.port).toBeNull(); + }); + + test('should handle missing bypassProxy (defaults to empty string)', () => { + const oldConfig = { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080 + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.bypassProxy).toBe(''); + }); + + test('should handle auth with missing username/password', () => { + const oldConfig = { + enabled: true, + protocol: 'http', + hostname: 'proxy.example.com', + port: 8080, + auth: { + enabled: true + } + }; + + const result = transformProxyConfig(oldConfig); + + expect((result as any).config.auth.username).toBe(''); + expect((result as any).config.auth.password).toBe(''); + }); + }); + }); +}); diff --git a/packages/bruno-requests/src/utils/proxy-util.ts b/packages/bruno-requests/src/utils/proxy-util.ts new file mode 100644 index 000000000..d2fea9eb8 --- /dev/null +++ b/packages/bruno-requests/src/utils/proxy-util.ts @@ -0,0 +1,92 @@ +/** + * Transform proxy config from old format to new format. + * Old format: { enabled: true | false | 'global', protocol, hostname, port, auth: { enabled, ... }, ... } + * New format: { disabled?, inherit, config: { protocol, hostname, port, auth: { disabled?, ... }, ... } } + */ + +interface OldProxyAuth { + enabled?: boolean; + username?: string; + password?: string; +} + +interface OldProxyConfig { + enabled?: true | false | 'global'; + protocol?: string; + hostname?: string; + port?: number | null; + auth?: OldProxyAuth; + bypassProxy?: string; +} + +interface NewProxyAuth { + disabled?: boolean; + username?: string; + password?: string; +} + +interface NewProxyConfig { + disabled?: boolean; + inherit: boolean; + config: { + protocol: string; + hostname: string; + port: number | null; + auth: NewProxyAuth; + bypassProxy: string; + }; +} + +export const transformProxyConfig = (proxy: OldProxyConfig | NewProxyConfig | null | undefined): NewProxyConfig | OldProxyConfig => { + proxy = proxy || {}; + // Check if this is an old format (has 'enabled' property) + if (proxy.hasOwnProperty('enabled')) { + const oldProxy = proxy as OldProxyConfig; + const enabled = oldProxy.enabled; + + const newProxy: NewProxyConfig = { + inherit: true, + config: { + protocol: oldProxy.protocol || 'http', + hostname: oldProxy.hostname || '', + port: oldProxy.port || null, + auth: { + username: oldProxy.auth?.username || '', + password: oldProxy.auth?.password || '' + }, + bypassProxy: oldProxy.bypassProxy || '' + } + }; + + // Handle old format: enabled (true | false | 'global') + if (enabled === true) { + newProxy.disabled = false; + newProxy.inherit = false; + } else if (enabled === false) { + newProxy.disabled = true; + newProxy.inherit = false; + } else if (enabled === 'global') { + newProxy.disabled = false; + newProxy.inherit = true; + } + + // Migrate auth.enabled to auth.disabled + if (oldProxy.auth?.enabled === false) { + newProxy.config.auth.disabled = true; + } + // If auth.enabled is true or undefined, omit disabled (defaults to false) + + // Omit disabled: false at top level (optional field) + if (newProxy.disabled === false) { + delete newProxy.disabled; + } + // Omit auth.disabled: false (optional field) + if (newProxy.config.auth.disabled === false) { + delete newProxy.config.auth.disabled; + } + + return newProxy; + } + + return proxy; +};