Skip to content
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
34 changes: 0 additions & 34 deletions Extension/ThirdPartyNotices.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3241,40 +3241,6 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


---------------------------------------------------------

---------------------------------------------------------

shell-quote 1.8.4 - MIT
https://github.com/ljharb/shell-quote

Copyright (c) 2013 James Halliday (mail@substack.net)

The MIT License

Copyright (c) 2013 James Halliday (mail@substack.net)

Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
associated documentation files (the "Software"), to
deal in the Software without restriction, including
without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom
the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice
shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

---------------------------------------------------------

---------------------------------------------------------
Expand Down
2 changes: 0 additions & 2 deletions Extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6870,7 +6870,6 @@
"@types/plist": "^3.0.5",
"@types/proxyquire": "^1.3.31",
"@types/semver": "^7.5.8",
"@types/shell-quote": "^1.7.5",
"@types/sinon": "^21.0.0",
"@types/tmp": "^0.2.6",
"@types/which": "^2.0.2",
Expand Down Expand Up @@ -6926,7 +6925,6 @@
"node-vswhere": "^1.0.2",
"plist": "^3.1.0",
"posix-getopt": "^1.2.1",
"shell-quote": "1.8.4",
"ssh-config": "^4.4.4",
"tmp": "^0.2.7",
"vscode-cpptools": "^7.1.1",
Expand Down
75 changes: 71 additions & 4 deletions Extension/src/SSH/sshCommandToConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* ------------------------------------------------------------------------------------------ */

import { BasicParser, IParsedOption } from 'posix-getopt';
import { parse } from 'shell-quote';

