From 3e7089815f65693dca91472d832b389a8c2ad585 Mon Sep 17 00:00:00 2001 From: Kelvin Schoofs Date: Mon, 25 Feb 2019 18:46:01 +0100 Subject: [PATCH] Add initial support for sudo sftp --- src/connect.ts | 131 +++++++++++++++++++++++++++++++++++++------------ src/manager.ts | 3 ++ 2 files changed, 102 insertions(+), 32 deletions(-) diff --git a/src/connect.ts b/src/connect.ts index 16e2ac3..5d7f073 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -1,6 +1,6 @@ import { readFile } from 'fs'; import { Socket } from 'net'; -import { Client, ConnectConfig, SFTPWrapper as SFTPWrapperReal } from 'ssh2'; +import { Client, ClientChannel, ConnectConfig, SFTPWrapper as SFTPWrapperReal } from 'ssh2'; import { SFTPStream } from 'ssh2-streams'; import * as vscode from 'vscode'; import { loadConfigs, openConfigurationEditor } from './config'; @@ -122,7 +122,6 @@ export async function calculateActualConfig(config: FileSystemConfig): Promise { + client.once('error', (error: Error & { description?: string }) => { if (error.description) { error.message = `${error.description}\n${error.message}`; } @@ -207,37 +206,105 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable }); } -export function getSFTP(client: Client, config: FileSystemConfig): Promise { +function startSudo(shell: ClientChannel, config: FileSystemConfig, user: string | boolean = true): Promise { + Logging.debug(`Turning shell into a sudo shell for ${typeof user === 'string' ? user : 'default sudo user'}`); return new Promise((resolve, reject) => { - if (!config.sftpCommand) { - Logging.info(`Creating SFTP session using standard sftp subsystem`); - return client.sftp((err, sftp) => { - if (err) { - client.end(); - reject(err); - } - resolve(sftp); - }); - } - Logging.info(`Creating SFTP session for ${config.name} using specified command: ${config.sftpCommand}`); - client.exec(config.sftpCommand, (err, channel) => { - if (err) { - Logging.error(`Couldn't create SFTP session for ${config.name} using specified command: ${config.sftpCommand}\n${err}`); - client.end(); - return reject(err); + function stdout(data: Buffer | string) { + data = data.toString(); + if (data.trim() === 'SUDO OK') { + return cleanup(), resolve(); + } else { + Logging.debug(`Unexpected STDOUT: ${data}`); } - channel.once('close', () => (client.end(), reject())); - channel.once('error', () => (client.end(), reject())); - try { - Logging.debug(`\tSFTP session created, wrapping resulting channel in SFTPWrapper`); - const sftps = new SFTPStream(); - channel.pipe(sftps).pipe(channel); - const sftp = new SFTPWrapper(sftps); - resolve(sftp); - } catch (e) { - Logging.error(`Couldn't wrap SFTP session for ${config.name} using specified command: ${config.sftpCommand}\n${err}`); - reject(e); + } + async function stderr(data: Buffer | string) { + data = data.toString(); + if (data.match(/^\[sudo\]/)) { + const password = typeof config.password === 'string' ? config.password : + await vscode.window.showInputBox({ + password: true, + ignoreFocusOut: true, + placeHolder: 'Password', + prompt: data.substr(7), + }); + if (!password) return cleanup(), reject(new Error('No password given')); + return shell.write(`${password}\n`); } - }); + return cleanup(), reject(new Error(`Sudo error: ${data}`)); + } + function cleanup() { + shell.stdout.removeListener('data', stdout); + shell.stderr.removeListener('data', stderr); + } + shell.stdout.on('data', stdout); + shell.stderr.on('data', stderr); + const uFlag = typeof user === 'string' ? `-u ${user} ` : ''; + shell.write(`sudo -S ${uFlag}bash -c "echo SUDO OK; cat | bash"\n`); + }); +} + +function stripSudo(cmd: string) { + cmd = cmd.replace(/^sudo\s+/, ''); + let res = cmd; + while (true) { + cmd = res.trim(); + res = cmd.replace(/^\-\-\s+/, ''); + if (res !== cmd) break; + res = cmd.replace(/^\-[AbEeKklnPSsVv]/, ''); + if (res !== cmd) continue; + res = cmd.replace(/^\-[CHhprtUu]\s+\S+/, ''); + if (res !== cmd) continue; + res = cmd.replace(/^\-\-(close\-from|group|host|role|type|other\-user|user)=\S+/, ''); + if (res !== cmd) continue; + break; + } + return cmd; +} + +export async function getSFTP(client: Client, config: FileSystemConfig): Promise { + config = (await calculateActualConfig(config))!; + if (!config) throw new Error('Couldn\'t calculate the config'); + if (config.sftpSudo && !config.sftpCommand) { + Logging.warning('sftpSudo is set without sftpCommand. Assuming /usr/lib/openssh/sftp-server'); + config.sftpCommand = '/usr/lib/openssh/sftp-server'; + } + if (!config.sftpCommand) { + Logging.info(`Creating SFTP session using standard sftp subsystem`); + return toPromise(cb => client.sftp(cb)); + } + let cmd = config.sftpCommand; + Logging.info(`Creating SFTP session for ${config.name} using specified command: ${cmd}`); + const shell = await toPromise(cb => client.shell(false, cb)); + // shell.stdout.on('data', (d: string | Buffer) => Logging.debug(`[${config.name}][SFTP-STDOUT] ${d}`)); + // shell.stderr.on('data', (d: string | Buffer) => Logging.debug(`[${config.name}][SFTP-STDERR] ${d}`)); + // Maybe the user hasn't specified `sftpSudo`, but did put `sudo` in `sftpCommand` + // I can't find a good way of differentiating welcome messages, SFTP traffic, sudo password prompts, ... + // so convert the `sftpCommand` to make use of `sftpSudo`, since that seems to work + if (cmd.match(/^sudo/)) { + // If the -u flag is given, use that too + const mat = cmd.match(/\-u\s+(\S+)/) || cmd.match(/\-\-user=(\S+)/); + config.sftpSudo = mat ? mat[1] : true; + // Now the tricky part of splitting the sudo and sftp command + config.sftpCommand = cmd = stripSudo(cmd); + Logging.warning(`Reformed sftpCommand due to sudo to: ${cmd}`); + } + // If the user wants sudo, we'll first convert this shell into a sudo shell + if (config.sftpSudo) await startSudo(shell, config, config.sftpSudo); + shell.write(`echo SFTP READY\n`); + // Wait until we see "SFTP READY" (skipping welcome messages etc) + await new Promise((ready, nvm) => { + const handler = (data: string | Buffer) => { + if (data.toString().trim() !== 'SFTP READY') return; + shell.stdout.removeListener('data', handler); + ready(); + }; + shell.stdout.on('data', handler); + shell.on('close', nvm); }); + // Start sftpCommand (e.g. /usr/lib/openssh/sftp-server) and wrap everything nicely + const sftps = new SFTPStream({ debug: config.debug }); + shell.pipe(sftps).pipe(shell); + const sftp = new SFTPWrapper(sftps); + await toPromise(cb => shell.write(`${cmd}\n`, cb)); + return sftp; } diff --git a/src/manager.ts b/src/manager.ts index 533b10c..0921dab 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -32,6 +32,7 @@ export interface FileSystemConfig extends ConnectConfig { privateKeyPath?: string; hop?: string; sftpCommand?: string; + sftpSudo?: string | boolean; newFileMode?: number | string; } @@ -316,6 +317,7 @@ export class Manager implements vscode.FileSystemProvider, vscode.TreeDataProvid fs.disconnect(); this.fileSystems.splice(this.fileSystems.indexOf(fs), 1); } + delete this.creatingFileSystems[name]; const folders = vscode.workspace.workspaceFolders!; const index = folders.findIndex(f => f.uri.scheme === 'ssh' && f.uri.authority === name); if (index !== -1) vscode.workspace.updateWorkspaceFolders(index, 1); @@ -328,6 +330,7 @@ export class Manager implements vscode.FileSystemProvider, vscode.TreeDataProvid fs.disconnect(); this.fileSystems.splice(this.fileSystems.indexOf(fs), 1); } + delete this.creatingFileSystems[name]; this.commandConnect(name); } public commandConnect(name: string) {