/*--------------------------------------------------------------------------------------------- * 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((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((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((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((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((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(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 | ConnectContext > => 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(); };