Quote VS Code Dosh proxy commands on Windows
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
This commit is contained in:
@@ -70,6 +70,8 @@ jobs:
|
|||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
- name: Test macOS client
|
- name: Test macOS client
|
||||||
run: cargo test --all-targets
|
run: cargo test --all-targets
|
||||||
|
- name: Test VS Code extension helpers
|
||||||
|
run: node --test vscode-extension/extension.test.js
|
||||||
|
|
||||||
windows-client:
|
windows-client:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
@@ -90,6 +92,8 @@ jobs:
|
|||||||
run: cargo build --release --bin dosh-client --bin dosh-bench
|
run: cargo build --release --bin dosh-client --bin dosh-bench
|
||||||
- name: Test Windows client
|
- name: Test Windows client
|
||||||
run: cargo test --all-targets
|
run: cargo test --all-targets
|
||||||
|
- name: Test VS Code extension helpers
|
||||||
|
run: node --test vscode-extension/extension.test.js
|
||||||
- name: Package Windows client
|
- name: Package Windows client
|
||||||
shell: bash
|
shell: bash
|
||||||
run: sh scripts/package-release.sh
|
run: sh scripts/package-release.sh
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ fn release_gates_test_all_targets() {
|
|||||||
macos_client.contains("run: cargo test --all-targets"),
|
macos_client.contains("run: cargo test --all-targets"),
|
||||||
"macOS CI must run Rust tests, not only release packaging"
|
"macOS CI must run Rust tests, not only release packaging"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
macos_client.contains("run: node --test vscode-extension/extension.test.js"),
|
||||||
|
"macOS CI must exercise VS Code helper quoting"
|
||||||
|
);
|
||||||
let windows_client = ci
|
let windows_client = ci
|
||||||
.split("windows-client:")
|
.split("windows-client:")
|
||||||
.nth(1)
|
.nth(1)
|
||||||
@@ -100,6 +104,10 @@ fn release_gates_test_all_targets() {
|
|||||||
windows_client.contains("run: cargo test --all-targets"),
|
windows_client.contains("run: cargo test --all-targets"),
|
||||||
"Windows CI must run Rust tests, not only build/package"
|
"Windows CI must run Rust tests, not only build/package"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
windows_client.contains("run: node --test vscode-extension/extension.test.js"),
|
||||||
|
"Windows CI must exercise VS Code helper quoting"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,22 +1,37 @@
|
|||||||
const vscode = require('vscode');
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
let vscode;
|
||||||
|
try {
|
||||||
|
vscode = require('vscode');
|
||||||
|
} catch (_) {
|
||||||
|
vscode = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireVscode() {
|
||||||
|
if (!vscode) {
|
||||||
|
throw new Error('VS Code API is only available inside VS Code');
|
||||||
|
}
|
||||||
|
return vscode;
|
||||||
|
}
|
||||||
|
|
||||||
function activate(context) {
|
function activate(context) {
|
||||||
|
const api = requireVscode();
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand('dosh.openRemote', openRemote),
|
api.commands.registerCommand('dosh.openRemote', openRemote),
|
||||||
vscode.commands.registerCommand('dosh.configureHost', configureHostCommand),
|
api.commands.registerCommand('dosh.configureHost', configureHostCommand),
|
||||||
vscode.commands.registerCommand('dosh.showSshConfig', showSshConfig)
|
api.commands.registerCommand('dosh.showSshConfig', showSshConfig)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openRemote() {
|
async function openRemote() {
|
||||||
|
const api = requireVscode();
|
||||||
const configured = await configureHost();
|
const configured = await configureHost();
|
||||||
if (!configured) {
|
if (!configured) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const remotePath = await vscode.window.showInputBox({
|
const remotePath = await api.window.showInputBox({
|
||||||
title: 'Remote path',
|
title: 'Remote path',
|
||||||
prompt: 'Path to open on the remote host',
|
prompt: 'Path to open on the remote host',
|
||||||
value: '~'
|
value: '~'
|
||||||
@@ -25,22 +40,24 @@ async function openRemote() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const suffix = remotePath ? `/${remotePath.replace(/^\/+/, '')}` : '';
|
const suffix = remotePath ? `/${remotePath.replace(/^\/+/, '')}` : '';
|
||||||
await vscode.commands.executeCommand(
|
await api.commands.executeCommand(
|
||||||
'vscode.openFolder',
|
'vscode.openFolder',
|
||||||
vscode.Uri.parse(`vscode-remote://ssh-remote+${configured.alias}${suffix}`),
|
api.Uri.parse(`vscode-remote://ssh-remote+${configured.alias}${suffix}`),
|
||||||
{ forceNewWindow: true }
|
{ forceNewWindow: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function configureHostCommand() {
|
async function configureHostCommand() {
|
||||||
|
const api = requireVscode();
|
||||||
const configured = await configureHost();
|
const configured = await configureHost();
|
||||||
if (configured) {
|
if (configured) {
|
||||||
vscode.window.showInformationMessage(`Dosh Remote-SSH host ready: ${configured.alias}`);
|
api.window.showInformationMessage(`Dosh Remote-SSH host ready: ${configured.alias}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function configureHost() {
|
async function configureHost() {
|
||||||
const host = await vscode.window.showInputBox({
|
const api = requireVscode();
|
||||||
|
const host = await api.window.showInputBox({
|
||||||
title: 'Dosh host',
|
title: 'Dosh host',
|
||||||
prompt: 'Dosh host alias, e.g. palav',
|
prompt: 'Dosh host alias, e.g. palav',
|
||||||
ignoreFocusOut: true
|
ignoreFocusOut: true
|
||||||
@@ -48,12 +65,12 @@ async function configureHost() {
|
|||||||
if (!host) {
|
if (!host) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const user = await vscode.window.showInputBox({
|
const user = await api.window.showInputBox({
|
||||||
title: 'Remote SSH user',
|
title: 'Remote SSH user',
|
||||||
prompt: 'Optional. Leave blank to let SSH config decide.',
|
prompt: 'Optional. Leave blank to let SSH config decide.',
|
||||||
ignoreFocusOut: true
|
ignoreFocusOut: true
|
||||||
});
|
});
|
||||||
const config = vscode.workspace.getConfiguration('dosh');
|
const config = api.workspace.getConfiguration('dosh');
|
||||||
const alias = `dosh-${safeAlias(host)}`;
|
const alias = `dosh-${safeAlias(host)}`;
|
||||||
const block = sshBlock({
|
const block = sshBlock({
|
||||||
alias,
|
alias,
|
||||||
@@ -62,7 +79,8 @@ async function configureHost() {
|
|||||||
executable: config.get('executable') || 'dosh',
|
executable: config.get('executable') || 'dosh',
|
||||||
targetHost: config.get('targetHost') || '127.0.0.1',
|
targetHost: config.get('targetHost') || '127.0.0.1',
|
||||||
targetPort: Number(config.get('targetPort') || 22),
|
targetPort: Number(config.get('targetPort') || 22),
|
||||||
doshPort: Number(config.get('doshPort') || 0)
|
doshPort: Number(config.get('doshPort') || 0),
|
||||||
|
platform: process.platform
|
||||||
});
|
});
|
||||||
const sshDir = path.join(os.homedir(), '.ssh');
|
const sshDir = path.join(os.homedir(), '.ssh');
|
||||||
fs.mkdirSync(sshDir, { recursive: true });
|
fs.mkdirSync(sshDir, { recursive: true });
|
||||||
@@ -74,11 +92,12 @@ async function configureHost() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sshBlock(options) {
|
function sshBlock(options) {
|
||||||
let proxy = shellQuote(options.executable);
|
const platform = options.platform || process.platform;
|
||||||
|
let proxy = sshConfigWord(options.executable, platform);
|
||||||
if (options.doshPort > 0) {
|
if (options.doshPort > 0) {
|
||||||
proxy += ` --dosh-port ${options.doshPort}`;
|
proxy += ` --dosh-port ${options.doshPort}`;
|
||||||
}
|
}
|
||||||
proxy += ` proxy-stdio ${shellQuote(options.host)} %h %p`;
|
proxy += ` proxy-stdio ${sshConfigWord(options.host, platform)} %h %p`;
|
||||||
const lines = [
|
const lines = [
|
||||||
`# BEGIN DOSH ${options.alias}`,
|
`# BEGIN DOSH ${options.alias}`,
|
||||||
`Host ${options.alias}`,
|
`Host ${options.alias}`,
|
||||||
@@ -139,15 +158,16 @@ function upsertBlock(configPath, alias, block) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function showSshConfig() {
|
async function showSshConfig() {
|
||||||
const config = vscode.workspace.getConfiguration('dosh');
|
const api = requireVscode();
|
||||||
|
const config = api.workspace.getConfiguration('dosh');
|
||||||
const sshDir = path.join(os.homedir(), '.ssh');
|
const sshDir = path.join(os.homedir(), '.ssh');
|
||||||
const generatedPath = config.get('generatedSshConfig') || path.join(sshDir, 'config.dosh');
|
const generatedPath = config.get('generatedSshConfig') || path.join(sshDir, 'config.dosh');
|
||||||
if (!fs.existsSync(generatedPath)) {
|
if (!fs.existsSync(generatedPath)) {
|
||||||
vscode.window.showWarningMessage('No generated Dosh SSH config yet.');
|
api.window.showWarningMessage('No generated Dosh SSH config yet.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const doc = await vscode.workspace.openTextDocument(generatedPath);
|
const doc = await api.workspace.openTextDocument(generatedPath);
|
||||||
await vscode.window.showTextDocument(doc);
|
await api.window.showTextDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeAlias(value) {
|
function safeAlias(value) {
|
||||||
@@ -162,9 +182,43 @@ function shellQuote(value) {
|
|||||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sshConfigWord(value, platform = process.platform) {
|
||||||
|
if (platform === 'win32') {
|
||||||
|
return windowsCommandWord(value);
|
||||||
|
}
|
||||||
|
return shellQuote(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowsCommandWord(value) {
|
||||||
|
let out = '"';
|
||||||
|
let backslashes = 0;
|
||||||
|
for (const ch of value) {
|
||||||
|
if (ch === '\\') {
|
||||||
|
backslashes += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '"') {
|
||||||
|
out += '\\'.repeat(backslashes * 2 + 1);
|
||||||
|
out += '"';
|
||||||
|
backslashes = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out += '\\'.repeat(backslashes);
|
||||||
|
backslashes = 0;
|
||||||
|
out += ch;
|
||||||
|
}
|
||||||
|
out += '\\'.repeat(backslashes * 2);
|
||||||
|
out += '"';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function deactivate() {}
|
function deactivate() {}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
activate,
|
activate,
|
||||||
deactivate
|
deactivate,
|
||||||
|
sshBlock,
|
||||||
|
shellQuote,
|
||||||
|
sshConfigWord,
|
||||||
|
windowsCommandWord
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const {
|
||||||
|
shellQuote,
|
||||||
|
sshBlock,
|
||||||
|
sshConfigWord,
|
||||||
|
windowsCommandWord
|
||||||
|
} = require('./extension');
|
||||||
|
|
||||||
|
test('posix ssh config words use shell quoting', () => {
|
||||||
|
assert.equal(
|
||||||
|
sshConfigWord('/Users/palav/My App/dosh', 'darwin'),
|
||||||
|
"'/Users/palav/My App/dosh'"
|
||||||
|
);
|
||||||
|
assert.equal(shellQuote("say 'hi'"), "'say '\\''hi'\\'''");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('windows ssh config words use Windows command quoting', () => {
|
||||||
|
assert.equal(
|
||||||
|
sshConfigWord('C:\\Program Files\\Dosh\\dosh.exe', 'win32'),
|
||||||
|
'"C:\\Program Files\\Dosh\\dosh.exe"'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
windowsCommandWord('C:\\path\\before"quote'),
|
||||||
|
'"C:\\path\\before\\"quote"'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
windowsCommandWord('C:\\Program Files\\Dosh\\'),
|
||||||
|
'"C:\\Program Files\\Dosh\\\\"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ssh block quotes proxy command for the target platform', () => {
|
||||||
|
const block = sshBlock({
|
||||||
|
alias: 'dosh-work',
|
||||||
|
host: 'work host',
|
||||||
|
executable: 'C:\\Program Files\\Dosh\\dosh.exe',
|
||||||
|
targetHost: '127.0.0.1',
|
||||||
|
targetPort: 22,
|
||||||
|
doshPort: 50000,
|
||||||
|
platform: 'win32'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
block,
|
||||||
|
/ProxyCommand "C:\\Program Files\\Dosh\\dosh\.exe" --dosh-port 50000 proxy-stdio "work host" %h %p/
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(block, /'C:\\Program Files\\Dosh\\dosh\.exe'/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user