Skip to content

Commit

Permalink
Local and remote proxy settings (microsoft/vscode-copilot-release#3821)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrmarti committed Jan 16, 2025
1 parent 4d44668 commit 215f10c
Show file tree
Hide file tree
Showing 15 changed files with 237 additions and 124 deletions.
58 changes: 58 additions & 0 deletions extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AddressInfo } from 'net';
import { resetCaches } from '@vscode/proxy-agent';
import * as vscode from 'vscode';
import { middleware, Straightforward } from 'straightforward';
import assert from 'assert';

(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - network proxy support', () => {

Expand Down Expand Up @@ -166,4 +167,61 @@ import { middleware, Straightforward } from 'straightforward';
await vscode.workspace.getConfiguration().update('integration-test.http.proxyAuth', undefined, vscode.ConfigurationTarget.Global);
}
});

(vscode.env.remoteName ? test : test.skip)('separate local / remote proxy settings', async () => {
// Assumes test resolver runs with `--use-host-proxy`.
const localProxy = 'http://localhost:1234';
const remoteProxy = 'http://localhost:4321';

const actualLocalProxy1 = vscode.workspace.getConfiguration().get('http.proxy');

const p1 = waitForConfigChange('http.proxy');
await vscode.workspace.getConfiguration().update('http.proxy', localProxy, vscode.ConfigurationTarget.Global);
await p1;
const actualLocalProxy2 = vscode.workspace.getConfiguration().get('http.proxy');

const p2 = waitForConfigChange('http.useLocalProxyConfiguration');
await vscode.workspace.getConfiguration().update('http.useLocalProxyConfiguration', false, vscode.ConfigurationTarget.Global);
await p2;
const actualRemoteProxy1 = vscode.workspace.getConfiguration().get('http.proxy');

const p3 = waitForConfigChange('http.proxy');
await vscode.workspace.getConfiguration().update('http.proxy', remoteProxy, vscode.ConfigurationTarget.Global);
await p3;
const actualRemoteProxy2 = vscode.workspace.getConfiguration().get('http.proxy');

const p4 = waitForConfigChange('http.proxy');
await vscode.workspace.getConfiguration().update('http.proxy', undefined, vscode.ConfigurationTarget.Global);
await p4;
const actualRemoteProxy3 = vscode.workspace.getConfiguration().get('http.proxy');

const p5 = waitForConfigChange('http.proxy');
await vscode.workspace.getConfiguration().update('http.useLocalProxyConfiguration', true, vscode.ConfigurationTarget.Global);
await p5;
const actualLocalProxy3 = vscode.workspace.getConfiguration().get('http.proxy');

const p6 = waitForConfigChange('http.proxy');
await vscode.workspace.getConfiguration().update('http.proxy', undefined, vscode.ConfigurationTarget.Global);
await p6;
const actualLocalProxy4 = vscode.workspace.getConfiguration().get('http.proxy');

assert.strictEqual(actualLocalProxy1, '');
assert.strictEqual(actualLocalProxy2, localProxy);
assert.strictEqual(actualRemoteProxy1, '');
assert.strictEqual(actualRemoteProxy2, remoteProxy);
assert.strictEqual(actualRemoteProxy3, '');
assert.strictEqual(actualLocalProxy3, localProxy);
assert.strictEqual(actualLocalProxy4, '');

function waitForConfigChange(key: string) {
return new Promise<void>(resolve => {
const s = vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(key)) {
s.dispose();
resolve();
}
});
});
}
});
});
2 changes: 1 addition & 1 deletion src/vs/base/parts/ipc/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ export class ChannelClient implements IChannelClient, IDisposable {
});

