Skip to content

Commit cb40d5c

Browse files
azureutils: honor VS Code proxy settings in Azure clients (#1536)
The Azure SDK's built-in proxy policy reads only proxy environment variables (HTTPS_PROXY/HTTP_PROXY/NO_PROXY) and never consults VS Code's `http.proxy` / `http.proxyStrictSSL` / `http.noProxy` settings. Its proxy agent also discards TLS settings. On corporate networks that require an explicit VS Code proxy, ARM and generic requests therefore bypass the proxy and can fail. Add a shared ProxyAgentPolicy and two exported helpers that resolve proxy/TLS configuration from VS Code's http settings, falling back to the standard proxy env vars: - getProxyAgent(requestUrl): an http/https Agent for clients that accept a raw agent (also carries an http.proxyStrictSSL:false TLS override). - getProxySettings(requestUrl): a ProxySettings object for pipeline-based Azure SDK data-plane clients that accept proxyOptions (e.g. Storage's BlobServiceClient/ShareServiceClient); no TLS override since proxyOptions cannot express one. The policy is wired into addAzExtPipeline so it applies to createGenericClient, createAzureClient, and createAzureSubscriptionClient. TLS validation is only relaxed when http.proxyStrictSSL is explicitly false, and http.proxySupport: off makes the policy a no-op. CA trust continues to rely on NODE_EXTRA_CA_CERTS (documented separately in vscode-azureresourcegroups#1537), which Node honors globally including through proxy agents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 97c9d9a commit cb40d5c

7 files changed

Lines changed: 825 additions & 1 deletion

File tree

azure/index.d.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { StorageAccount } from '@azure/arm-storage';
1313
import { type StorageManagementClient } from '@azure/arm-storage';
1414
import type { CommonClientOptions, ServiceClient, ServiceClientOptions } from '@azure/core-client';
1515
import type { PagedAsyncIterableIterator } from '@azure/core-paging';
16-
import type { PipelineRequestOptions, PipelineResponse } from '@azure/core-rest-pipeline';
16+
import type { PipelineRequestOptions, PipelineResponse, ProxySettings as ProxySettingsBase } from '@azure/core-rest-pipeline';
1717
import type { Environment } from '@azure/ms-rest-azure-env';
1818
import type { AzExtParentTreeItem, AzExtServiceClientCredentials, AzExtServiceClientCredentialsT2, AzExtTreeItem, AzureNameStep, AzureWizardExecuteStep, AzureWizardExecuteStepWithActivityOutput, AzureWizardPromptStep, IActionContext, IAzureNamingRules, IAzureQuickPickItem, IAzureQuickPickOptions, IAzureUserInput, IRelatedNameWizardContext, ISubscriptionActionContext, ISubscriptionContext, IWizardOptions, TreeElementBase, UIExtensionVariables } from '@microsoft/vscode-azext-utils';
1919
import type { AzureSubscription } from '@microsoft/vscode-azureresources-api';
@@ -556,6 +556,36 @@ export function createAuthorizationManagementClient(context: AzExtClientContext)
556556
export type AzExtRequestPrepareOptions = PipelineRequestOptions & { rejectUnauthorized?: boolean }
557557
export type AzExtPipelineResponse = PipelineResponse & { parsedBody?: any }
558558

559+
/**
560+
* Returns an `http`/`https` `Agent` configured from VS Code's `http.proxy` / `http.proxyStrictSSL`
561+
* settings (falling back to the standard `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY` environment
562+
* variables) for the given request URL, or `undefined` when no proxy or TLS override applies.
563+
*
564+
* Azure clients created via this package already apply this configuration automatically. This
565+
* helper is exported so extensions can apply the same proxy behavior to their own HTTP clients
566+
* that don't go through the Azure SDK pipeline.
567+
*/
568+
export declare function getProxyAgent(requestUrl: string): import('http').Agent | undefined;
569+
570+
/**
571+
* Proxy configuration (host/port/username/password) accepted by pipeline-based Azure SDK clients
572+
* via their `proxyOptions`/`proxySettings` option. Re-exported from `@azure/core-rest-pipeline`.
573+
*/
574+
export type ProxySettings = ProxySettingsBase;
575+
576+
/**
577+
* Returns a `ProxySettings` object configured from VS Code's `http.proxy` setting (falling back to
578+
* the standard `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY` environment variables) for the given request
579+
* URL, or `undefined` when no proxy applies (unset, `http.noProxy` bypass, or
580+
* `http.proxySupport: off`).
581+
*
582+
* Intended for pipeline-based Azure SDK data-plane clients that accept a `proxyOptions` option
583+
* (e.g. Storage's `BlobServiceClient`/`ShareServiceClient`) rather than a raw `http.Agent`. Unlike
584+
* {@link getProxyAgent}, it does not carry a TLS (`http.proxyStrictSSL: false`) override, since
585+
* `proxyOptions` cannot express one.
586+
*/
587+
export declare function getProxySettings(requestUrl: string): ProxySettings | undefined;
588+
559589
/**
560590
* Send request with a timeout specified. Retries are disabled (because retrying could take a lot longer)
561591
* and `addStatusCodePolicy` is enabled; both override any matching fields in `genericClientOptions`.

azure/package-lock.json

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

azure/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
"@microsoft/vscode-azext-azureauth": "^6.1.0-alpha.2",
5151
"@microsoft/vscode-azext-utils": "^4.1.1",
5252
"@microsoft/vscode-azureresources-api": "^3.1.0",
53+
"http-proxy-agent": "^7.0.2",
54+
"https-proxy-agent": "^7.0.6",
5355
"semver": "^7.7.4"
5456
},
5557
"peerDependencies": {

azure/src/createAzureClient.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Agent as HttpsAgent } from 'https';
1212
import * as vscode from "vscode";
1313
import * as types from '../index';
1414
import { FeedMirrorPolicy } from './utils/FeedMirrorPolicy';
15+
import { ProxyAgentPolicy } from './utils/ProxyAgentPolicy';
1516
import { parseJson, removeBom } from './utils/parseJson';
1617

1718
export type InternalAzExtClientContext = ISubscriptionActionContext | [IActionContext, ISubscriptionContext | AzExtTreeItem];
@@ -175,6 +176,16 @@ function addAzExtPipeline(context: IActionContext, pipeline: Pipeline, endpoint?
175176

176177
pipeline.addPolicy(new AllowInsecureConnectionPolicy());
177178

179+
// Apply VS Code's `http.proxy` / `http.proxyStrictSSL` / `http.noProxy` settings to requests.
180+
// Runs before the SDK's built-in proxy policy (which only reads proxy env vars). When
181+
// `http.proxySupport` is `off`, this policy no-ops; env-var proxies still flow through the
182+
// built-in policy, matching VS Code's default extension-host behavior (`off` opts out of VS
183+
// Code's setting-based proxy handling, not of the standard proxy environment variables).
184+
// Known limitation: `http.noProxy` only bypasses proxies this policy applies. When a proxy comes
185+
// solely from an env var (e.g. `HTTPS_PROXY`) and `http.proxy` is unset, the built-in policy still
186+
// proxies it because it consults only `NO_PROXY`, not VS Code's `http.noProxy` list.
187+
ProxyAgentPolicy.addIfNeeded(pipeline);
188+
178189
if (bearerChallengePolicy) {
179190
pipeline.addPolicy(bearerChallengePolicy, { phase: 'Sign' });
180191
}

azure/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export * from './tree/RoleDefinitionsItem';
1313
export * from './tree/SubscriptionTreeItemBase';
1414
export * from './utils/createPortalUri';
1515
export * from './utils/parseAzureResourceId';
16+
export { getProxyAgent, getProxySettings } from './utils/ProxyAgentPolicy';
17+
export type { ProxySettings } from '@azure/core-rest-pipeline';
1618
export * from './utils/setupAzureLogger';
1719
export * from './utils/uiUtils';
1820
export { LocationCache } from './wizard/LocationCache';
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { getDefaultProxySettings, proxyPolicyName, type Pipeline, type PipelinePolicy, type PipelineRequest, type PipelineResponse, type ProxySettings, type SendRequest } from '@azure/core-rest-pipeline';
7+
import { HttpProxyAgent } from 'http-proxy-agent';
8+
import { Agent as HttpsAgent } from 'https';
9+
import { HttpsProxyAgent } from 'https-proxy-agent';
10+
import * as vscode from 'vscode';
11+
12+
/**
13+
* Proxy/TLS configuration resolved from VS Code's `http.*` settings.
14+
*/
15+
interface HttpProxyConfig {
16+
/**
17+
* `false` when `http.proxySupport` is `off`, meaning the user has opted out of proxy handling
18+
* and callers should apply no proxy or TLS override at all.
19+
*/
20+
proxySupportEnabled: boolean;
21+
/** Value of the `http.proxy` setting, or `undefined` when unset. */
22+
proxyUrl?: string;
23+
/** Value of the `http.proxyStrictSSL` setting. Defaults to `true` (validate certificates). */
24+
strictSSL: boolean;
25+
/** Value of the `http.noProxy` setting merged with the `NO_PROXY` env var. */
26+
noProxy: string[];
27+
}
28+
29+
/**
30+
* Reads proxy-related configuration from VS Code's `http` settings, merging `http.noProxy`
31+
* with the standard `NO_PROXY`/`no_proxy` environment variable.
32+
*/
33+
function getHttpProxyConfig(): HttpProxyConfig {
34+
const config = vscode.workspace.getConfiguration('http');
35+
// `http.proxySupport: 'off'` is the user's kill-switch for proxy support; callers short-circuit
36+
// to a no-op when it's set. Default is `'override'`, matching VS Code's own default behavior.
37+
const proxySupportEnabled = config.get<string>('proxySupport', 'override') !== 'off';
38+
const proxyUrl = config.get<string>('proxy')?.trim() || undefined;
39+
// Default to `true`: only relax TLS validation when the user has explicitly opted out.
40+
const strictSSL = config.get<boolean>('proxyStrictSSL', true);
41+
const noProxy = [...(config.get<string[]>('noProxy') ?? []), ...parseNoProxyEnv()]
42+
.map((entry) => entry.trim())
43+
.filter((entry) => entry.length > 0);
44+
return { proxySupportEnabled, proxyUrl, strictSSL, noProxy };
45+
}
46+
47+
function parseNoProxyEnv(): string[] {
48+
const value = process.env.NO_PROXY ?? process.env.no_proxy;
49+
return value ? value.split(',') : [];
50+
}
51+
52+
/**
53+
* Resolves the proxy URL to use, preferring VS Code's `http.proxy` setting and falling back to
54+
* the standard proxy environment variables (case-insensitive). The env-var fallback is
55+
* protocol-aware: `http:` requests prefer `HTTP_PROXY` and `https:` requests prefer `HTTPS_PROXY`,
56+
* matching common tooling (curl, etc.), with `ALL_PROXY` as a cross-protocol fallback.
57+
*/
58+
function resolveProxyUrl(config: HttpProxyConfig, isTls: boolean): string | undefined {
59+
if (config.proxyUrl) {
60+
return config.proxyUrl;
61+
}
62+
const protocolProxy = isTls
63+
? (process.env.HTTPS_PROXY ?? process.env.https_proxy)
64+
: (process.env.HTTP_PROXY ?? process.env.http_proxy);
65+
return protocolProxy
66+
?? process.env.ALL_PROXY ?? process.env.all_proxy
67+
?? undefined;
68+
}
69+
70+
/**
71+
* Returns `true` for the only schemes we proxy (`http:` and `https:`). Other schemes (e.g. `ftp:`,
72+
* `file:`) are left untouched.
73+
*/
74+
function isProxyableProtocol(protocol: string): boolean {
75+
return protocol === 'http:' || protocol === 'https:';
76+
}
77+
78+
/**
79+
* Returns `true` when `host` matches an entry in `noProxyList` and should therefore bypass the
80+
* proxy. An entry that starts with `.` (or `*.`) matches the domain and any subdomain; otherwise
81+
* an exact host match is required.
82+
*/
83+
export function isHostBypassed(host: string, noProxyList: string[]): boolean {
84+
if (!host || noProxyList.length === 0) {
85+
return false;
86+
}
87+
const lowerHost = host.toLowerCase();
88+
for (const rawPattern of noProxyList) {
89+
// Treat "*", "*.foo.com" and ".foo.com" as domain suffixes.
90+
let pattern = rawPattern.toLowerCase();
91+
if (pattern === '*') {
92+
return true;
93+
}
94+
if (pattern.startsWith('*')) {
95+
pattern = pattern.slice(1);
96+
}
97+
if (pattern.startsWith('.')) {
98+
if (lowerHost === pattern.slice(1) || lowerHost.endsWith(pattern)) {
99+
return true;
100+
}
101+
} else if (lowerHost === pattern) {
102+
return true;
103+
}
104+
}
105+
return false;
106+
}
107+
108+
// Caches so repeated requests reuse a single Agent (and its socket pool) per configuration.
109+
const proxyAgentCache = new Map<string, HttpsProxyAgent<string> | HttpProxyAgent<string>>();
110+
let insecureTlsAgent: HttpsAgent | undefined;
111+
112+
function getProxyAgentForUrl(proxyUrl: string, isTls: boolean, rejectUnauthorized: boolean): HttpsProxyAgent<string> | HttpProxyAgent<string> {
113+
const key = `${isTls ? 'https' : 'http'}|${proxyUrl}|${rejectUnauthorized}`;
114+
let agent = proxyAgentCache.get(key);
115+
if (!agent) {
116+
if (isTls) {
117+
const httpsAgent = new HttpsProxyAgent(proxyUrl, { rejectUnauthorized });
118+
// https-proxy-agent stores `rejectUnauthorized` only in `connectOpts` (the TLS handshake
119+
// to the proxy) and resets the agent's `.options` to `{ path: undefined }`. Node merges
120+
// the agent's `.options`, not `connectOpts`, into the destination TLS handshake inside the
121+
// CONNECT tunnel, so the flag would otherwise never reach the server certificate check.
122+
// Put it on `.options` so a `proxyStrictSSL: false` override actually relaxes destination
123+
// TLS (matching the no-proxy insecure agent).
124+
httpsAgent.options = { ...httpsAgent.options, rejectUnauthorized };
125+
agent = httpsAgent;
126+
} else {
127+
agent = new HttpProxyAgent(proxyUrl);
128+
}
129+
proxyAgentCache.set(key, agent);
130+
}
131+
return agent;
132+
}
133+
134+
function getInsecureTlsAgent(): HttpsAgent {
135+
return (insecureTlsAgent ??= new HttpsAgent({ keepAlive: true, rejectUnauthorized: false }));
136+
}
137+
138+
/**
139+
* Result of resolving a request URL against VS Code's proxy configuration, shared by
140+
* {@link getProxyAgent} and {@link getProxySettings}.
141+
*/
142+
interface ResolvedProxy {
143+
/** The parsed request URL. */
144+
url: URL;
145+
/** The resolved proxy configuration. */
146+
config: HttpProxyConfig;
147+
/** The proxy URL to use, or `undefined` when the host is bypassed or no proxy is configured. */
148+
proxyUrl?: string;
149+
}
150+
151+
/**
152+
* Shared proxy resolution: parses `requestUrl`, ignores non-http(s) schemes, honors
153+
* `http.proxySupport: off` (returns `undefined`), applies the `http.noProxy` bypass, and resolves
154+
* the proxy URL from `http.proxy` with the standard proxy environment variables as a fallback.
155+
* Returns `undefined` when the URL is invalid, its scheme is not proxyable, or proxy support is off.
156+
*/
157+
function resolveProxyForRequest(requestUrl: string): ResolvedProxy | undefined {
158+
let url: URL;
159+
try {
160+
url = new URL(requestUrl);
161+
} catch {
162+
return undefined;
163+
}
164+
165+
// Only http(s) requests are proxied; leave other schemes (e.g. `ftp:`, `file:`) untouched.
166+
if (!isProxyableProtocol(url.protocol)) {
167+
return undefined;
168+
}
169+
170+
const config = getHttpProxyConfig();
171+
// `http.proxySupport: off` is an explicit opt-out: no proxy or TLS override applies.
172+
if (!config.proxySupportEnabled) {
173+
return undefined;
174+
}
175+
const isTls = url.protocol === 'https:';
176+
const bypassed = isHostBypassed(url.hostname, config.noProxy);
177+
const proxyUrl = bypassed ? undefined : resolveProxyUrl(config, isTls);
178+
return { url, config, proxyUrl };
179+
}
180+
181+
/**
182+
* Returns an `http`/`https` `Agent` configured from VS Code's `http.proxy` / `http.proxyStrictSSL`
183+
* settings (falling back to the standard proxy environment variables) for the given request URL,
184+
* or `undefined` when no proxy or TLS override applies to it.
185+
*
186+
* Shared so extensions can apply the same proxy behavior to HTTP clients that don't go through the
187+
* Azure SDK pipeline. The Azure SDK's built-in proxy policy only reads proxy environment variables
188+
* and ignores VS Code's `http.*` settings, so this bridges that gap.
189+
*/
190+
export function getProxyAgent(requestUrl: string): HttpsAgent | HttpProxyAgent<string> | HttpsProxyAgent<string> | undefined {
191+
const resolved = resolveProxyForRequest(requestUrl);
192+
if (!resolved) {
193+
return undefined;
194+
}
195+
196+
const { url, config, proxyUrl } = resolved;
197+
const isTls = url.protocol === 'https:';
198+
const insecure = isTls && config.strictSSL === false;
199+
200+
if (proxyUrl) {
201+
// Own the agent when a TLS override is needed, since the SDK's proxy agent drops TLS options.
202+
return getProxyAgentForUrl(proxyUrl, isTls, /* rejectUnauthorized */ !insecure);
203+
}
204+
if (insecure) {
205+
return getInsecureTlsAgent();
206+
}
207+
return undefined;
208+
}
209+
210+
/**
211+
* Returns a {@link ProxySettings} object (host/port/username/password) configured from VS Code's
212+
* `http.proxy` setting (falling back to the standard proxy environment variables) for the given
213+
* request URL, or `undefined` when no proxy applies (unset, `http.noProxy` bypass, or
214+
* `http.proxySupport: off`).
215+
*
216+
* Intended for pipeline-based Azure SDK data-plane clients that accept a `proxyOptions`/
217+
* `proxySettings` option (e.g. Storage's `BlobServiceClient`/`ShareServiceClient`) rather than a
218+
* raw `http.Agent`. Unlike {@link getProxyAgent}, this does not carry a TLS override
219+
* (`http.proxyStrictSSL: false`), since `ProxySettings` cannot express one.
220+
*/
221+
export function getProxySettings(requestUrl: string): ProxySettings | undefined {
222+
const proxyUrl = resolveProxyForRequest(requestUrl)?.proxyUrl;
223+
return proxyUrl ? getDefaultProxySettings(proxyUrl) : undefined;
224+
}
225+
226+
/**
227+
* Pipeline policy that applies VS Code's proxy configuration to Azure SDK requests.
228+
*
229+
* The Azure SDK ships a built-in `proxyPolicy` that reads only proxy environment variables and
230+
* whose proxy agent discards TLS settings. This policy runs before it and:
231+
* - injects `request.proxySettings` from VS Code's `http.proxy` when only a secure proxy is needed
232+
* (letting the built-in policy build and cache the agent, honoring `NODE_EXTRA_CA_CERTS`), or
233+
* - sets `request.agent` directly when a TLS override (`http.proxyStrictSSL: false`) is required,
234+
* which the built-in policy respects by skipping requests that already have an agent.
235+
*
236+
* It also honors VS Code's `http.noProxy` (merged with `NO_PROXY`) and `http.proxySupport: off`
237+
* (which makes this policy a no-op), and never overrides an agent a caller set explicitly
238+
* (e.g. `sendRequestWithTimeout` with `rejectUnauthorized`).
239+
*/
240+
export class ProxyAgentPolicy implements PipelinePolicy {
241+
public static readonly Name = 'azExtProxyAgentPolicy';
242+
public readonly name = ProxyAgentPolicy.Name;
243+
244+
/**
245+
* Adds the policy to `pipeline` (before the built-in proxy policy) unless it is already present.
246+
*/
247+
public static addIfNeeded(pipeline: Pipeline): void {
248+
const alreadyAdded = pipeline.getOrderedPolicies().some((policy) => policy.name === ProxyAgentPolicy.Name);
249+
if (!alreadyAdded) {
250+
pipeline.addPolicy(new ProxyAgentPolicy(), { beforePolicies: [proxyPolicyName] });
251+
}
252+
}
253+
254+
public async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
255+
// Respect an agent a caller set explicitly.
256+
if (!request.agent) {
257+
this.applyProxy(request);
258+
}
259+
return next(request);
260+
}
261+
262+
private applyProxy(request: PipelineRequest): void {
263+
const resolved = resolveProxyForRequest(request.url);
264+
if (!resolved) {
265+
return;
266+
}
267+
const { url, config, proxyUrl } = resolved;
268+
const isTls = url.protocol === 'https:';
269+
const insecure = isTls && config.strictSSL === false;
270+
271+
// Secure proxy from VS Code config: hand off to the built-in policy so it builds/caches the
272+
// agent and honors NODE_EXTRA_CA_CERTS. Env-var proxies are already handled by that policy.
273+
if (config.proxyUrl && proxyUrl && !insecure) {
274+
request.proxySettings ??= getDefaultProxySettings(config.proxyUrl);
275+
return;
276+
}
277+
278+
// A TLS override is needed (strictSSL: false), so own the agent — with or without a proxy.
279+
if (insecure) {
280+
request.agent = proxyUrl
281+
? getProxyAgentForUrl(proxyUrl, isTls, /* rejectUnauthorized */ false)
282+
: getInsecureTlsAgent();
283+
}
284+
}
285+
}

0 commit comments

Comments
 (0)