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 { readFile } from 'fs';
import { Socket } from 'net'; 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 { SFTPStream } from 'ssh2-streams';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { loadConfigs, openConfigurationEditor } from './config'; 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)}`); Logging.debug(`\tFinal configuration:\n${JSON.stringify(Logging.censorConfig(config), null, 4)}`);
return config; return config;
} }
@ -191,7 +190,7 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable
}), }),
)).then(finish); )).then(finish);
}); });
client.once('error', (error) => { client.once('error', (error: Error & { description?: string }) => {
if (error.description) { if (error.description) {
error.message = `${error.description}\n${error.message}`; 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) => { return new Promise((resolve, reject) => {
if (!config.sftpCommand) { function stdout(data: Buffer | string) {
Logging.info(`Creating SFTP session using standard sftp subsystem`); data = data.toString();
return client.sftp((err, sftp) => { if (data.trim() === 'SUDO OK') {
if (err) { return cleanup(), resolve();
client.end(); } else {
reject(err); Logging.debug(`Unexpected STDOUT: ${data}`);
}
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);
} }
channel.once('close', () => (client.end(), reject())); }
channel.once('error', () => (client.end(), reject())); async function stderr(data: Buffer | string) {
try { data = data.toString();
Logging.debug(`\tSFTP session created, wrapping resulting channel in SFTPWrapper`); if (data.match(/^\[sudo\]/)) {
const sftps = new SFTPStream(); const password = typeof config.password === 'string' ? config.password :
channel.pipe(sftps).pipe(channel); await vscode.window.showInputBox({
const sftp = new SFTPWrapper(sftps); password: true,
resolve(sftp); ignoreFocusOut: true,
} catch (e) { placeHolder: 'Password',
Logging.error(`Couldn't wrap SFTP session for ${config.name} using specified command: ${config.sftpCommand}\n${err}`); prompt: data.substr(7),
reject(e); });
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; privateKeyPath?: string;
hop?: string; hop?: string;
sftpCommand?: string; sftpCommand?: string;
sftpSudo?: string | boolean;
newFileMode?: number | string; newFileMode?: number | string;
} }
@ -316,6 +317,7 @@ export class Manager implements vscode.FileSystemProvider, vscode.TreeDataProvid
fs.disconnect(); fs.disconnect();
this.fileSystems.splice(this.fileSystems.indexOf(fs), 1); this.fileSystems.splice(this.fileSystems.indexOf(fs), 1);
} }
delete this.creatingFileSystems[name];
const folders = vscode.workspace.workspaceFolders!; const folders = vscode.workspace.workspaceFolders!;
const index = folders.findIndex(f => f.uri.scheme === 'ssh' && f.uri.authority === name); const index = folders.findIndex(f => f.uri.scheme === 'ssh' && f.uri.authority === name);
if (index !== -1) vscode.workspace.updateWorkspaceFolders(index, 1); if (index !== -1) vscode.workspace.updateWorkspaceFolders(index, 1);
@ -328,6 +330,7 @@ export class Manager implements vscode.FileSystemProvider, vscode.TreeDataProvid
fs.disconnect(); fs.disconnect();
this.fileSystems.splice(this.fileSystems.indexOf(fs), 1); this.fileSystems.splice(this.fileSystems.indexOf(fs), 1);
} }
delete this.creatingFileSystems[name];
this.commandConnect(name); this.commandConnect(name);
} }
public commandConnect(name: string) { public commandConnect(name: string) {

Loading…
Cancel
Save