Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load NODE_EXTRA_CA_CERTS in Electron #559

Merged
merged 1 commit into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/spec-configuration/controlManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function getControlManifest(cacheFolder: string, output: Log): Prom
async function updateControlManifest(controlManifestPath: string, oldManifest: DevContainerControlManifest | undefined, output: Log): Promise<DevContainerControlManifest> {
let manifestBuffer: Buffer;
try {
manifestBuffer = await fetchControlManifest();
manifestBuffer = await fetchControlManifest(output);
} catch (error) {
output.write(`Failed to fetch control manifest: ${error.message}`, LogLevel.Error);
if (oldManifest) {
Expand All @@ -81,15 +81,15 @@ async function updateControlManifest(controlManifestPath: string, oldManifest: D
return sanitizeControlManifest(jsonc.parse(manifestBuffer.toString()));
}

async function fetchControlManifest() {
async function fetchControlManifest(output: Log) {
return request({
type: 'GET',
url: 'https://containers.dev/static/devcontainer-control-manifest.json',
headers: {
'user-agent': 'devcontainers-vscode',
'accept': 'application/json',
},
});
}, output);
}

function sanitizeControlManifest(manifest: any): DevContainerControlManifest {
Expand Down
71 changes: 55 additions & 16 deletions src/spec-utils/httpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,33 @@ import type { RequestOptions } from 'https';
import { https, http } from 'follow-redirects';
import { ProxyAgent } from 'proxy-agent';
import * as url from 'url';
import * as tls from 'tls';
import { Log, LogLevel } from './log';
import { readLocalFile } from './pfs';

export function request(options: { type: string; url: string; headers: Record<string, string>; data?: Buffer }, output?: Log) {
export async function request(options: { type: string; url: string; headers: Record<string, string>; data?: Buffer }, output: Log) {
const secureContext = await secureContextWithExtraCerts(output);
return new Promise<Buffer>((resolve, reject) => {
const parsed = new url.URL(options.url);
const reqOptions: RequestOptions = {
const reqOptions: RequestOptions & tls.CommonConnectionOptions = {
hostname: parsed.hostname,
port: parsed.port,
path: parsed.pathname + parsed.search,
method: options.type,
headers: options.headers,
agent: new ProxyAgent(),
secureContext,
};

const plainHTTP = parsed.protocol === 'http:' || parsed.hostname === 'localhost';
if (output && plainHTTP) {
if (plainHTTP) {
output.write('Sending as plain HTTP request', LogLevel.Warning);
}

const req = (plainHTTP ? http : https).request(reqOptions, res => {
if (res.statusCode! < 200 || res.statusCode! > 299) {
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
if (output) {
output.write(`[-] HTTP request failed with status code ${res.statusCode}: : ${res.statusMessage}`, LogLevel.Trace);
}
output.write(`[-] HTTP request failed with status code ${res.statusCode}: : ${res.statusMessage}`, LogLevel.Trace);
} else {
res.on('error', reject);
const chunks: Buffer[] = [];
Expand All @@ -48,28 +50,28 @@ export function request(options: { type: string; url: string; headers: Record<st
}

// HTTP HEAD request that returns status code.
export function headRequest(options: { url: string; headers: Record<string, string> }, output?: Log) {
export async function headRequest(options: { url: string; headers: Record<string, string> }, output: Log) {
const secureContext = await secureContextWithExtraCerts(output);
return new Promise<number>((resolve, reject) => {
const parsed = new url.URL(options.url);
const reqOptions: RequestOptions = {
const reqOptions: RequestOptions & tls.CommonConnectionOptions = {
hostname: parsed.hostname,
port: parsed.port,
path: parsed.pathname + parsed.search,
method: 'HEAD',
headers: options.headers,
agent: new ProxyAgent(),
secureContext,
};

const plainHTTP = parsed.protocol === 'http:' || parsed.hostname === 'localhost';
if (output && plainHTTP) {
if (plainHTTP) {
output.write('Sending as plain HTTP request', LogLevel.Warning);
}

const req = (plainHTTP ? http : https).request(reqOptions, res => {
res.on('error', reject);
if (output) {
output.write(`HEAD ${options.url} -> ${res.statusCode}`, LogLevel.Trace);
}
output.write(`HEAD ${options.url} -> ${res.statusCode}`, LogLevel.Trace);
resolve(res.statusCode!);
});
req.on('error', reject);
Expand All @@ -79,20 +81,22 @@ export function headRequest(options: { url: string; headers: Record<string, stri

// Send HTTP Request.
// Does not throw on status code, but rather always returns 'statusCode', 'resHeaders', and 'resBody'.
export function requestResolveHeaders(options: { type: string; url: string; headers: Record<string, string>; data?: Buffer }, output?: Log) {
export async function requestResolveHeaders(options: { type: string; url: string; headers: Record<string, string>; data?: Buffer }, output: Log) {
const secureContext = await secureContextWithExtraCerts(output);
return new Promise<{ statusCode: number; resHeaders: Record<string, string>; resBody: Buffer }>((resolve, reject) => {
const parsed = new url.URL(options.url);
const reqOptions: RequestOptions = {
const reqOptions: RequestOptions & tls.CommonConnectionOptions = {
hostname: parsed.hostname,
port: parsed.port,
path: parsed.pathname + parsed.search,
method: options.type,
headers: options.headers,
agent: new ProxyAgent(),
secureContext,
};

const plainHTTP = parsed.protocol === 'http:' || parsed.hostname === 'localhost';
if (output && plainHTTP) {
if (plainHTTP) {
output.write('Sending as plain HTTP request', LogLevel.Warning);
}

Expand All @@ -118,4 +122,39 @@ export function requestResolveHeaders(options: { type: string; url: string; head
req.on('error', reject);
req.end();
});
}
}

let _secureContextWithExtraCerts: Promise<tls.SecureContext | undefined> | undefined;

async function secureContextWithExtraCerts(output: Log, options?: tls.SecureContextOptions) {
// Work around https://github.com/electron/electron/issues/10257.

if (_secureContextWithExtraCerts) {
return _secureContextWithExtraCerts;
}

return _secureContextWithExtraCerts = (async () => {
if (!process.versions.electron || !process.env.NODE_EXTRA_CA_CERTS) {
return undefined;
}

try {
const content = await readLocalFile(process.env.NODE_EXTRA_CA_CERTS, { encoding: 'utf8' });
const certs = (content.split(/(?=-----BEGIN CERTIFICATE-----)/g)
.filter(pem => !!pem.length));
output.write(`Loading ${certs.length} extra certificates from ${process.env.NODE_EXTRA_CA_CERTS}.`);
if (!certs.length) {
return undefined;
}

const secureContext = tls.createSecureContext(options);
for (const cert of certs) {
secureContext.context.addCACert(cert);
}
return secureContext;
} catch (err) {
output.write(`Error loading extra certificates from ${process.env.NODE_EXTRA_CA_CERTS}: ${err.message}`, LogLevel.Error);
return undefined;
}
})();
}