Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
591 changes: 559 additions & 32 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions runtimes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,13 @@
"vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.1.0",
"win-ca": "^3.5.1",
"winreg": "^1.2.5"
"registry-js": "^1.16.1"
Copy link
Member

Choose a reason for hiding this comment

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

instead of re-introducing this dependency with known CVE - is there a better fix?

},
"devDependencies": {
"@types/mocha": "^10.0.9",
"@types/mock-fs": "^4.13.4",
"@types/node": "^22.15.17",
"@types/node-forge": "^1.3.11",
"@types/winreg": "^1.2.36",
"assert": "^2.0.0",
"copyfiles": "^2.4.1",
"husky": "^9.1.7",
Expand Down
8 changes: 4 additions & 4 deletions runtimes/runtimes/standalone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('standalone', () => {
let chatStub: sinon.SinonStubbedInstance<encryptedChatModule.EncryptedChat> & encryptedChatModule.EncryptedChat
let baseChatStub: sinon.SinonStubbedInstance<baseChatModule.BaseChat> & baseChatModule.BaseChat

it('should initialize without encryption when no key is present', async () => {
it('should initialize without encryption when no key is present', () => {
sinon.stub(authEncryptionModule, 'shouldWaitForEncryptionKey').returns(false)
authStub = stubInterface<authModule.Auth>()
authStub.getCredentialsProvider.returns({
Expand All @@ -61,7 +61,7 @@ describe('standalone', () => {
baseChatStub = stubInterface<baseChatModule.BaseChat>()
sinon.stub(baseChatModule, 'BaseChat').returns(baseChatStub)

await standalone(props)
standalone(props)

sinon.assert.calledWithExactly(authModule.Auth as unknown as sinon.SinonStub, stubConnection, lspRouterStub)
sinon.assert.calledWithExactly(
Expand Down Expand Up @@ -126,8 +126,8 @@ describe('standalone', () => {
describe('features', () => {
let features: Features

beforeEach(async () => {
await standalone(props)
beforeEach(() => {
standalone(props)
features = stubServer.getCall(0).args[0]
})

Expand Down
52 changes: 29 additions & 23 deletions runtimes/runtimes/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function setupCrashMonitoring(telemetryEmitter?: (metric: MetricEvent) => void)
* @param props.servers The list of servers to initialize and run
* @returns
*/
export const standalone = async (props: RuntimeProps) => {
export const standalone = (props: RuntimeProps) => {
handleVersionArgument(props.version)

const lspConnection = createConnection(ProposedFeatures.all)
Expand All @@ -179,42 +179,48 @@ export const standalone = async (props: RuntimeProps) => {

let auth: Auth
let chat: Chat
await initializeAuth()
initializeAuth()

// Initialize Auth service
async function initializeAuth() {
function initializeAuth() {
if (shouldWaitForEncryptionKey()) {
// Before starting the runtime, accept encryption initialization details
// directly from the destination for standalone runtimes.
// Contract: Only read up to (and including) the first newline (\n).
try {
const encryptionDetails: EncryptionInitialization = await readEncryptionDetails(process.stdin)
validateEncryptionDetails(encryptionDetails)
lspConnection.console.info('Runtime: Initializing runtime with encryption')
auth = new Auth(lspConnection, lspRouter, encryptionDetails.key, encryptionDetails.mode)
chat = new EncryptedChat(lspConnection, encryptionDetails.key, encryptionDetails.mode)
await initializeRuntime(encryptionDetails.key)
} catch (error) {
console.error(error)
// arbitrary 5 second timeout to ensure console.error flushes before process exit
// note: webpacked version may output exclusively to stdout, not stderr.
setTimeout(() => {
process.exit(10)
}, 5000)
}
readEncryptionDetails(process.stdin)
.then(
(encryptionDetails: EncryptionInitialization) => {
validateEncryptionDetails(encryptionDetails)
lspConnection.console.info('Runtime: Initializing runtime with encryption')
auth = new Auth(lspConnection, lspRouter, encryptionDetails.key, encryptionDetails.mode)
chat = new EncryptedChat(lspConnection, encryptionDetails.key, encryptionDetails.mode)
initializeRuntime(encryptionDetails.key)
},
error => {
console.error(error)
// arbitrary 5 second timeout to ensure console.error flushes before process exit
// note: webpacked version may output exclusively to stdout, not stderr.
setTimeout(() => {
process.exit(10)
}, 5000)
}
)
.catch((error: Error) => {
console.error('Error at runtime initialization:', error.message)
})
} else {
lspConnection.console.info('Runtime: Initializing runtime without encryption')
auth = new Auth(lspConnection, lspRouter)

await initializeRuntime()
initializeRuntime()
}
}

// Initialize the LSP connection based on the supported LSP capabilities
// TODO: make this dependent on the actual requirements of the
// capabilities parameter.

async function initializeRuntime(encryptionKey?: string) {
function initializeRuntime(encryptionKey?: string) {
const documents = new TextDocuments(TextDocument)
// Set up telemetry over LSP
const telemetry: Telemetry = {
Expand Down Expand Up @@ -367,8 +373,6 @@ export const standalone = async (props: RuntimeProps) => {

const agent = newAgent()

const v3ProxyConfig = await sdkProxyConfigManager.getV3ProxyConfig()

// Initialize every Server
const disposables = props.servers.map(s => {
// Create LSP server representation that holds internal server state
Expand Down Expand Up @@ -455,7 +459,9 @@ export const standalone = async (props: RuntimeProps) => {
current_config: P
): T => {
try {
const requestHandler = isExperimentalProxy ? v3ProxyConfig : makeProxyConfigv3Standalone(workspace)
const requestHandler = isExperimentalProxy
? sdkProxyConfigManager.getV3ProxyConfig()
: makeProxyConfigv3Standalone(workspace)

logging.log(`Using ${isExperimentalProxy ? 'experimental' : 'standard'} proxy util`)

Expand Down
19 changes: 10 additions & 9 deletions runtimes/runtimes/util/standalone/experimentalProxyUtil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('ProxyConfigManager', function () {

beforeEach(() => {
originalEnv = { ...process.env }
process.env = {}

telemetryStub = {
emitMetric: sinon.stub(),
Expand All @@ -71,16 +72,16 @@ describe('ProxyConfigManager', function () {
mockFs.restore()
})

it('should cache and return same V3 config', async () => {
const config1 = await proxyManager.getV3ProxyConfig()
const config2 = await proxyManager.getV3ProxyConfig()
it('should cache and return same V3 config', () => {
const config1 = proxyManager.getV3ProxyConfig()
const config2 = proxyManager.getV3ProxyConfig()

assert.strictEqual(config1, config2)
})

it('should use secure agent for V3 config', async () => {
it('should use secure agent for V3 config', () => {
const getAgentSpy = sinon.spy(proxyManager, 'getSecureAgent')
await proxyManager.getV3ProxyConfig()
proxyManager.getV3ProxyConfig()

assert(getAgentSpy.calledOnce)
})
Expand Down Expand Up @@ -263,8 +264,8 @@ describe('ProxyConfigManager', function () {
readLinuxCertificatesStub.returns(sysCerts)
})

it('should create HttpsAgent when no proxy set', async () => {
const agent = await proxyManager.createSecureAgent()
it('should create HttpsAgent when no proxy set', () => {
const agent = proxyManager.createSecureAgent()

assert(agent instanceof HttpsAgent)
assert.strictEqual((agent as HttpsAgent).options.rejectUnauthorized, true)
Expand All @@ -283,9 +284,9 @@ describe('ProxyConfigManager', function () {
)
})

it('should create HttpsProxyAgent when proxy set', async () => {
it('should create HttpsProxyAgent when proxy set', () => {
process.env.HTTPS_PROXY = 'https://proxy'
const agent = await proxyManager.createSecureAgent()
const agent = proxyManager.createSecureAgent()

assert(agent instanceof HttpsProxyAgent)
assert.strictEqual(agent.options.rejectUnauthorized, true)
Expand Down
16 changes: 8 additions & 8 deletions runtimes/runtimes/util/standalone/experimentalProxyUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ export class ProxyConfigManager {

constructor(private readonly telemetry: Telemetry) {}

async getSecureAgent(): Promise<HttpsAgent | HttpsProxyAgent> {
getSecureAgent(): HttpsAgent | HttpsProxyAgent {
if (!this.cachedAgent) {
this.cachedAgent = await this.createSecureAgent()
this.cachedAgent = this.createSecureAgent()
}
return this.cachedAgent
}

public async getV3ProxyConfig(): Promise<NodeHttpHandler> {
public getV3ProxyConfig(): NodeHttpHandler {
if (!this.cachedV3Config) {
const agent = await this.getSecureAgent()
const agent = this.getSecureAgent()
this.cachedV3Config = new NodeHttpHandler({
httpAgent: agent,
httpsAgent: agent,
Expand All @@ -57,12 +57,12 @@ export class ProxyConfigManager {
return undefined
}

private static async getSystemProxy(): Promise<string | undefined> {
private static getSystemProxy(): string | undefined {
switch (process.platform) {
case 'darwin':
return getMacSystemProxy()?.proxyUrl
case 'win32':
return (await getWindowsSystemProxy())?.proxyUrl
return getWindowsSystemProxy()?.proxyUrl
default:
return undefined
}
Expand Down Expand Up @@ -166,7 +166,7 @@ export class ProxyConfigManager {
*
* @returns {HttpsAgent | HttpsProxyAgent}
*/
async createSecureAgent(): Promise<HttpsAgent | HttpsProxyAgent> {
createSecureAgent(): HttpsAgent | HttpsProxyAgent {
const certs = this.getCertificates()
const agentOptions = {
ca: certs,
Expand All @@ -184,7 +184,7 @@ export class ProxyConfigManager {
}

// Fall back to OS auto‑detect (HTTP or HTTPS only)
const sysProxyUrl = await ProxyConfigManager.getSystemProxy()
const sysProxyUrl = ProxyConfigManager.getSystemProxy()
if (sysProxyUrl) {
this.emitProxyMetric('AutoDetect', certs.length, sysProxyUrl)
return new HttpsProxyAgent({ ...agentOptions, proxy: sysProxyUrl })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import * as assert from 'assert'
import { getWindowsSystemProxy } from './getWindowsProxySettings'

describe('getWindowsSystemProxy', () => {
it('can get the Windows system proxy', async function () {
it('can get the Windows system proxy', function () {
if (process.platform !== 'win32') return this.skip()

const result = await getWindowsSystemProxy()
const result = getWindowsSystemProxy()
assert.ok(result === undefined || (typeof result === 'object' && result !== null))

if (result) {
Expand All @@ -18,15 +18,15 @@ describe('getWindowsSystemProxy', () => {
}
})

it('returns undefined on non-Windows platforms', async function () {
it('returns undefined on non-Windows platforms', function () {
if (process.platform === 'win32') return this.skip()

const result = await getWindowsSystemProxy()
const result = getWindowsSystemProxy()
assert.strictEqual(result, undefined)
})

it('handles registry access failure gracefully', async function () {
it('handles registry access failure gracefully', function () {
// This test verifies the function doesn't throw
assert.doesNotThrow(async () => await getWindowsSystemProxy())
assert.doesNotThrow(() => getWindowsSystemProxy())
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,36 @@
* Based on windows-system-proxy 1.0.0 (Apache-2.0). Modified for synchronous use
* https://github.com/httptoolkit/windows-system-proxy/blob/main/src/index.ts
*/
import winreg from 'winreg'
import { enumerateValues, HKEY, RegistryValue } from 'registry-js'

export interface ProxyConfig {
proxyUrl: string
noProxy: string[]
}

const KEY_PROXY_ENABLE = 'ProxyEnable'
const KEY_PROXY_SERVER = 'ProxyServer'
const KEY_PROXY_OVERRIDE = 'ProxyOverride'

type WindowsProxyRegistryKeys = {
proxyEnable: string | undefined
proxyServer: string | undefined
proxyOverride: string | undefined
}

function readWindowsRegistry(): Promise<WindowsProxyRegistryKeys> {
return new Promise((resolve, reject) => {
const regKey = new winreg({
hive: winreg.HKCU,
key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
})

regKey.values((err: Error, items: winreg.RegistryItem[]) => {
if (err) {
console.warn('', err.message)
resolve({
proxyEnable: undefined,
proxyServer: undefined,
proxyOverride: undefined,
})
return
}

const results: Record<string, string> = {}

items.forEach((item: winreg.RegistryItem) => {
results[item.name] = item.value as string
})

resolve({
proxyEnable: results[KEY_PROXY_ENABLE],
proxyServer: results[KEY_PROXY_SERVER],
proxyOverride: results[KEY_PROXY_OVERRIDE],
})
})
})
}
export function getWindowsSystemProxy(): ProxyConfig | undefined {
const proxyValues = enumerateValues(
HKEY.HKEY_CURRENT_USER,
'Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'
)
console.debug(`Retrieved ${proxyValues.length} registry values for proxy settings`)

export async function getWindowsSystemProxy(): Promise<ProxyConfig | undefined> {
const registryValues = await readWindowsRegistry()
const proxyEnabled = registryValues.proxyEnable
const proxyServer = registryValues.proxyServer
const proxyOverride = registryValues.proxyOverride
const proxyEnabled = getValue(proxyValues, 'ProxyEnable')
const proxyServer = getValue(proxyValues, 'ProxyServer')

if (!proxyEnabled || !proxyServer) {
if (!proxyEnabled || !proxyEnabled.data || !proxyServer || !proxyServer.data) {
console.debug('Proxy not enabled or server not configured')
return undefined
}

// Build noProxy list from ProxyOverride (semicolon-separated, with <local> → localhost,127.0.0.1,::1)
const proxyOverride = getValue(proxyValues, 'ProxyOverride')?.data
const noProxy = (proxyOverride ? (proxyOverride as string).split(';') : []).flatMap(host =>
host === '<local>' ? ['localhost', '127.0.0.1', '::1'] : [host]
)

// Parse proxy configuration which can be in multiple formats
const proxyConfigString = proxyServer
const proxyConfigString = proxyServer.data as string

if (proxyConfigString.startsWith('http://') || proxyConfigString.startsWith('https://')) {
console.debug('Using full URL format proxy configuration')
Expand Down Expand Up @@ -115,3 +78,5 @@ export async function getWindowsSystemProxy(): Promise<ProxyConfig | undefined>
}
}
}

const getValue = (values: readonly RegistryValue[], name: string) => values.find(value => value?.name === name)