Skip to content

Commit

Permalink
Network: respect NO_PROXY settings (#6555)
Browse files Browse the repository at this point in the history
Before JetBrains would pass proxy settings to the agent using HTTP_PROXY
and HTTPS_PROXY. However these settings would not allow creating of a
"proxyless" http agent since they can be used directly by node
libraries. Instead now settings are passed under the
CODY_NODE_DEFAULT_PROXY (which falls back to HTTP_PROXY and
HTTPS_PROXY).

In addition there's a new CODY_NODE_NO_PROXY environment variable that
mirrors the NO_PROXY node environment variable. This now allows ignoring
of proxy settings for particular endpoints.

Lastly, before the NetworkDiagnostics would output extremely verbose
logging. Including error logs on cancelled requests. The logging could
also potentially leave dangling requests/sockets as event handlers
weren't properly cleaned up. By moving to a weak-map for timing events
and cleaning up of event handlers this is now prevented. The logging
output has also been made a lot more concise and readable.

![CleanShot 2025-01-08 at 12 34
43@2x](https://github.com/user-attachments/assets/7e07b8e5-1746-46af-b1e7-6548bb8fd4e0)


## Test plan

- Added unit tests to assert the correct re-use of agents depending on
configuration and the new CODY_NODE_NO_PROXY setting
- Manually verified working in JetBrains and VSCode
  • Loading branch information
RXminuS authored Jan 8, 2025
1 parent d597a7e commit ae37d00
Show file tree
Hide file tree
Showing 6 changed files with 586 additions and 167 deletions.
44 changes: 32 additions & 12 deletions jetbrains/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ private constructor(
val processBuilder = ProcessBuilder(command)
if (java.lang.Boolean.getBoolean("cody.accept-non-trusted-certificates-automatically") ||
ConfigUtil.getShouldAcceptNonTrustedCertificatesAutomatically()) {
processBuilder.environment()["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"
processBuilder.environment()["CODY_NODE_TLS_REJECT_UNAUTHORIZED"] = "0"
}

if (java.lang.Boolean.getBoolean("cody.log-events-to-connected-instance-only")) {
Expand All @@ -212,18 +212,34 @@ private constructor(

val proxy = HttpConfigurable.getInstance()
val proxyUrl = proxy.PROXY_HOST + ":" + proxy.PROXY_PORT
val proxyProto =
if (proxy.PROXY_TYPE_IS_SOCKS) {
"socks:"
} else {
"http:"
}
val proxyAuth =
if (proxy.PROXY_AUTHENTICATION) {
// TODO: we should maybe prompt the user here instead?
val password = proxy.plainProxyPassword
val username = proxy.proxyLogin
if (!password.isNullOrEmpty() && !username.isNullOrEmpty()) {
"${username}:${password}@"
} else {
""
}
} else {
""
}
if (proxy.USE_HTTP_PROXY) {
if (proxy.PROXY_TYPE_IS_SOCKS) {
processBuilder.environment()["HTTP_PROXY"] = "socks://$proxyUrl"
} else {
processBuilder.environment()["HTTP_PROXY"] = "http://$proxyUrl"
processBuilder.environment()["HTTPS_PROXY"] = "http://$proxyUrl"
if (!proxy.PROXY_EXCEPTIONS.isNullOrEmpty()) {
processBuilder.environment()["CODY_NODE_NO_PROXY"] = proxy.PROXY_EXCEPTIONS
}
processBuilder.environment()["CODY_NODE_DEFAULT_PROXY"] =
"${proxyProto}//${proxyAuth}${proxyUrl}"
}

logger.info("starting Cody agent ${command.joinToString(" ")}")
logger.info(
"Cody agent proxyUrl ${proxyUrl} PROXY_TYPE_IS_SOCKS ${proxy.PROXY_TYPE_IS_SOCKS}")

val process =
processBuilder
Expand Down Expand Up @@ -292,15 +308,18 @@ private constructor(
run {
gsonBuilder
// emit `null` instead of leaving fields undefined because Cody
// VSC has many `=== null` checks that return false for undefined fields.
// VSC has many `=== null` checks that return false for
// undefined fields.
.serializeNulls()
// TODO: Once all protocols have migrated we can remove these legacy enum
// TODO: Once all protocols have migrated we can remove these
// legacy enum
// conversions
.registerTypeAdapter(URI::class.java, uriDeserializer)
.registerTypeAdapter(URI::class.java, uriSerializer)

ProtocolTypeAdapters.register(gsonBuilder)
// This ensures that by default all enums are always serialized to their string
// This ensures that by default all enums are always serialized to their
// string
// equivalents
gsonBuilder.registerTypeAdapterFactory(EnumTypeAdapterFactory())
}
Expand Down Expand Up @@ -365,7 +384,8 @@ private constructor(
binaryTarget?.toFile()?.deleteOnExit()
token.onFinished {
// Important: delete the file from disk after the process exists
// Ideally, we should eventually replace this temporary file with a permanent location
// Ideally, we should eventually replace this temporary file with a permanent
// location
// in the plugin directory.
Files.deleteIfExists(binaryTarget)
}
Expand Down
5 changes: 5 additions & 0 deletions lib/shared/src/configuration/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export const cenv = defineEnvBuilder({
*/
CODY_NODE_DEFAULT_PROXY: proxyStringWithNodeFallback,

/**
* Endpoints for which the proxy should be ignored
*/
CODY_NODE_NO_PROXY: (envValue, _) => str(envValue) || str(getEnv('NO_PROXY')),

/**
* A setting that is similar (and falls back to) Node's NODE_TLS_REJECT_UNAUTHORIZED setting
*/
Expand Down
5 changes: 4 additions & 1 deletion vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ This is a log of all notable changes to Cody for VS Code.
- autoedit: address dogfooding feedback [pull/6454](https://github.com/sourcegraph/cody/pull/6454)
- feat(audoedit): implement basic analytics logger [pull/6430](https://github.com/sourcegraph/cody/pull/6430)
- feat(onebox): Use new prompt editor when onebox is enabled [pull/6288](https://github.com/sourcegraph/cody/pull/6288)

- feat(network): Support for NO_PROXY (CODY_NODE_NO_PROXY) environment variable [pull/6555](https://github.com/sourcegraph/cody/pull/6555)

### Fixed
- feat(logging): Add interactionId to header of Cody Client requests (CODY-4117) [pull/6450](https://github.com/sourcegraph/cody/pull/6450)
Expand All @@ -36,13 +36,16 @@ This is a log of all notable changes to Cody for VS Code.
- chore(onebox/telemetry): add `billingMetadata` [pull/6426](https://github.com/sourcegraph/cody/pull/6426)
- fix(audoedit): fix renderer testing command [pull/6408](https://github.com/sourcegraph/cody/pull/6408)
- chore/release: Bump package version and update changelog for 1.52 [pull/6414](https://github.com/sourcegraph/cody/pull/6414)
- fix(logging): removed unecessary logging when requests are aborted [pull/6555](https://github.com/sourcegraph/cody/pull/6555)
- fix(network): removed dangling request handlers on network requests which could potentially cause memory leaks [pull/6555](https://github.com/sourcegraph/cody/pull/6555)

### Changed
- feat(prompt-editor): Add new ProseMirror-based implementation [pull/6272](https://github.com/sourcegraph/cody/pull/6272)
- refactor(user-menu): improve display of user menu [pull/6389](https://github.com/sourcegraph/cody/pull/6389)
- Use omnibox ff for intent detector [pull/6419](https://github.com/sourcegraph/cody/pull/6419)
- Enable repo boost for inactive editor [pull/6443](https://github.com/sourcegraph/cody/pull/6443)
- include symbol matches in search results [pull/6441](https://github.com/sourcegraph/cody/pull/6441)
- improved network logging with less verbose output [pull/6555](https://github.com/sourcegraph/cody/pull/6555)

## 1.54.0

Expand Down
145 changes: 145 additions & 0 deletions vscode/src/net/DelegatingAgent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as https from 'node:https'
import { HttpsProxyAgent } from 'hpagent'
import { describe, expect, test, vi } from 'vitest'
import { DelegatingAgent } from './DelegatingAgent'

vi.mock('@sourcegraph/cody-shared', async () => {
const actual = await vi.importActual('@sourcegraph/cody-shared')
return {
...actual,
globalAgentRef: {
isSet: false,
agent: undefined,
},
}
})

// Mock Socket as before
vi.mock('node:net', () => ({
Socket: vi.fn(() => ({
connect: vi.fn(),
end: vi.fn(),
destroy: vi.fn(),
})),
}))

// Mock TLS as before
vi.mock('node:tls', () => ({
connect: vi.fn(() => ({
end: vi.fn(),
destroy: vi.fn(),
})),
rootCertificates: [],
}))

// Mock the entire https.request
vi.mock('node:https', async () => {
const actual = await vi.importActual('node:https')
return {
...actual,
request: vi.fn(() => ({
on: vi.fn(),
end: vi.fn(),
destroy: vi.fn(),
})),
}
})

describe.sequential('DelegatingAgent caching', () => {
test('reuses cached agents for identical requests', async () => {
const agent = await DelegatingAgent.initialize({})

const requestOptions = {
host: 'api.example.com',
port: 443,
protocol: 'https:',
method: 'GET',
path: '/',
headers: {},
secureEndpoint: true,
}

// First connection should create a new agent
const agent1 = await agent.connect(https.request(requestOptions), requestOptions)

// Second connection to same endpoint should return cached agent
const agent2 = await agent.connect(https.request(requestOptions), requestOptions)

// Verify same agent instance is returned
expect(agent1).toBe(agent2)

// Different endpoint should create new agent
const differentRequestOptions = {
...requestOptions,
host: 'different.example.com',
}
const agent3 = await agent.connect(
https.request(differentRequestOptions),
differentRequestOptions
)

expect(agent3).not.toBe(agent1)

agent.dispose()
})

test('respects CODY_NODE_DEFAULT_PROXY and CODY_NODE_NO_PROXY', async () => {
vi.stubEnv('CODY_NODE_DEFAULT_PROXY', 'http://proxy.example.com:8080')
vi.stubEnv('CODY_NODE_NO_PROXY', 'localhost,internal.example.com,*.other.com')

const agent = await DelegatingAgent.initialize({})

// Test request that should be proxied
const proxyRequest = {
host: 'api.example.com',
port: 443,
protocol: 'https:',
method: 'GET',
path: '/',
headers: {},
secureEndpoint: true,
}

// Test request that should not be proxied (matches NO_PROXY)
const noProxyRequest = {
host: 'internal.example.com',
port: 443,
protocol: 'https:',
method: 'GET',
path: '/',
headers: {},
secureEndpoint: true,
}

const wildcardNoProxyRequest = {
host: 'subdomain.other.com',
port: 443,
protocol: 'https:',
method: 'GET',
path: '/subpath',
headers: {},
secureEndpoint: true,
}

const proxiedAgent = await agent.connect(https.request(proxyRequest), proxyRequest)
const noProxiedAgent = await agent.connect(https.request(noProxyRequest), noProxyRequest)
const wildcardNoProxyAgent = await agent.connect(
https.request(wildcardNoProxyRequest),
wildcardNoProxyRequest
)

// Verify different agents are used
expect(proxiedAgent).instanceOf(HttpsProxyAgent)

// Check that these are normal https Agents (e.g. not a subclass)
expect(noProxiedAgent.constructor).toBe(https.Agent)
expect(wildcardNoProxyAgent.constructor).toBe(https.Agent)

// They shouldn't use the same agent
expect(noProxiedAgent).not.toBe(wildcardNoProxyAgent)

// Clean up
agent.dispose()
vi.unstubAllEnvs()
})
})
Loading

0 comments on commit ae37d00

Please sign in to comment.