/**
* Mapping of flags to functions that add the relevant flag to the map of
Expand Down Expand Up @@ -124,7 +123,11 @@ export class CommandParseError extends Error { }
* Attempts to convert an SSH command to an SSH config entry.
*/
export function sshCommandToConfig(command: string, name?: string): { [key: string]: string } {
const parts: string[] = parse(command) as string[];
// Split the command line into arguments. We deliberately use shell-like tokenization that
// strips single and double quotes and lets an unquoted backslash escape a following space,
// while keeping backslashes before other characters literal, so both Unix paths with escaped
// spaces (e.g. /home/me/my\ key) and Windows paths (e.g. C:\Users\me\key) are preserved.
const parts: string[] = splitArgs(command);
Comment thread
sean-mcmanus marked this conversation as resolved.

// ignore 'ssh' if the user entered that as their first word
if (parts[0] === 'ssh') {
Expand Down Expand Up @@ -167,6 +170,70 @@ export function sshCommandToConfig(command: string, name?: string): { [key: stri
return { Host, HostName, ...options };
}

/**
* Splits a command line into arguments using shell-like tokenization that behaves
* consistently across platforms. Both single and double quotes group their contents
* and are removed, and unquoted whitespace separates arguments.
*
* Outside of quotes, a backslash escapes only a following whitespace character (so a
* Unix path such as `/home/me/my\ key` keeps its space as a single argument). Before
* any other character a backslash is kept literal, so Windows paths such as
* `C:\Users\me\key` are preserved rather than being consumed as escape sequences.
*
* This tokenizer is intentionally lenient for a single-line input box: an unterminated
Comment thread
Colengms marked this conversation as resolved.
* quote is not treated as an error but simply runs to the end of the string.
*/
export function splitArgs(command: string): string[] {
const args: string[] = [];
let current: string = '';
let inToken: boolean = false;
let quoteChar: string | undefined;
for (let i: number = 0; i < command.length; i++) {
const c: string = command[i];
if (quoteChar !== undefined) {
if (c === quoteChar) {
quoteChar = undefined;
} else {
current += c;
}
continue;
}
if (c === '"' || c === '\'') {
quoteChar = c;
inToken = true;
continue;
}
if (c === '\\') {
const next: string | undefined = command[i + 1];
// Only escape a following whitespace character; otherwise keep the backslash
// literal so Windows path separators survive.
if (next === ' ' || next === '\t' || next === '\r' || next === '\n') {
current += next;
inToken = true;
i++;
continue;
}
current += c;
inToken = true;
continue;
}
if (c === ' ' || c === '\t' || c === '\r' || c === '\n') {
if (inToken) {
args.push(current);
current = '';
inToken = false;
}
continue;
}
Comment thread
sean-mcmanus marked this conversation as resolved.
current += c;
inToken = true;
}
if (inToken) {
args.push(current);
}
return args;
}

/**
* Parses flags from the given array of arguments, returning the index of the
* next non-flag in the input (or the total length of the input if none are found).
Expand Down Expand Up @@ -218,8 +285,8 @@ function parseFlags(input: string[], entries: { [key: string]: string }): number
* are not mentioned on the ssh(1) man page and don't seem to have use in the
* wild. In the OpenSSH source, they appear to be ignored[3].
*
* The `shell-quote` library, like libc does for OpenSSH, takes care of dealing
* with quotations for for us.
* The `splitArgs` tokenizer has already stripped any surrounding quotes before this
* function sees a token, so it only has to deal with the unquoted connection string.
*
* 1. https://github.com/openssh/openssh-portable/blob/e3b6c966b79c3ea5d51b923c3bbdc41e13b96ea0/ssh.c#L999
* 2. https://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04#section-3.3
Expand Down
95 changes: 95 additions & 0 deletions Extension/test/unit/sshCommandToConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */

import { deepStrictEqual, strictEqual } from 'assert';
import { describe, it } from 'mocha';
import { splitArgs, sshCommandToConfig } from '../../src/SSH/sshCommandToConfig';

// eslint-disable-next-line import/no-unassigned-import
require('source-map-support/register');

describe('splitArgs', () => {
// [description, input, expected tokens]
const cases: [string, string, string[]][] = [
['empty string', '', []],
['whitespace only', ' \t ', []],
['simple words', 'ssh user@host', ['ssh', 'user@host']],
['collapses runs of whitespace', 'ssh \t user@host', ['ssh', 'user@host']],
['trims leading/trailing whitespace', ' ssh user@host ', ['ssh', 'user@host']],

// Windows paths: backslashes must stay literal (the original bug).
['bare Windows path', 'ssh -i C:\\Users\\me\\key user@host', ['ssh', '-i', 'C:\\Users\\me\\key', 'user@host']],
['double-quoted Windows path with spaces', 'ssh -i "C:\\Program Files\\me\\key" user@host', ['ssh', '-i', 'C:\\Program Files\\me\\key', 'user@host']],
['single-quoted Windows path with spaces', "ssh -i 'C:\\Program Files\\me\\key' user@host", ['ssh', '-i', 'C:\\Program Files\\me\\key', 'user@host']],
['single-quoted Windows path without spaces', "ssh -i 'C:\\Users\\me\\key' user@host", ['ssh', '-i', 'C:\\Users\\me\\key', 'user@host']],

// Quote handling.
['strips double quotes', '"a b" c', ['a b', 'c']],
['strips single quotes', "'a b' c", ['a b', 'c']],
['quotes joined to adjacent text', 'a"b c"d', ['ab cd']],
['single quotes inside double quotes are literal', '"it\'s here"', ["it's here"]],
['double quotes inside single quotes are literal', "'say \"hi\"'", ['say "hi"']],
['empty double-quoted token is preserved', 'a "" b', ['a', '', 'b']],
['empty single-quoted token is preserved', "a '' b", ['a', '', 'b']],

// Forward-slash (POSIX-style) paths are unaffected.
['forward-slash path', 'ssh -i /home/me/.ssh/id_rsa user@host', ['ssh', '-i', '/home/me/.ssh/id_rsa', 'user@host']],

// An unquoted backslash escapes a following whitespace character (POSIX behavior), so a
// Unix path with an escaped space stays a single argument.
['backslash escapes a space', 'ssh -i /home/me/my\\ key user@host', ['ssh', '-i', '/home/me/my key', 'user@host']],
['backslash escapes multiple spaces', 'ssh -i /home/me/key\\ with\\ spaces user@host', ['ssh', '-i', '/home/me/key with spaces', 'user@host']],
['backslash escapes a tab', 'a\\\tb', ['a\tb']],
['trailing backslash is literal', 'foo\\', ['foo\\']],
['backslash before a letter stays literal (Windows path)', 'C:\\Users\\me', ['C:\\Users\\me']],
['UNC path keeps doubled backslashes', '\\\\server\\share', ['\\\\server\\share']],
// A backslash that ends a quoted segment is literal and must not escape the following
// separator (only an unquoted backslash directly before whitespace escapes).
['quoted path ending in backslash is not joined to the next arg', '"C:\\Program Files\\" next', ['C:\\Program Files\\', 'next']],

// Lenient handling of an unterminated quote: runs to end of string.
['unterminated double quote runs to end', 'ssh -i "C:\\Users\\me', ['ssh', '-i', 'C:\\Users\\me']],
['unterminated single quote runs to end', "ssh -i 'C:\\Users\\me", ['ssh', '-i', 'C:\\Users\\me']]
];

for (const [description, input, expected] of cases) {
it(`${description}: ${JSON.stringify(input)}`, () => {
deepStrictEqual(splitArgs(input), expected);
});
}
});

describe('sshCommandToConfig', () => {
it('preserves a bare Windows identity-file path', () => {
const config = sshCommandToConfig('ssh -i C:\\Users\\me\\.ssh\\id_rsa user@host');
strictEqual(config.IdentityFile, 'C:\\Users\\me\\.ssh\\id_rsa');
strictEqual(config.HostName, 'host');
strictEqual(config.User, 'user');
});

it('preserves a single-quoted Windows identity-file path with spaces', () => {
const config = sshCommandToConfig("ssh -i 'C:\\Program Files\\me\\key' user@host");
strictEqual(config.IdentityFile, 'C:\\Program Files\\me\\key');
});

it('preserves a double-quoted Windows identity-file path with spaces', () => {
const config = sshCommandToConfig('ssh -i "C:\\Program Files\\me\\key" user@host');
strictEqual(config.IdentityFile, 'C:\\Program Files\\me\\key');
});

it('preserves a Unix identity-file path with a backslash-escaped space', () => {
const config = sshCommandToConfig('ssh -i /home/me/my\\ key user@host');
strictEqual(config.IdentityFile, '/home/me/my key');
strictEqual(config.HostName, 'host');
strictEqual(config.User, 'user');
});

it('parses host, user, and port from the connection string', () => {
const config = sshCommandToConfig('ssh -p 2222 user@host');
strictEqual(config.HostName, 'host');
strictEqual(config.User, 'user');
strictEqual(config.Port, '2222');
});
});
10 changes: 0 additions & 10 deletions Extension/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -896,11 +896,6 @@
resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528"
integrity sha1-POOvGlUk7zJ9Lank/YttlcjXBSg=

"@types/shell-quote@^1.7.5":
version "1.7.5"
resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/@types/shell-quote/-/shell-quote-1.7.5.tgz#6db4704742d307cd6d604e124e3ad6cd5ed943f3"
integrity sha1-bbRwR0LTB81tYE4STjrWzV7ZQ/M=

"@types/sinon@^21.0.0":
version "21.0.0"
resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/@types/sinon/-/sinon-21.0.0.tgz#3a598a29b3aec0512a21e57ae0fd4c09aa013ca9"
Expand Down Expand Up @@ -5508,11 +5503,6 @@ shebang-regex@^3.0.0:
resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=

shell-quote@1.8.4:
version "1.8.4"
resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190"
integrity sha1-Lt2aTc78lmSeLiyxL2N7Hx2SoZA=

side-channel-list@^1.0.0:
version "1.0.0"
resolved "https://pkgs.dev.azure.com/azure-public/VisualCpp/_packaging/cpp_PublicPackages/npm/registry/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
Expand Down
Loading