Skip to content

Commit

Permalink
Add support for certificates (#627)
Browse files Browse the repository at this point in the history
* Add support for certificates

* Count

* Comment

* Make opt-in and provide better error

* specify type

* Force rebuild

* Read cert files

* Skip URL tests on Linux

* ...and mac
  • Loading branch information
StephenWeatherford committed Nov 17, 2018
1 parent 342a76c commit 86afb81
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 26 deletions.
52 changes: 45 additions & 7 deletions dockerExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
let loadStartTime = Date.now();

import * as assert from 'assert';
import * as fse from 'fs-extra';
import * as https from 'https';
import * as path from 'path';
import { CoreOptions } from 'request';
import * as request from 'request-promise-native';
import { RequestPromise } from 'request-promise-native';
import * as vscode from 'vscode';
import { AzureUserInput, callWithTelemetryAndErrorHandling, createTelemetryReporter, IActionContext, registerCommand as uiRegisterCommand, registerUIExtensionVariables, TelemetryProperties, UserCancelledError } from 'vscode-azureextensionui';
import { AzureUserInput, callWithTelemetryAndErrorHandling, callWithTelemetryAndErrorHandlingSync, createTelemetryReporter, IActionContext, registerCommand as uiRegisterCommand, registerUIExtensionVariables, TelemetryProperties, UserCancelledError } from 'vscode-azureextensionui';
import { ConfigurationParams, DidChangeConfigurationNotification, DocumentSelector, LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from 'vscode-languageclient/lib/main';
import { viewACRLogs } from "./commands/azureCommands/acr-logs";
import { LogContentProvider } from "./commands/azureCommands/acr-logs-utils/logFileManager";
Expand Down Expand Up @@ -60,10 +64,14 @@ import { NodeBase } from './explorer/models/nodeBase';
import { RootNode } from './explorer/models/rootNode';
import { browseAzurePortal } from './explorer/utils/browseAzurePortal';
import { browseDockerHub, dockerHubLogout } from './explorer/utils/dockerHubUtils';
import { wrapError } from './explorer/utils/wrapError';
import { ext } from './extensionVariables';
import { globAsync } from './helpers/async';
import { isLinux, isMac, isWindows } from './helpers/osVersion';
import { initializeTelemetryReporter, reporter } from './telemetry/telemetry';
import { addUserAgent } from './utils/addUserAgent';
import { AzureUtilityManager } from './utils/azureUtilityManager';
import { getTrustedCertificates } from './utils/getTrustedCertificates';
import { Keytar } from './utils/keytar';

export const FROM_DIRECTIVE_PATTERN = /^\s*FROM\s*([\w-\/:]*)(\s*AS\s*[a-z][a-z0-9-_\\.]*)?$/i;
Expand Down Expand Up @@ -109,12 +117,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
let activateStartTime = Date.now();

initializeExtensionVariables(ctx);

// Set up the user agent for all direct 'request' calls in the extension (must use ext.request)
let defaultRequestOptions = {};
addUserAgent(defaultRequestOptions);
ext.request = request.defaults(defaultRequestOptions);

await setRequestDefaults();
await callWithTelemetryAndErrorHandling('docker.activate', async function (this: IActionContext): Promise<void> {
this.properties.isActivationEvent = 'true';
this.measurements.mainFileLoad = (loadEndTime - loadStartTime) / 1000;
Expand Down Expand Up @@ -189,6 +192,39 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
});
}

async function setRequestDefaults(): Promise<void> {
// Set up the user agent for all direct 'request' calls in the extension (as long as they use ext.request)
// ... Trusted root certificate authorities
let caList = await getTrustedCertificates();
let defaultRequestOptions: CoreOptions = { agentOptions: { ca: caList } };
// ... User agent
addUserAgent(defaultRequestOptions);
let requestWithDefaults = request.defaults(defaultRequestOptions);

// Wrap 'get' to provide better error message for self-signed certificates
let originalGet = <(...args: unknown[]) => RequestPromise>requestWithDefaults.get;
// tslint:disable-next-line:no-any
async function wrappedGet(this: unknown, ...args: unknown[]): Promise<any> {
try {
// tslint:disable-next-line: no-unsafe-any
return await originalGet.call(this, ...args);
} catch (err) {
let error = <{ cause?: { code?: string } }>err;

if (error && error.cause && error.cause.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
err = wrapError(err, `There was a problem verifying a certificate. This could be caused by a self-signed or corporate certificate. You may need to set the 'docker.useCertificateStore' or 'docker.certificatePaths' setting.`)
}

throw err;
}
}

// tslint:disable-next-line:no-any
requestWithDefaults.get = <any>wrappedGet;

ext.request = requestWithDefaults;
}

async function createWebApp(context?: AzureImageTagNode | DockerHubImageTagNode): Promise<void> {
assert(!!context, "Should not be available through command palette");

Expand Down Expand Up @@ -324,6 +360,8 @@ namespace Configuration {
// Update endpoint and refresh explorer if needed
if (e.affectsConfiguration('docker')) {
docker.refreshEndpoint();
// tslint:disable-next-line: no-floating-promises
setRequestDefaults();
vscode.commands.executeCommand('vscode-docker.explorer.refresh');
}
}
Expand Down
2 changes: 1 addition & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ gulp.task('test', ['install-azure-account'], (cb) => {
const env = process.env;
env.DEBUGTELEMETRY = 1;
env.CODE_TESTS_WORKSPACE = './test/test.code-workspace';
env.MOCHA_reporter = 'mocha-junit-reporter';
//env.MOCHA_reporter = 'mocha-junit-reporter';
env.MOCHA_FILE = path.join(__dirname, 'test-results.xml');
const cmd = cp.spawn('node', ['./node_modules/vscode/bin/test'], { stdio: 'inherit', env });
cmd.on('close', (code) => {
Expand Down
21 changes: 18 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,19 @@
"default": "",
"description": "Host to connect to (same as setting the DOCKER_HOST environment variable)"
},
"docker.certificatePaths": {
"type": "array",
"items": {
"type": "string"
},
"description": "Paths to files or folders containing certificates to import. For Linux, the correct path to pick up system-wide certificates will depend on the distribution. The list [\"/etc/ssl/certs/ca-certificates\", \"/etc/openssl/certs\", \"/etc/pki/tls/certs\", \"/usr/local/share/certs\"] may suffice for many.",
"default": []
},
"docker.useCertificateStore": {
"type": "boolean",
"description": "On Mac and Windows, indicates whether to automatically import certificates from the system certificate store. Ignored on Linux.",
"default": false
},
"docker.languageserver.diagnostics.deprecatedMaintainer": {
"scope": "resource",
"type": "string",
Expand Down Expand Up @@ -851,21 +864,23 @@
"azure-arm-containerregistry": "^3.0.0",
"azure-arm-resource": "^2.0.0-preview",
"azure-arm-website": "^1.0.0-preview",
"deep-equal": "^1.0.1",
"dockerfile-language-server-nodejs": "^0.0.19",
"azure-storage": "^2.8.1",
"clipboardy": "^1.2.3",
"deep-equal": "^1.0.1",
"dockerfile-language-server-nodejs": "^0.0.19",
"dockerode": "^2.5.1",
"fs-extra": "^6.0.1",
"glob": "7.1.2",
"gradle-to-js": "^1.0.1",
"mac-ca": "^1.0.4",
"moment": "^2.19.3",
"opn": "^5.2.0",
"pom-parser": "^1.1.1",
"request-promise-native": "^1.0.5",
"semver": "^5.5.1",
"tar": "^4.4.6",
"vscode-azureextensionui": "^0.19.0",
"vscode-languageclient": "^4.4.0"
"vscode-languageclient": "^4.4.0",
"win-ca": "^2.2.0"
}
}
35 changes: 20 additions & 15 deletions test/testUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,34 @@ import { ITestCallbackContext } from "mocha";
import { ext } from "../extensionVariables";
import { wrapError } from "../explorer/utils/wrapError";
import { Uri } from "vscode";
import { isLinux, isWindows } from "../helpers/osVersion";

export async function testUrl(url: string): Promise<void> {
test(`Testing ${url} exists`, async function (this: ITestCallbackContext) {
this.timeout(10000);

let contents: string | undefined;
if (!isWindows()) {
this.skip();
} else {
let contents: string | undefined;

try {
let options = {
method: 'GET',
url
};
try {
let options = {
method: 'GET',
url
};

contents = <string>await ext.request(options);
} catch (error) {
throw wrapError(error, `Could not connect to ${url}`);
}
contents = <string>await ext.request(options);
} catch (error) {
throw wrapError(error, `Could not connect to ${url}`);
}

let fragment = Uri.parse(url).fragment;
if (fragment) {
// If contains a fragment, verify a link with that ID actually exists in the contents
if (!contents.includes(`href="#${fragment}"`)) {
throw new Error(`Found page for ${url}, but couldn't find target for fragment ${fragment}`);
let fragment = Uri.parse(url).fragment;
if (fragment) {
// If contains a fragment, verify a link with that ID actually exists in the contents
if (!contents.includes(`href="#${fragment}"`)) {
throw new Error(`Found page for ${url}, but couldn't find target for fragment ${fragment}`);
}
}
}
});
Expand Down
64 changes: 64 additions & 0 deletions thirdpartynotices.txt
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,67 @@ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

14. win-ca (https://github.com/ukoloff/win-ca)

BSD 3-Clause License

Copyright (c) 2016, Stas Ukolov
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

15. mac-ca (https://github.com/jfromaniello/mac-ca)

BSD 3-Clause License

Copyright (c) 2018, José F. Romaniello
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
109 changes: 109 additions & 0 deletions utils/getTrustedCertificates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as fse from 'fs-extra';
import * as https from 'https';
import * as path from 'path';
import * as vscode from 'vscode';
import { callWithTelemetryAndErrorHandling, IActionContext } from "vscode-azureextensionui";
import { ext } from '../extensionVariables';
import { globAsync } from '../helpers/async';
import { isLinux, isMac, isWindows } from '../helpers/osVersion';

let _systemCertificates: (string | Buffer)[] | undefined;

export async function getTrustedCertificates(): Promise<(string | Buffer)[]> {
// tslint:disable-next-line:no-function-expression
return callWithTelemetryAndErrorHandling('docker.certificates', async function (this: IActionContext): Promise<(string | Buffer)[]> {
this.suppressTelemetry = true;

let useCertificateStore: boolean = !!vscode.workspace.getConfiguration('docker').get<boolean>('useCertificateStore');
this.properties.useCertStore = String(useCertificateStore);
let systemCerts: (string | Buffer)[] = useCertificateStore ? getCertificatesFromSystem() : [];

let certificatePaths: string[] = vscode.workspace.getConfiguration('docker').get<string[] | undefined>('certificatePaths') || [];
this.properties.certPathsCount = String(certificatePaths.length);
let filesCerts = certificatePaths ? await getCertificatesFromPaths(certificatePaths) : [];

this.properties.systemCertsCount = String(systemCerts.length);
this.properties.fileCertsCount = String(filesCerts.length);

let certificates = systemCerts;
certificates.push(...filesCerts);

return certificates;
});
}

async function getCertificatesFromPaths(paths: string[]): Promise<Buffer[]> {
let certs: Buffer[] = [];

for (let certPath of paths) {
if (!path.isAbsolute(certPath)) {
// tslint:disable-next-line: no-floating-promises
ext.ui.showWarningMessage(`Certificate path "${certPath}" is not an absolute path, ignored.`);
} else {
let isFile = false;
let isFolder = false;
try {
if (await fse.pathExists(certPath)) {
let stat = await fse.stat(certPath);
isFolder = stat.isDirectory();
isFile = stat.isFile();
}
} catch {
// Ignore (could be permission issues, for instance)
}

let certFiles: string[] = [];
if (isFolder) {
let files = await globAsync('**', { absolute: true, nodir: true, cwd: certPath });
certFiles.push(...files);
} else if (isFile) {
certFiles.push(certPath);
} else {
console.log(`Could not find certificate path "${certPath}.`);
}

for (let cf of certFiles) {
certs.push(fse.readFileSync(cf));
}
}
}

return certs;
}

function getCertificatesFromSystem(): (string | Buffer)[] {
if (!_systemCertificates) {
// {win,mac}-ca automatically read trusted certificate authorities from the system and place them into the global
// Node agent. We don't want them in the global agent because that will affect all other extensions
// loaded in the same process, which will make them behave inconsistently depending on whether we're loaded.
let previousCertificateAuthorities = https.globalAgent.options.ca;
let certificates: string | Buffer | (string | Buffer)[] = [];

try {
if (isWindows()) {
require('win-ca');
} else if (isMac()) {
require('mac-ca');
} else if (isLinux()) {
}
} finally {
certificates = https.globalAgent.options.ca;
https.globalAgent.options.ca = previousCertificateAuthorities;
}

if (!certificates) {
certificates = [];
} else if (!Array.isArray(certificates)) {
certificates = [certificates];
}

_systemCertificates = certificates;
}

return _systemCertificates;
}

0 comments on commit 86afb81

Please sign in to comment.