Skip to content

Commit e1b033f

Browse files
authored
fix: refactor auto-discovery for OS proxy settings (#654)
# Problem Related PRs: - #553 - #595 (Original) - #604 (Reverted PR) We originally bolted on an exec-time shim that `require.resolve’d` [os-proxy-config](https://www.npmjs.com/package/os-proxy-config) and ran it in a child-process so we could keep `ProxyConfigManager.getSystemProxySync()` fully synchronous. This avoided a ripple-effect async refactor through every SDK-initialisation call-chain and let us ship quickly, but it also meant the code was never picked up by Webpack, so the proxy lookup silently no-oped inside the bundled LSP. ## Solution - Vendored the tiny os-proxy-config implementation directly (`getMacProxySettings.ts`, `getWindowsProxySettings.ts`) and removed the child-process shim. - Reference Code: [os-proxy-config](https://github.com/httptoolkit/os-proxy-config), [windows-system-proxy](https://github.com/httptoolkit/windows-system-proxy/tree/main), [mac-system-proxy](https://github.com/httptoolkit/mac-system-proxy/tree/main) - Re-wrote the Windows & macOS helpers to expose sync variants (registry reads with `registry-js` sync API, `scutil --proxy` via `spawnSync`) so `getSystemProxy()` remains blocking. - Behaviour is unchanged for callers, but proxy auto-detect now runs in all packed LSPs because the code is part of the bundle. ## TODO - Add support for SOCKS Proxy ## Testing Simulated customer proxy environments on Windows and Mac and tested plugin build: - #### Mac Proxy is being auto-detected and requests are going through proxy: <img width="1244" alt="Screenshot 2025-07-01 at 12 44 15 AM" src="https://github.com/user-attachments/assets/53989ccb-c39b-4276-b447-ff3f50cf867a" /> - #### Windows Proxy is being auto-detected and requests are going through proxy: ``` Using bare hostname format, defaulting to HTTP [Trace - 6:30:06 PM] Received notification 'telemetry/event'. Params: { "name": "runtime_httpProxyConfiguration", "result": "Succeeded", "data": { "proxyMode": "AutoDetect", "certificatesNumber": 208, "proxyUrl": "http://localhost:8888/" } } ``` <img width="1287" alt="Screenshot 2025-07-01 at 6 32 53 PM" src="https://github.com/user-attachments/assets/494b264c-7049-4a52-81d3-c0b94f91a2ac" /> ## License By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 2325ccb commit e1b033f

File tree

9 files changed

+539
-62
lines changed

9 files changed

+539
-62
lines changed

package-lock.json

Lines changed: 7 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

runtimes/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@
4545
"hpagent": "^1.2.0",
4646
"jose": "^5.9.6",
4747
"mac-ca": "^3.1.1",
48-
"os-proxy-config": "^1.1.2",
4948
"rxjs": "^7.8.2",
5049
"vscode-languageserver": "^9.0.1",
5150
"vscode-languageserver-protocol": "^3.17.5",
5251
"vscode-uri": "^3.1.0",
53-
"win-ca": "^3.5.1"
52+
"win-ca": "^3.5.1",
53+
"registry-js": "^1.16.1"
5454
},
5555
"devDependencies": {
5656
"@types/mocha": "^10.0.9",

runtimes/runtimes/util/standalone/experimentalProxyUtil.ts

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { NodeHttpHandler } from '@smithy/node-http-handler'
1313
import { readMacosCertificates, readLinuxCertificates, readWindowsCertificates } from './certificatesReaders'
1414
import { Telemetry } from '../../../server-interface'
1515
import { OperationalTelemetryProvider, TELEMETRY_SCOPES } from '../../operational-telemetry/operational-telemetry'
16-
import { pathToFileURL } from 'node:url'
17-
import { execFileSync } from 'node:child_process'
16+
import { getMacSystemProxy } from './getProxySettings/getMacProxySettings'
17+
import { getWindowsSystemProxy } from './getProxySettings/getWindowsProxySettings'
1818

1919
export class ProxyConfigManager {
2020
/**
@@ -71,35 +71,14 @@ export class ProxyConfigManager {
7171
return undefined
7272
}
7373

74-
private static getSystemProxySync(): string | undefined {
75-
try {
76-
const resolved = require.resolve('os-proxy-config')
77-
const resolvedUrl = pathToFileURL(resolved).href
78-
79-
const snippet = `
80-
(async () => {
81-
try {
82-
const mod = await import(${JSON.stringify(resolvedUrl)});
83-
const r = await mod.getSystemProxy();
84-
console.log(JSON.stringify(r ?? {}));
85-
} catch (e) {
86-
console.error(e?.message ?? e);
87-
console.log("{}");
88-
}
89-
})();
90-
`
91-
92-
const raw = execFileSync(process.execPath, ['-e', snippet], {
93-
encoding: 'utf8',
94-
stdio: ['ignore', 'pipe', 'inherit'],
95-
})
96-
97-
console.debug(`os-proxy-config output: ${raw}`)
98-
const { proxyUrl } = JSON.parse(raw.trim() || '{}')
99-
return proxyUrl && /^https?:\/\//.test(proxyUrl) ? proxyUrl : undefined
100-
} catch (err) {
101-
console.warn('os‑proxy‑config shim failed:', (err as Error).message)
102-
return undefined
74+
private static getSystemProxy(): string | undefined {
75+
switch (process.platform) {
76+
case 'darwin':
77+
return getMacSystemProxy()?.proxyUrl
78+
case 'win32':
79+
return getWindowsSystemProxy()?.proxyUrl
80+
default:
81+
return undefined
10382
}
10483
}
10584

@@ -209,7 +188,7 @@ export class ProxyConfigManager {
209188
}
210189

211190
// Fall back to OS auto‑detect (HTTP or HTTPS only)
212-
const sysProxyUrl = ProxyConfigManager.getSystemProxySync()
191+
const sysProxyUrl = ProxyConfigManager.getSystemProxy()
213192
if (sysProxyUrl) {
214193
this.emitProxyMetric('AutoDetect', certs.length, sysProxyUrl)
215194
return new HttpsProxyAgent({ ...agentOptions, proxy: sysProxyUrl })
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Based on mac-system-proxy 1.0.2 (Apache-2.0). Modified for synchronous use
3+
* https://github.com/httptoolkit/mac-system-proxy/blob/main/test/index.spec.ts
4+
*/
5+
import * as assert from 'assert'
6+
import { getMacSystemProxy } from './getMacProxySettings'
7+
8+
describe('getMacSystemProxy', () => {
9+
it('can get the Mac system proxy', function () {
10+
if (process.platform !== 'darwin') return this.skip()
11+
12+
const result = getMacSystemProxy()
13+
assert.ok(result === undefined || (typeof result === 'object' && result !== null))
14+
15+
if (result) {
16+
assert.strictEqual(typeof result.proxyUrl, 'string')
17+
assert.ok(Array.isArray(result.noProxy))
18+
}
19+
})
20+
21+
it('returns undefined on non-Mac platforms', function () {
22+
if (process.platform === 'darwin') return this.skip()
23+
24+
const result = getMacSystemProxy()
25+
assert.strictEqual(result, undefined)
26+
})
27+
28+
it('handles scutil command failure gracefully', function () {
29+
// This test verifies the function doesn't throw
30+
assert.doesNotThrow(() => getMacSystemProxy())
31+
})
32+
33+
it('returns undefined when no proxy is configured', function () {
34+
if (process.platform !== 'darwin') return this.skip()
35+
36+
const result = getMacSystemProxy()
37+
if (result) {
38+
assert.strictEqual(typeof result.proxyUrl, 'string')
39+
assert.ok(Array.isArray(result.noProxy))
40+
}
41+
})
42+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* Based on mac-system-proxy 1.0.2 (Apache-2.0). Modified for synchronous use
6+
* https://github.com/httptoolkit/mac-system-proxy/blob/main/src/index.ts
7+
*/
8+
import { spawnSync } from 'child_process'
9+
import { parseScutilOutput } from './parseScutil'
10+
11+
export interface ProxyConfig {
12+
proxyUrl: string
13+
noProxy: string[]
14+
}
15+
16+
export function getMacSystemProxy(): ProxyConfig | undefined {
17+
// Invoke `scutil --proxy` synchronously
18+
console.debug('Executing scutil --proxy to retrieve Mac system proxy settings')
19+
const result = spawnSync('scutil', ['--proxy'], { encoding: 'utf8' })
20+
if (result.error || result.status !== 0) {
21+
console.warn(`scutil --proxy failed: ${result.error?.message ?? 'exit code ' + result.status}`)
22+
return undefined
23+
}
24+
console.debug('Successfully retrieved scutil output')
25+
26+
let settings: Record<string, any>
27+
try {
28+
settings = parseScutilOutput(result.stdout)
29+
} catch (e: any) {
30+
console.warn(`Failed to parse scutil output: ${e.message}`)
31+
return undefined
32+
}
33+
34+
const noProxy = settings.ExceptionsList ?? []
35+
36+
// Honor PAC URL first if configured
37+
if (settings.ProxyAutoConfigEnable === '1' && settings.ProxyAutoConfigURLString) {
38+
console.debug(`Using PAC URL: ${settings.ProxyAutoConfigURLString}`)
39+
return { proxyUrl: settings.ProxyAutoConfigURLString, noProxy }
40+
}
41+
42+
// Otherwise pick the first enabled protocol
43+
console.debug('Checking for enabled proxy protocols')
44+
if (settings.HTTPEnable === '1' && settings.HTTPProxy && settings.HTTPPort) {
45+
console.debug(`Using HTTP proxy: ${settings.HTTPProxy}:${settings.HTTPPort}`)
46+
return { proxyUrl: `http://${settings.HTTPProxy}:${settings.HTTPPort}`, noProxy }
47+
}
48+
if (settings.HTTPSEnable === '1' && settings.HTTPSProxy && settings.HTTPSPort) {
49+
console.debug(`Using HTTPS proxy: ${settings.HTTPSProxy}:${settings.HTTPSPort}`)
50+
return { proxyUrl: `http://${settings.HTTPSProxy}:${settings.HTTPSPort}`, noProxy }
51+
}
52+
// TODO: Enable support for SOCKS Proxy
53+
// if (settings.SOCKSEnable === '1' && settings.SOCKSProxy && settings.SOCKSPort) {
54+
// console.debug(`Using SOCKS proxy: ${settings.SOCKSProxy}:${settings.SOCKSPort}`)
55+
// return { proxyUrl: `socks://${settings.SOCKSProxy}:${settings.SOCKSPort}`, noProxy }
56+
// }
57+
58+
return undefined
59+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Based on windows-system-proxy 1.0.0 (Apache-2.0).
3+
* https://github.com/httptoolkit/windows-system-proxy/blob/main/test/index.spec.ts
4+
*/
5+
import * as assert from 'assert'
6+
import { getWindowsSystemProxy } from './getWindowsProxySettings'
7+
8+
describe('getWindowsSystemProxy', () => {
9+
it('can get the Windows system proxy', function () {
10+
if (process.platform !== 'win32') return this.skip()
11+
12+
const result = getWindowsSystemProxy()
13+
assert.ok(result === undefined || (typeof result === 'object' && result !== null))
14+
15+
if (result) {
16+
assert.strictEqual(typeof result.proxyUrl, 'string')
17+
assert.ok(Array.isArray(result.noProxy))
18+
}
19+
})
20+
21+
it('returns undefined on non-Windows platforms', function () {
22+
if (process.platform === 'win32') return this.skip()
23+
24+
const result = getWindowsSystemProxy()
25+
assert.strictEqual(result, undefined)
26+
})
27+
28+
it('handles registry access failure gracefully', function () {
29+
// This test verifies the function doesn't throw
30+
assert.doesNotThrow(() => getWindowsSystemProxy())
31+
})
32+
})
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* Based on windows-system-proxy 1.0.0 (Apache-2.0). Modified for synchronous use
6+
* https://github.com/httptoolkit/windows-system-proxy/blob/main/src/index.ts
7+
*/
8+
import { enumerateValues, HKEY, RegistryValue } from 'registry-js'
9+
10+
export interface ProxyConfig {
11+
proxyUrl: string
12+
noProxy: string[]
13+
}
14+
15+
export function getWindowsSystemProxy(): ProxyConfig | undefined {
16+
const proxyValues = enumerateValues(
17+
HKEY.HKEY_CURRENT_USER,
18+
'Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'
19+
)
20+
console.debug(`Retrieved ${proxyValues.length} registry values for proxy settings`)
21+
22+
const proxyEnabled = getValue(proxyValues, 'ProxyEnable')
23+
const proxyServer = getValue(proxyValues, 'ProxyServer')
24+
25+
if (!proxyEnabled || !proxyEnabled.data || !proxyServer || !proxyServer.data) {
26+
console.debug('Proxy not enabled or server not configured')
27+
return undefined
28+
}
29+
30+
// Build noProxy list from ProxyOverride (semicolon-separated, with <local> → localhost,127.0.0.1,::1)
31+
const proxyOverride = getValue(proxyValues, 'ProxyOverride')?.data
32+
const noProxy = (proxyOverride ? (proxyOverride as string).split(';') : []).flatMap(host =>
33+
host === '<local>' ? ['localhost', '127.0.0.1', '::1'] : [host]
34+
)
35+
36+
// Parse proxy configuration which can be in multiple formats
37+
const proxyConfigString = proxyServer.data as string
38+
39+
if (proxyConfigString.startsWith('http://') || proxyConfigString.startsWith('https://')) {
40+
console.debug('Using full URL format proxy configuration')
41+
// Handle full URL format (documented in Microsoft registry configuration guide)
42+
// https://docs.microsoft.com/en-us/troubleshoot/windows-client/networking/configure-client-proxy-server-settings-by-registry-file
43+
return {
44+
proxyUrl: proxyConfigString,
45+
noProxy,
46+
}
47+
} else if (proxyConfigString.includes('=')) {
48+
console.debug('Using protocol-specific format proxy configuration')
49+
// Handle protocol-specific format: protocol=host;protocol=host pairs
50+
// Prefer HTTPS, then HTTP, then SOCKS proxy
51+
const proxies = Object.fromEntries(
52+
proxyConfigString.split(';').map(proxyPair => proxyPair.split('=') as [string, string])
53+
)
54+
55+
const proxyUrl = proxies['https']
56+
? `https://${proxies['https']}`
57+
: proxies['http']
58+
? `http://${proxies['http']}`
59+
: // TODO: Enable support for SOCKS Proxy
60+
// proxies['socks'] ? `socks://${proxies['socks']}`:
61+
undefined
62+
63+
if (!proxyUrl) {
64+
throw new Error(`Could not get usable proxy URL from ${proxyConfigString}`)
65+
}
66+
console.debug(`Selected proxy URL: ${proxyUrl}`)
67+
68+
return {
69+
proxyUrl,
70+
noProxy,
71+
}
72+
} else {
73+
console.debug('Using bare hostname format, defaulting to HTTP')
74+
// Handle bare hostname format, default to HTTP
75+
return {
76+
proxyUrl: `http://${proxyConfigString}`,
77+
noProxy,
78+
}
79+
}
80+
}
81+
82+
const getValue = (values: readonly RegistryValue[], name: string) => values.find(value => value?.name === name)

0 commit comments

Comments
 (0)