|
|
|
@ -6,7 +6,7 @@ import { SFTPStream } from 'ssh2-streams';
|
|
|
|
|
import * as vscode from 'vscode';
|
|
|
|
|
import { getConfig, getFlagBoolean } from './config';
|
|
|
|
|
import type { FileSystemConfig } from './fileSystemConfig';
|
|
|
|
|
import { censorConfig, Logging } from './logging';
|
|
|
|
|
import { Logging } from './logging';
|
|
|
|
|
import type { PuttySession } from './putty';
|
|
|
|
|
import { toPromise, validatePort } from './utils';
|
|
|
|
|
|
|
|
|
@ -38,7 +38,7 @@ async function promptFields(config: FileSystemConfig, ...fields: (keyof FileSyst
|
|
|
|
|
for (const field of fields) {
|
|
|
|
|
const prompt = PROMPT_FIELDS[field];
|
|
|
|
|
if (!prompt) {
|
|
|
|
|
Logging.error(`Prompting unexpected field '${field}'`);
|
|
|
|
|
Logging.error`Prompting unexpected field '${field}'`;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const value = config[field];
|
|
|
|
@ -69,7 +69,7 @@ export async function calculateActualConfig(config: FileSystemConfig): Promise<F
|
|
|
|
|
config.port = port ? validatePort(port) : 22;
|
|
|
|
|
config.agent = replaceVariables(config.agent);
|
|
|
|
|
config.privateKeyPath = replaceVariables(config.privateKeyPath);
|
|
|
|
|
logging.info(`Calculating actual config`);
|
|
|
|
|
logging.info`Calculating actual config`;
|
|
|
|
|
if (config.instantConnection) {
|
|
|
|
|
// Created from an instant connection string, so enable PuTTY (in try mode)
|
|
|
|
|
config.putty = '<TRY>'; // Could just set it to `true` but... consistency?
|
|
|
|
@ -86,7 +86,7 @@ export async function calculateActualConfig(config: FileSystemConfig): Promise<F
|
|
|
|
|
if (tryPutty) {
|
|
|
|
|
// If we're trying to find one, we also check whether `config.host` represents the name of a PuTTY session
|
|
|
|
|
session = await getSession(config.host);
|
|
|
|
|
logging.info(`\ttryPutty is true, tried finding a config named '${config.host}' and found ${session ? `'${session.name}'` : 'no match'}`);
|
|
|
|
|
logging.info`\ttryPutty is true, tried finding a config named '${config.host}' and found ${session ? `'${session.name}'` : 'no match'}`;
|
|
|
|
|
}
|
|
|
|
|
if (!session) {
|
|
|
|
|
let nameOnly = true;
|
|
|
|
@ -129,11 +129,11 @@ export async function calculateActualConfig(config: FileSystemConfig): Promise<F
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`The requested PuTTY session uses an unsupported proxy method`);
|
|
|
|
|
}
|
|
|
|
|
logging.debug(`\tReading PuTTY configuration lead to the following configuration:\n${JSON.stringify(config, null, 4)}`);
|
|
|
|
|
logging.debug`\tReading PuTTY configuration lead to the following configuration:\n${JSON.stringify(config, null, 4)}`;
|
|
|
|
|
} else if (!tryPutty) {
|
|
|
|
|
throw new Error(`Couldn't find the requested PuTTY session`);
|
|
|
|
|
} else {
|
|
|
|
|
logging.debug(`\tConfig suggested finding a PuTTY configuration, did not find one`);
|
|
|
|
|
logging.debug`\tConfig suggested finding a PuTTY configuration, did not find one`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If the username is (still) `$USER` at this point, use the local user's username
|
|
|
|
@ -142,7 +142,7 @@ export async function calculateActualConfig(config: FileSystemConfig): Promise<F
|
|
|
|
|
try {
|
|
|
|
|
const key = await toPromise<Buffer>(cb => readFile(config.privateKeyPath!, cb));
|
|
|
|
|
config.privateKey = key;
|
|
|
|
|
logging.debug(`\tRead private key from ${config.privateKeyPath}`);
|
|
|
|
|
logging.debug`\tRead private key from ${config.privateKeyPath}`;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new Error(`Error while reading the keyfile at:\n${config.privateKeyPath}`);
|
|
|
|
|
}
|
|
|
|
@ -166,11 +166,11 @@ export async function calculateActualConfig(config: FileSystemConfig): Promise<F
|
|
|
|
|
delete config.passphrase;
|
|
|
|
|
}
|
|
|
|
|
if (config.agentForward && !config.agent) {
|
|
|
|
|
logging.debug(`\tNo agent while having agentForward, disabling agent forwarding`);
|
|
|
|
|
logging.debug`\tNo agent while having agentForward, disabling agent forwarding`;
|
|
|
|
|
config.agentForward = false;
|
|
|
|
|
}
|
|
|
|
|
if (!config.privateKey && !config.agent && !config.password) {
|
|
|
|
|
logging.debug(`\tNo privateKey, agent or password. Gonna prompt for password`);
|
|
|
|
|
logging.debug`\tNo privateKey, agent or password. Gonna prompt for password`;
|
|
|
|
|
config.password = true as any;
|
|
|
|
|
await promptFields(config, 'password');
|
|
|
|
|
}
|
|
|
|
@ -182,25 +182,25 @@ export async function createSocket(config: FileSystemConfig): Promise<NodeJS.Rea
|
|
|
|
|
config = (await calculateActualConfig(config))!;
|
|
|
|
|
if (!config) return null;
|
|
|
|
|
const logging = Logging.scope(`createSocket(${config.name})`);
|
|
|
|
|
logging.info(`Creating socket`);
|
|
|
|
|
logging.info`Creating socket`;
|
|
|
|
|
if (config.hop) {
|
|
|
|
|
logging.debug(`\tHopping through ${config.hop}`);
|
|
|
|
|
logging.debug`\tHopping through ${config.hop}`;
|
|
|
|
|
const hop = getConfig(config.hop);
|
|
|
|
|
if (!hop) throw new Error(`A SSH FS configuration with the name '${config.hop}' doesn't exist`);
|
|
|
|
|
const ssh = await createSSH(hop);
|
|
|
|
|
if (!ssh) {
|
|
|
|
|
logging.debug(`\tFailed in connecting to hop ${config.hop}`);
|
|
|
|
|
logging.debug`\tFailed in connecting to hop ${config.hop}`;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return new Promise<NodeJS.ReadableStream>((resolve, reject) => {
|
|
|
|
|
ssh.forwardOut('localhost', 0, config.host!, config.port || 22, (err, channel) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
logging.debug(`\tError connecting to hop ${config.hop} for ${config.name}: ${err}`);
|
|
|
|
|
err.message = `Couldn't connect through the hop:\n${err.message}`;
|
|
|
|
|
logging.debug`\tError connecting to hop ${config.hop} for ${config.name}: ${err}`;
|
|
|
|
|
err.message = `Couldn't connect through the hop:\n${err}`;
|
|
|
|
|
return reject(err);
|
|
|
|
|
} else if (!channel) {
|
|
|
|
|
err = new Error('Did not receive a channel');
|
|
|
|
|
logging.debug(`\tGot no channel when connecting to hop ${config.hop} for ${config.name}`);
|
|
|
|
|
logging.debug`\tGot no channel when connecting to hop ${config.hop} for ${config.name}`;
|
|
|
|
|
return reject(err);
|
|
|
|
|
}
|
|
|
|
|
channel.once('close', () => ssh.destroy());
|
|
|
|
@ -221,7 +221,7 @@ export async function createSocket(config: FileSystemConfig): Promise<NodeJS.Rea
|
|
|
|
|
throw new Error(`Unknown proxy method`);
|
|
|
|
|
}
|
|
|
|
|
return new Promise<NodeJS.ReadableStream>((resolve, reject) => {
|
|
|
|
|
logging.debug(`Connecting to ${config.host}:${config.port || 22}`);
|
|
|
|
|
logging.debug`Connecting to ${config.host}:${config.port || 22}`;
|
|
|
|
|
const socket = new Socket();
|
|
|
|
|
socket.connect(config.port || 22, config.host!, () => resolve(socket as NodeJS.ReadableStream));
|
|
|
|
|
socket.once('error', reject);
|
|
|
|
@ -239,7 +239,7 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable
|
|
|
|
|
client.once('ready', () => resolve(client));
|
|
|
|
|
client.once('timeout', () => reject(new Error(`Socket timed out while connecting SSH FS '${config.name}'`)));
|
|
|
|
|
client.on('keyboard-interactive', (name, instructions, lang, prompts, finish) => {
|
|
|
|
|
logging.debug(`Received keyboard-interactive request with prompts "${JSON.stringify(prompts)}"`);
|
|
|
|
|
logging.debug`Received keyboard-interactive request with prompts ${prompts}`;
|
|
|
|
|
Promise.all<string>(prompts.map(prompt =>
|
|
|
|
|
vscode.window.showInputBox({
|
|
|
|
|
password: true, // prompt.echo was false for me while testing password prompting
|
|
|
|
@ -250,9 +250,9 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable
|
|
|
|
|
});
|
|
|
|
|
client.on('error', (error: Error & { description?: string }) => {
|
|
|
|
|
if (error.description) {
|
|
|
|
|
error.message = `${error.description}\n${error.message}`;
|
|
|
|
|
error.message = `${error.description}\n${error}`;
|
|
|
|
|
}
|
|
|
|
|
logging.error(`${error.message || error}`);
|
|
|
|
|
logging.error(error);
|
|
|
|
|
reject(error);
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
@ -265,10 +265,10 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable
|
|
|
|
|
// Note: If the config already specifies a custom `algorithms.key`, ignore it (trust the user?)
|
|
|
|
|
const [flagV, flagR] = getFlagBoolean('DF-GE', false, config.flags);
|
|
|
|
|
if (flagV) {
|
|
|
|
|
logging.info(`Flag "DF-GE" enabled due to '${flagR}', disabling DiffieHellman kex groupex algorithms`);
|
|
|
|
|
logging.info`Flag "DF-GE" enabled due to '${flagR}', disabling DiffieHellman kex groupex algorithms`;
|
|
|
|
|
let kex: string[] = require('ssh2-streams/lib/constants').ALGORITHMS.KEX;
|
|
|
|
|
kex = kex.filter(algo => !algo.includes('diffie-hellman-group-exchange'));
|
|
|
|
|
logging.debug(`\tResulting algorithms.kex: ${kex}`);
|
|
|
|
|
logging.debug`\tResulting algorithms.kex: ${kex}`;
|
|
|
|
|
finalConfig.algorithms = { ...finalConfig.algorithms, kex };
|
|
|
|
|
}
|
|
|
|
|
client.connect(finalConfig);
|
|
|
|
@ -279,14 +279,14 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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'}`);
|
|
|
|
|
Logging.debug`Turning shell into a sudo shell for ${typeof user === 'string' ? `'${user}'` : 'default sudo user'}`;
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
function stdout(data: Buffer | string) {
|
|
|
|
|
data = data.toString();
|
|
|
|
|
if (data.trim() === 'SUDO OK') {
|
|
|
|
|
return cleanup(), resolve();
|
|
|
|
|
} else {
|
|
|
|
|
Logging.debug(`Unexpected STDOUT: ${data}`);
|
|
|
|
|
Logging.debug`Unexpected STDOUT: ${data}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async function stderr(data: Buffer | string) {
|
|
|
|
@ -342,14 +342,14 @@ export async function getSFTP(client: Client, config: FileSystemConfig): Promise
|
|
|
|
|
config.sftpCommand = '/usr/lib/openssh/sftp-server';
|
|
|
|
|
}
|
|
|
|
|
if (!config.sftpCommand) {
|
|
|
|
|
logging.info(`Creating SFTP session using standard sftp subsystem`);
|
|
|
|
|
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 using specified command: ${cmd}`);
|
|
|
|
|
logging.info`Creating SFTP session using specified command: ${cmd}`;
|
|
|
|
|
const shell = await toPromise<ClientChannel>(cb => client.shell(false, cb));
|
|
|
|
|
// shell.stdout.on('data', (d: string | Buffer) => logging.debug(`[SFTP-STDOUT] ${d}`));
|
|
|
|
|
// shell.stderr.on('data', (d: string | Buffer) => logging.debug(`[SFTP-STDERR] ${d}`));
|
|
|
|
|
// shell.stdout.on('data', (d: string | Buffer) => logging.debug`[SFTP-STDOUT] ${d}`);
|
|
|
|
|
// shell.stderr.on('data', (d: string | Buffer) => logging.debug`[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
|
|
|
|
|