Skip to content

Commit

Permalink
feat: runVSCodeCommand as workaround for CVE-2024-27980
Browse files Browse the repository at this point in the history
  • Loading branch information
connor4312 committed May 13, 2024
1 parent 40ecedf commit 3f7a3cc
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 84 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### 2.3.10 | 2024-01-19

- Add `runVSCodeCommand` method and workaround for Node CVE-2024-27980

### 2.3.9 | 2024-01-19

- Fix archive extraction on Windows failing when run under Electron
Expand Down
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![Test Status Badge](https://github.com/microsoft/vscode-test/workflows/Tests/badge.svg)

This module helps you test VS Code extensions.
This module helps you test VS Code extensions. Note that new extensions may want to use the [VS Code Test CLI](https://github.com/microsoft/vscode-test-cli/blob/main/README.md), which leverages this module, for a richer editing and execution experience.

Supported:

Expand All @@ -13,10 +13,10 @@ Supported:

## Usage

See [./sample](./sample) for a runnable sample, with [Azure DevOps Pipelines](https://github.com/microsoft/vscode-test/blob/master/sample/azure-pipelines.yml) and [Travis CI](https://github.com/microsoft/vscode-test/blob/master/.travis.yml) configuration.
See [./sample](./sample) for a runnable sample, with [Azure DevOps Pipelines](https://github.com/microsoft/vscode-test/blob/main/sample/azure-pipelines.yml) and [Github ACtions](https://github.com/microsoft/vscode-test/blob/main/sample/.travis.yml) configuration.

```ts
import { runTests } from '@vscode/test-electron';
import { runTests, runVSCodeCommand, downloadAndUnzipVSCode } from '@vscode/test-electron';

async function go() {
try {
Expand Down Expand Up @@ -82,11 +82,7 @@ async function go() {
/**
* Install Python extension
*/
const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);
cp.spawnSync(cli, [...args, '--install-extension', 'ms-python.python'], {
encoding: 'utf-8',
stdio: 'inherit',
});
await runVSCodeCommand(['--install-extension', 'ms-python.python'], { version: '1.35.0' });

/**
* - Add additional launch flags for VS Code
Expand Down
3 changes: 2 additions & 1 deletion lib/download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ describe('sane downloads', () => {
}

if (platform === systemDefaultPlatform) {
const version = spawnSync(exePath, ['--version']);
const shell = process.platform === 'win32';
const version = spawnSync(shell ? `"${exePath}"` : exePath, ['--version'], { shell });
expect(version.status).to.equal(0);
expect(version.stdout.toString().trim()).to.not.be.empty;
}
Expand Down
65 changes: 58 additions & 7 deletions lib/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,64 @@ export type DownloadPlatform = StringLiteralUnion<
>;

export interface DownloadOptions {
readonly cachePath: string;
readonly version: DownloadVersion;
readonly platform: DownloadPlatform;
readonly extensionDevelopmentPath?: string | string[];
readonly reporter?: ProgressReporter;
readonly extractSync?: boolean;
readonly timeout?: number;
/**
* The VS Code version to download. Valid versions are:
* - `'stable'`
* - `'insiders'`
* - `'1.32.0'`, `'1.31.1'`, etc
*
* Defaults to `stable`, which is latest stable version.
*
* *If a local copy exists at `.vscode-test/vscode-<VERSION>`, skip download.*
*/
version: DownloadVersion;

/**
* The VS Code platform to download. If not specified, it defaults to the
* current platform.
*
* Possible values are:
* - `win32-x64-archive`
* - `win32-arm64-archive `
* - `darwin`
* - `darwin-arm64`
* - `linux-x64`
* - `linux-arm64`
* - `linux-armhf`
*/
platform: DownloadPlatform;

/**
* Path where the downloaded VS Code instance is stored.
* Defaults to `.vscode-test` within your working directory folder.
*/
cachePath: string;

/**
* Absolute path to the extension root. Passed to `--extensionDevelopmentPath`.
* Must include a `package.json` Extension Manifest.
*/
extensionDevelopmentPath?: string | string[];

/**
* Progress reporter to use while VS Code is downloaded. Defaults to a
* console reporter. A {@link SilentReporter} is also available, and you
* may implement your own.
*/
reporter?: ProgressReporter;

/**
* Whether the downloaded zip should be synchronously extracted. Should be
* omitted unless you're experiencing issues installing VS Code versions.
*/
extractSync?: boolean;

/**
* Number of milliseconds after which to time out if no data is received from
* the remote when downloading VS Code. Note that this is an 'idle' timeout
* and does not enforce the total time VS Code may take to download.
*/
timeout?: number;
}

interface IDownload {
Expand Down
12 changes: 9 additions & 3 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

export { download, downloadAndUnzipVSCode } from './download';
export { runTests } from './runTest';
export { resolveCliPathFromVSCodeExecutablePath, resolveCliArgsFromVSCodeExecutablePath } from './util';
export { download, downloadAndUnzipVSCode, DownloadOptions } from './download';
export { runTests, TestOptions } from './runTest';
export {
resolveCliPathFromVSCodeExecutablePath,
resolveCliArgsFromVSCodeExecutablePath,
runVSCodeCommand,
VSCodeCommandError,
RunVSCodeCommandOptions,
} from './util';
export * from './progress';
56 changes: 5 additions & 51 deletions lib/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@

import * as cp from 'child_process';
import * as path from 'path';
import { downloadAndUnzipVSCode, DownloadVersion, DownloadPlatform, defaultCachePath } from './download';
import { ProgressReporter } from './progress';
import { DownloadOptions, defaultCachePath, downloadAndUnzipVSCode } from './download';
import { killTree } from './util';

export interface TestOptions {
export interface TestOptions extends Partial<DownloadOptions> {
/**
* The VS Code executable path used for testing.
*
Expand All @@ -18,33 +17,6 @@ export interface TestOptions {
*/
vscodeExecutablePath?: string;

/**
* The VS Code version to download. Valid versions are:
* - `'stable'`
* - `'insiders'`
* - `'1.32.0'`, `'1.31.1'`, etc
*
* Defaults to `stable`, which is latest stable version.
*
* *If a local copy exists at `.vscode-test/vscode-<VERSION>`, skip download.*
*/
version?: DownloadVersion;

/**
* The VS Code platform to download. If not specified, it defaults to the
* current platform.
*
* Possible values are:
* - `win32-x64-archive`
* - `win32-arm64-archive `
* - `darwin`
* - `darwin-arm64`
* - `linux-x64`
* - `linux-arm64`
* - `linux-armhf`
*/
platform?: DownloadPlatform;

/**
* Whether VS Code should be launched using default settings and extensions
* installed on this machine. If `false`, then separate directories will be
Expand Down Expand Up @@ -95,26 +67,6 @@ export interface TestOptions {
* See `code --help` for possible arguments.
*/
launchArgs?: string[];

/**
* Progress reporter to use while VS Code is downloaded. Defaults to a
* console reporter. A {@link SilentReporter} is also available, and you
* may implement your own.
*/
reporter?: ProgressReporter;

/**
* Whether the downloaded zip should be synchronously extracted. Should be
* omitted unless you're experiencing issues installing VS Code versions.
*/
extractSync?: boolean;

/**
* Number of milliseconds after which to time out if no data is received from
* the remote when downloading VS Code. Note that this is an 'idle' timeout
* and does not enforce the total time VS Code may take to download.
*/
timeout?: number;
}

/**
Expand Down Expand Up @@ -185,7 +137,9 @@ async function innerRunTests(
}
): Promise<number> {
const fullEnv = Object.assign({}, process.env, testRunnerEnv);
const cmd = cp.spawn(executable, args, { env: fullEnv });
const shell = process.platform === 'win32';
const cmd = cp.spawn(shell ? `"${executable}"` : executable, args, { env: fullEnv, shell });

let exitRequested = false;
const ctrlc1 = () => {
process.removeListener(SIGINT, ctrlc1);
Expand Down
55 changes: 52 additions & 3 deletions lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ChildProcess, spawn } from 'child_process';
import { ChildProcess, SpawnOptions, spawn } from 'child_process';
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
import * as createHttpProxyAgent from 'http-proxy-agent';
import * as https from 'https';
import * as createHttpsProxyAgent from 'https-proxy-agent';
import * as path from 'path';
import { URL } from 'url';
import { DownloadPlatform } from './download';
import { DownloadOptions, DownloadPlatform, downloadAndUnzipVSCode } from './download';
import * as request from './request';
import { TestOptions, getProfileArguments } from './runTest';
import { createHash } from 'crypto';

export let systemDefaultPlatform: DownloadPlatform;

Expand Down Expand Up @@ -176,6 +176,7 @@ export function resolveCliPathFromVSCodeExecutablePath(
* cp.spawnSync(cli, [...args, '--install-extension', '<EXTENSION-ID-OR-PATH-TO-VSIX>'], {
* encoding: 'utf-8',
* stdio: 'inherit'
* shell: process.platform === 'win32',
* });
* ```
*
Expand All @@ -195,6 +196,54 @@ export function resolveCliArgsFromVSCodeExecutablePath(
return args;
}

export type RunVSCodeCommandOptions = Partial<DownloadOptions> & { spawn?: SpawnOptions };

export class VSCodeCommandError extends Error {
constructor(
args: string[],
public readonly exitCode: number | null,
public readonly stderr: string,
public stdout: string
) {
super(`'code ${args.join(' ')}' failed with exit code ${exitCode}:\n\n${stderr}\n\n${stdout}`);
}
}

/**
* Runs a VS Code command, and returns its output
* @throws a {@link VSCodeCommandError} if the command fails
*/
export async function runVSCodeCommand(args: string[], options: RunVSCodeCommandOptions = {}) {
const vscodeExecutablePath = await downloadAndUnzipVSCode(options);
const [cli, ...baseArgs] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);

const shell = process.platform === 'win32';

return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
let stdout = '';
let stderr = '';

const child = spawn(shell ? `"${cli}"` : cli, [...baseArgs, ...args], {
stdio: 'pipe',
shell,
windowsHide: true,
...options.spawn,
});

child.stdout?.setEncoding('utf-8').on('data', (data) => (stdout += data));
child.stderr?.setEncoding('utf-8').on('data', (data) => (stderr += data));

child.on('error', reject);
child.on('exit', (code) => {
if (code !== 0) {
reject(new VSCodeCommandError(args, code, stderr, stdout));
} else {
resolve({ stdout, stderr });
}
});
});
}

/** Predicates whether arg is undefined or null */
export function isDefined<T>(arg: T | undefined | null): arg is T {
return arg != null;
Expand Down
35 changes: 35 additions & 0 deletions sample/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Run VSCode Extension Tests

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: yarn install

- name: Compile
run: yarn compile

- name: Run tests
run: xvfb-run -a yarn test
if: runner.os == 'Linux'

- name: Run tests
run: yarn test
if: runner.os != 'Linux'
Loading

0 comments on commit 3f7a3cc

Please sign in to comment.