return result.finally(() => {
disposable.dispose();
disposable?.dispose(); // Seen as undefined in tests.
this.activeRequests.delete(disposableWithRequestCancel);
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/vs/code/node/cliProcessMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class CliMain extends Disposable {
services.set(IUriIdentityService, new UriIdentityService(fileService));

// Request
const requestService = new RequestService(configurationService, environmentService, logService);
const requestService = new RequestService('local', configurationService, environmentService, logService);
services.set(IRequestService, requestService);

// Download Service
Expand Down
186 changes: 106 additions & 80 deletions src/vs/platform/request/common/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,91 +134,117 @@ export async function asJson<T = {}>(context: IRequestContext): Promise<T | null
}
}

export function updateProxyConfigurationsScope(scope: ConfigurationScope): void {
registerProxyConfigurations(scope);
export function updateProxyConfigurationsScope(useHostProxy: boolean, useHostProxyDefault: boolean): void {
registerProxyConfigurations(useHostProxy, useHostProxyDefault);
}

let proxyConfiguration: IConfigurationNode | undefined;
function registerProxyConfigurations(scope: ConfigurationScope): void {
let proxyConfiguration: IConfigurationNode[] = [];
function registerProxyConfigurations(useHostProxy = true, useHostProxyDefault = true): void {
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
const oldProxyConfiguration = proxyConfiguration;
proxyConfiguration = {
id: 'http',
order: 15,
title: localize('httpConfigurationTitle', "HTTP"),
type: 'object',
scope,
properties: {
'http.proxy': {
type: 'string',
pattern: '^(https?|socks|socks4a?|socks5h?)://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$',
markdownDescription: localize('proxy', "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables."),
restricted: true
},
'http.proxyStrictSSL': {
type: 'boolean',
default: true,
description: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs."),
restricted: true
},
'http.proxyKerberosServicePrincipal': {
type: 'string',
markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set."),
restricted: true
},
'http.noProxy': {
type: 'array',
items: { type: 'string' },
markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests."),
restricted: true
},
'http.proxyAuthorization': {
type: ['null', 'string'],
default: null,
markdownDescription: localize('proxyAuthorization', "The value to send as the `Proxy-Authorization` header for every network request."),
restricted: true
},
'http.proxySupport': {
type: 'string',
enum: ['off', 'on', 'fallback', 'override'],
enumDescriptions: [
localize('proxySupportOff', "Disable proxy support for extensions."),
localize('proxySupportOn', "Enable proxy support for extensions."),
localize('proxySupportFallback', "Enable proxy support for extensions, fall back to request options, when no proxy found."),
localize('proxySupportOverride', "Enable proxy support for extensions, override request options."),
],
default: 'override',
description: localize('proxySupport', "Use the proxy support for extensions."),
restricted: true
},
'http.systemCertificates': {
type: 'boolean',
default: true,
description: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. (On Windows and macOS, a reload of the window is required after turning this off.)"),
restricted: true
},
'http.experimental.systemCertificatesV2': {
type: 'boolean',
tags: ['experimental'],
default: false,
description: localize('systemCertificatesV2', "Controls whether experimental loading of CA certificates from the OS should be enabled. This uses a more general approach than the default implementation."),
restricted: true
},
'http.electronFetch': {
type: 'boolean',
default: false,
description: localize('electronFetch', "Controls whether use of Electron's fetch implementation instead of Node.js' should be enabled. All local extensions will get Electron's fetch implementation for the global fetch API."),
restricted: true
},
'http.fetchAdditionalSupport': {
type: 'boolean',
default: true,
markdownDescription: localize('fetchAdditionalSupport', "Controls whether Node.js' fetch implementation should be extended with additional support. Currently proxy support ({0}) and system certificates ({1}) are added when the corresponding settings are enabled.", '`#http.proxySupport#`', '`#http.systemCertificates#`'),
restricted: true
proxyConfiguration = [
{
id: 'http',
order: 15,
title: localize('httpConfigurationTitle', "HTTP"),
type: 'object',
scope: ConfigurationScope.MACHINE,
properties: {
'http.useLocalProxyConfiguration': {
type: 'boolean',
default: useHostProxyDefault,
markdownDescription: localize('useLocalProxy', "Controls whether in the remote extension host the local proxy configuration should be used. This setting only applies as a remote setting during [remote development](https://aka.ms/vscode-remote)."),
restricted: true
},
}
},
{
id: 'http',
order: 15,
title: localize('httpConfigurationTitle', "HTTP"),
type: 'object',
scope: ConfigurationScope.APPLICATION,
properties: {
'http.electronFetch': {
type: 'boolean',
default: false,
description: localize('electronFetch', "Controls whether use of Electron's fetch implementation instead of Node.js' should be enabled. All local extensions will get Electron's fetch implementation for the global fetch API."),
restricted: true
},
}
},
{
id: 'http',
order: 15,
title: localize('httpConfigurationTitle', "HTTP"),
type: 'object',
scope: useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE,
properties: {
'http.proxy': {
type: 'string',
pattern: '^(https?|socks|socks4a?|socks5h?)://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$',
markdownDescription: localize('proxy', "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
restricted: true
},
'http.proxyStrictSSL': {
type: 'boolean',
default: true,
markdownDescription: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
restricted: true
},
'http.proxyKerberosServicePrincipal': {
type: 'string',
markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
restricted: true
},
'http.noProxy': {
type: 'array',
items: { type: 'string' },
markdownDescription: localize('noProxy', "Specifies domain names for which proxy settings should be ignored for HTTP/HTTPS requests. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
restricted: true
},
'http.proxyAuthorization': {
type: ['null', 'string'],
default: null,
markdownDescription: localize('proxyAuthorization', "The value to send as the `Proxy-Authorization` header for every network request. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
restricted: true
},
'http.proxySupport': {
type: 'string',
enum: ['off', 'on', 'fallback', 'override'],
enumDescriptions: [
localize('proxySupportOff', "Disable proxy support for extensions."),
localize('proxySupportOn', "Enable proxy support for extensions."),
localize('proxySupportFallback', "Enable proxy support for extensions, fall back to request options, when no proxy found."),
localize('proxySupportOverride', "Enable proxy support for extensions, override request options."),
],
default: 'override',
markdownDescription: localize('proxySupport', "Use the proxy support for extensions. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
restricted: true
},
'http.systemCertificates': {
type: 'boolean',
default: true,
markdownDescription: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. On Windows and macOS, a reload of the window is required after turning this off. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
restricted: true
},
'http.experimental.systemCertificatesV2': {
type: 'boolean',
tags: ['experimental'],
default: false,
markdownDescription: localize('systemCertificatesV2', "Controls whether experimental loading of CA certificates from the OS should be enabled. This uses a more general approach than the default implementation. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`'),
restricted: true
},
'http.fetchAdditionalSupport': {
type: 'boolean',
default: true,
markdownDescription: localize('fetchAdditionalSupport', "Controls whether Node.js' fetch implementation should be extended with additional support. Currently proxy support ({1}) and system certificates ({2}) are added when the corresponding settings are enabled. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.", '`#http.useLocalProxyConfiguration#`', '`#http.proxySupport#`', '`#http.systemCertificates#`'),
restricted: true
}
}
}
};
configurationRegistry.updateConfigurations({ add: [proxyConfiguration], remove: oldProxyConfiguration ? [oldProxyConfiguration] : [] });
];
configurationRegistry.updateConfigurations({ add: proxyConfiguration, remove: oldProxyConfiguration });
}

registerProxyConfigurations(ConfigurationScope.APPLICATION);
registerProxyConfigurations();
11 changes: 11 additions & 0 deletions src/vs/platform/request/electron-utility/requestService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@ import { net } from 'electron';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { IRequestContext, IRequestOptions } from '../../../base/parts/request/common/request.js';
import { IRawRequestFunction, RequestService as NodeRequestService } from '../node/requestService.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { INativeEnvironmentService } from '../../environment/common/environment.js';
import { ILogService } from '../../log/common/log.js';

function getRawRequest(options: IRequestOptions): IRawRequestFunction {
return net.request as any as IRawRequestFunction;
}

export class RequestService extends NodeRequestService {

constructor(
@IConfigurationService configurationService: IConfigurationService,
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@ILogService logService: ILogService,
) {
super('local', configurationService, environmentService, logService);
}

override request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
return super.request({ ...(options || {}), getRawRequest, isChromiumNetwork: true }, token);
}
Expand Down
25 changes: 13 additions & 12 deletions src/vs/platform/request/node/requestService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from '
import { Agent, getProxyAgent } from './proxy.js';
import { createGunzip } from 'zlib';

interface IHTTPConfiguration {
proxy?: string;
proxyStrictSSL?: boolean;
proxyAuthorization?: string;
}

export interface IRawRequestFunction {
(options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
}
Expand All @@ -52,6 +46,7 @@ export class RequestService extends AbstractRequestService implements IRequestSe
private shellEnvErrorLogged?: boolean;

constructor(
private readonly machine: 'local' | 'remote',
@IConfigurationService private readonly configurationService: IConfigurationService,
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@ILogService logService: ILogService,
Expand All @@ -66,11 +61,9 @@ export class RequestService extends AbstractRequestService implements IRequestSe
}

private configure() {
const config = this.configurationService.getValue<IHTTPConfiguration | undefined>('http');

this.proxyUrl = config?.proxy;
this.strictSSL = !!config?.proxyStrictSSL;
this.authorization = config?.proxyAuthorization;
this.proxyUrl = this.getConfigValue<string>('http.proxy');
this.strictSSL = !!this.getConfigValue<boolean>('http.proxyStrictSSL');
this.authorization = this.getConfigValue<string>('http.proxyAuthorization');
}

async request(options: NodeRequestOptions, token: CancellationToken): Promise<IRequestContext> {
Expand Down Expand Up @@ -115,7 +108,7 @@ export class RequestService extends AbstractRequestService implements IRequestSe

async lookupKerberosAuthorization(urlStr: string): Promise<string | undefined> {
try {
const spnConfig = this.configurationService.getValue<string>('http.proxyKerberosServicePrincipal');
const spnConfig = this.getConfigValue<string>('http.proxyKerberosServicePrincipal');
const response = await lookupKerberosAuthorization(urlStr, spnConfig, this.logService, 'RequestService#lookupKerberosAuthorization');
return 'Negotiate ' + response;
} catch (err) {
Expand All @@ -128,6 +121,14 @@ export class RequestService extends AbstractRequestService implements IRequestSe
const proxyAgent = await import('@vscode/proxy-agent');
return proxyAgent.loadSystemCertificates({ log: this.logService });
}

private getConfigValue<T>(key: string): T | undefined {
if (this.machine === 'remote') {
return this.configurationService.getValue<T>(key);
}
const values = this.configurationService.inspect<T>(key);
return values.userLocalValue || values.defaultValue;
}
}

export async function lookupKerberosAuthorization(urlStr: string, spnConfig: string | undefined, logService: ILogService, logPrefix: string) {
Expand Down
3 changes: 2 additions & 1 deletion src/vs/platform/windows/electron-main/windowImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,8 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {

// Proxy
if (!e || e.affectsConfiguration('http.proxy') || e.affectsConfiguration('http.noProxy')) {
let newHttpProxy = (this.configurationService.getValue<string>('http.proxy') || '').trim()
const inspect = this.configurationService.inspect<string>('http.proxy');
let newHttpProxy = (inspect.userLocalValue || '').trim()
|| (process.env['https_proxy'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['HTTP_PROXY'] || '').trim() // Not standardized.
|| undefined;

Expand Down
2 changes: 1 addition & 1 deletion src/vs/server/node/remoteExtensionHostAgentCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class CliMain extends Disposable {
userDataProfilesService.init()
]);

services.set(IRequestService, new SyncDescriptor(RequestService));
services.set(IRequestService, new SyncDescriptor(RequestService, ['remote']));
services.set(IDownloadService, new SyncDescriptor(DownloadService));
services.set(ITelemetryService, NullTelemetryService);
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService));
Expand Down
Loading

0 comments on commit 215f10c

Please sign in to comment.