mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 12:04:04 +01:00
271 lines
9.0 KiB
TypeScript
271 lines
9.0 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as https from 'https';
|
|
import 'mocha';
|
|
import { assertNoRpc } from '../utils';
|
|
import { pki } from 'node-forge';
|
|
import { AddressInfo } from 'net';
|
|
import { resetCaches } from '@vscode/proxy-agent';
|
|
import * as vscode from 'vscode';
|
|
import { Straightforward, Middleware, RequestContext, ConnectContext, isRequest, isConnect } from 'straightforward';
|
|
import assert from 'assert';
|
|
|
|
(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - network proxy support', () => {
|
|
|
|
teardown(async function () {
|
|
assertNoRpc();
|
|
});
|
|
|
|
test('custom root certificate', async () => {
|
|
const keys = pki.rsa.generateKeyPair(2048);
|
|
const cert = pki.createCertificate();
|
|
cert.publicKey = keys.publicKey;
|
|
cert.serialNumber = '01';
|
|
cert.validity.notBefore = new Date();
|
|
cert.validity.notAfter = new Date();
|
|
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
|
const attrs = [{
|
|
name: 'commonName',
|
|
value: 'localhost-proxy-test'
|
|
}];
|
|
cert.setSubject(attrs);
|
|
cert.setIssuer(attrs);
|
|
cert.sign(keys.privateKey);
|
|
const certPEM = pki.certificateToPem(cert);
|
|
const privateKeyPEM = pki.privateKeyToPem(keys.privateKey);
|
|
|
|
let resolvePort: (port: number) => void;
|
|
let rejectPort: (err: any) => void;
|
|
const port = new Promise<number>((resolve, reject) => {
|
|
resolvePort = resolve;
|
|
rejectPort = reject;
|
|
});
|
|
const server = https.createServer({
|
|
key: privateKeyPEM,
|
|
cert: certPEM,
|
|
}, (_req, res) => {
|
|
res.end();
|
|
}).listen(0, '127.0.0.1', () => {
|
|
const address = server.address();
|
|
resolvePort((address as AddressInfo).port);
|
|
}).on('error', err => {
|
|
rejectPort(err);
|
|
});
|
|
|
|
// Using https.globalAgent because it is shared with proxyResolver.ts and mutable.
|
|
(https.globalAgent as any).testCertificates = [certPEM];
|
|
resetCaches();
|
|
|
|
try {
|
|
const portNumber = await port;
|
|
await new Promise<void>((resolve, reject) => {
|
|
https.get(`https://127.0.0.1:${portNumber}`, { servername: 'localhost-proxy-test' }, res => {
|
|
if (res.statusCode === 200) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Unexpected status code: ${res.statusCode}`));
|
|
}
|
|
})
|
|
.on('error', reject);
|
|
});
|
|
} finally {
|
|
delete (https.globalAgent as any).testCertificates;
|
|
resetCaches();
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test('basic auth', async () => {
|
|
const url = 'https://example.com'; // Need to use non-local URL because local URLs are excepted from proxying.
|
|
const user = 'testuser';
|
|
const pass = 'testpassword';
|
|
|
|
const sf = new Straightforward();
|
|
let authEnabled = false;
|
|
const authOpts: AuthOpts = { user, pass };
|
|
const auth = middlewareAuth(authOpts);
|
|
sf.onConnect.use(async (context, next) => {
|
|
if (authEnabled) {
|
|
return auth(context, next);
|
|
}
|
|
next();
|
|
});
|
|
sf.onConnect.use(({ clientSocket }) => {
|
|
// Shortcircuit the request.
|
|
if (authEnabled) {
|
|
clientSocket.end('HTTP/1.1 204\r\n\r\n');
|
|
} else {
|
|
clientSocket.end('HTTP/1.1 418\r\n\r\n');
|
|
}
|
|
});
|
|
const proxyListen = sf.listen(0);
|
|
|
|
try {
|
|
await proxyListen;
|
|
const proxyPort = (sf.server.address() as AddressInfo).port;
|
|
|
|
const change = waitForConfigChange('http.proxy');
|
|
await vscode.workspace.getConfiguration().update('http.proxy', `http://127.0.0.1:${proxyPort}`, vscode.ConfigurationTarget.Global);
|
|
await change;
|
|
await new Promise<void>((resolve, reject) => {
|
|
https.get(url, res => {
|
|
if (res.statusCode === 418) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Unexpected status code (expected 418): ${res.statusCode}`));
|
|
}
|
|
})
|
|
.on('error', reject);
|
|
});
|
|
|
|
authEnabled = true;
|
|
await new Promise<void>((resolve, reject) => {
|
|
https.get(url, res => {
|
|
if (res.statusCode === 407) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Unexpected status code (expected 407): ${res.statusCode}`));
|
|
}
|
|
})
|
|
.on('error', reject);
|
|
});
|
|
|
|
authOpts.realm = Buffer.from(JSON.stringify({ username: user, password: pass })).toString('base64');
|
|
await new Promise<void>((resolve, reject) => {
|
|
https.get(url, res => {
|
|
if (res.statusCode === 204) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Unexpected status code (expected 204): ${res.statusCode}`));
|
|
}
|
|
})
|
|
.on('error', reject);
|
|
});
|
|
} finally {
|
|
sf.close();
|
|
const change = waitForConfigChange('http.proxy');
|
|
await vscode.workspace.getConfiguration().update('http.proxy', undefined, vscode.ConfigurationTarget.Global);
|
|
await change;
|
|
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();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// Added 'realm'. From MIT licensed https://github.com/berstend/straightforward/blob/84a4cb88024cffce37a05870da7d9d0aba7dcca8/src/middleware/auth.ts
|
|
|
|
export interface AuthOpts {
|
|
realm?: string;
|
|
user?: string;
|
|
pass?: string;
|
|
dynamic?: boolean;
|
|
}
|
|
|
|
export interface RequestAdditionsAuth {
|
|
locals: { proxyUser: string; proxyPass: string };
|
|
}
|
|
|
|
/**
|
|
* Authenticate an incoming proxy request
|
|
* Supports static `user` and `pass` or `dynamic`,
|
|
* in which case `ctx.req.locals` will be populated with `proxyUser` and `proxyPass`
|
|
* This middleware supports both onRequest and onConnect
|
|
*/
|
|
export const middlewareAuth =
|
|
(opts: AuthOpts): Middleware<
|
|
RequestContext<RequestAdditionsAuth> | ConnectContext<RequestAdditionsAuth>
|
|
> =>
|
|
async (ctx, next) => {
|
|
const { realm, user, pass, dynamic } = opts;
|
|
const sendAuthRequired = () => {
|
|
const realmStr = realm ? ` realm="${realm}"` : '';
|
|
if (isRequest(ctx)) {
|
|
ctx.res.writeHead(407, { 'Proxy-Authenticate': `Basic${realmStr}` });
|
|
ctx.res.end();
|
|
} else if (isConnect(ctx)) {
|
|
ctx.clientSocket.end(
|
|
'HTTP/1.1 407\r\n' + `Proxy-Authenticate: basic${realmStr}\r\n` + '\r\n'
|
|
);
|
|
}
|
|
};
|
|
const proxyAuth = ctx.req.headers['proxy-authorization'];
|
|
if (!proxyAuth) {
|
|
return sendAuthRequired();
|
|
}
|
|
const [proxyUser, proxyPass] = Buffer.from(
|
|
proxyAuth.replace('Basic ', ''),
|
|
'base64'
|
|
)
|
|
.toString()
|
|
.split(':');
|
|
|
|
if (!dynamic && !!(!!user && !!pass)) {
|
|
if (user !== proxyUser || pass !== proxyPass) {
|
|
return sendAuthRequired();
|
|
}
|
|
}
|
|
ctx.req.locals.proxyUser = proxyUser;
|
|
ctx.req.locals.proxyPass = proxyPass;
|
|
|
|
return next();
|
|
};
|