Add initial support for sudo sftp

feature/search
Kelvin Schoofs 6 years ago
parent 28473e8099
commit 3e7089815f

@ -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<F
}
}
}
if (config.password) config.agent = undefined;
Logging.debug(`\tFinal configuration:\n${JSON.stringify(Logging.censorConfig(config), null, 4)}`);
return config;
}
@ -191,7 +190,7 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable
}),
)).then(finish);
});
client.once('error', (error) => {
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<SFTPWrapper> {
function startSudo(shell: ClientChannel, config: FileSystemConfig, user: string | boolean = true): Promise<void> {
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<SFTPWrapper> {
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<SFTPWrapper>(cb => client.sftp(cb));
}
let cmd = config.sftpCommand;
Logging.info(`Creating SFTP session for ${config.name} using specified command: ${cmd}`);
const shell = await toPromise<ClientChannel>(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;
}

@ -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) {

Loading…
Cancel
Save