Allow instant connections

feature/ssh-config
Kelvin Schoofs 4 years ago
parent a192d5d935
commit 844b0e1b43

@ -2,7 +2,7 @@
import { readFile, writeFile } from 'fs';
import { parse as parseJsonc, ParseError } from 'jsonc-parser';
import * as vscode from 'vscode';
import { ConfigLocation, FileSystemConfig, invalidConfigName } from './fileSystemConfig';
import { ConfigLocation, FileSystemConfig, invalidConfigName, parseConnectionString } from './fileSystemConfig';
import { Logging } from './logging';
import { toPromise } from './toPromise';
@ -283,9 +283,17 @@ export async function deleteConfig(config: FileSystemConfig) {
});
}
export function getConfig(name: string) {
if (name === '<config>') return null;
return getConfigs().find(c => c.name === name);
/** If a loaded config with the given name exists (case insensitive), it is returned.
* Otherwise, if it contains a `@`, we parse it as a connection string.
* If this results in no (valid) configuration, `undefined` is returned.
*/
export function getConfig(input: string): FileSystemConfig | undefined {
const lower = input.toLowerCase();
const loaded = getConfigs().find(c => c.name.toLowerCase() === lower);
if (loaded) return loaded;
if (!input.includes('@')) return undefined;
const parsed = parseConnectionString(input);
return typeof parsed === 'string' ? undefined : parsed[0];
}
function valueMatches(a: any, b: any): boolean {

@ -139,6 +139,11 @@ export async function calculateActualConfig(config: FileSystemConfig): Promise<F
// Issue with the ssh2 dependency apparently not liking false
delete config.passphrase;
}
if (!config.privateKey && !config.agent && !config.password) {
logging.debug(`\tNo privateKey, agent or password. Gonna prompt for password`);
config.password = true as any;
await promptFields(config, 'password');
}
logging.debug(`\tFinal configuration:\n${JSON.stringify(censorConfig(config), null, 4)}`);
return config;
}

@ -74,7 +74,7 @@ export function activate(context: vscode.ExtensionContext) {
// sshfs.add(target?: string | FileSystemConfig)
registerCommandHandler('sshfs.add', {
promptOptions: { promptConfigs: true },
promptOptions: { promptConfigs: true, promptConnections: true, promptInstantConnection: true },
handleConfig: config => manager.commandConnect(config),
});
@ -95,7 +95,7 @@ export function activate(context: vscode.ExtensionContext) {
// sshfs.termninal(target?: string | FileSystemConfig | Connection | vscode.Uri)
registerCommandHandler('sshfs.terminal', {
promptOptions: { promptConfigs: true, promptConnections: true },
promptOptions: { promptConfigs: true, promptConnections: true, promptInstantConnection: true },
handleConfig: config => manager.commandTerminal(config),
handleConnection: con => manager.commandTerminal(con),
handleUri: async uri => {

@ -113,3 +113,35 @@ export function invalidConfigName(name: string) {
if (name.match(/^[\w_\\/.@\-+]+$/)) return null;
return `A SSH FS name can only exists of lowercase alphanumeric characters, slashes and any of these: _.+-@`;
}
/**
* https://regexr.com/5m3gl (mostly based on https://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04)
* Supports several formats, the first one being the "full" format, with others being partial:
* - `user;abc=def,a-b=1-5@server.example.com:22/some/file.ext`
* - `user@server.example.com/directory`
* - `server:22/directory`
* - `user@server`
* - `server`
* - `@server/path` - Unlike OpenSSH, we allow a @ (and connection parameters) without a username
*
* The resulting FileSystemConfig will have as name basically the input, but without the path. If there is no
* username given, the name will start with `@`, as to differentiate between connection strings and config names.
*/
const CONNECTION_REGEX = /^((?<user>\w+)?(;[\w-]+=[\w\d-]+(,[\w\d-]+=[\w\d-]+)*)?@)?(?<host>[^@\\/:,=]+)(:(?<port>\d+))?(?<path>\/.*)?$/;
export function parseConnectionString(input: string): [config: FileSystemConfig, path?: string] | string {
input = input.trim();
const match = input.match(CONNECTION_REGEX);
if (!match) return 'Invalid format, expected something like "user@example.com:22/some/path"';
const { user, host, path } = match.groups!;
const portStr = match.groups!.port;
const port = portStr ? Number.parseInt(portStr) : undefined;
if (portStr && (!port || port < 1 || port > 65535)) return `The string '${port}' is not a valid port number`;
const name = `${user || ''}@${host}${port ? `:${port}` : ''}${path || ''}`;
return [{
name, host, port,
username: user || '$USERNAME',
_locations: [],
//debug: true as any,
}, path];
}

@ -2,7 +2,7 @@
import * as vscode from 'vscode';
import { getConfigs } from './config';
import type { Connection } from './connection';
import type { FileSystemConfig } from './fileSystemConfig';
import { parseConnectionString, FileSystemConfig } from './fileSystemConfig';
import type { Manager } from './manager';
import type { SSHPseudoTerminal } from './pseudoTerminal';
import type { SSHFileSystem } from './sshFileSystem';
@ -70,6 +70,8 @@ export interface PickComplexOptions {
immediateReturn?: boolean;
/** If true, add all connections. If this is a string, filter by config name first */
promptConnections?: boolean | string;
/** If true, add an option to enter a connection string */
promptInstantConnection?: boolean;
/** If true, add all configurations. If this is a string, filter by config name first */
promptConfigs?: boolean | string;
/** If true, add all terminals. If this is a string, filter by config name first */
@ -78,10 +80,25 @@ export interface PickComplexOptions {
nameFilter?: string;
}
async function inputInstantConnection(): Promise<FileSystemConfig | undefined> {
const name = await vscode.window.showInputBox({
placeHolder: 'user@host:/home/user',
prompt: 'SSH connection string',
validateInput(value: string) {
const result = parseConnectionString(value);
return typeof result === 'string' ? result : undefined;
}
});
if (!name) return;
const result = parseConnectionString(name);
if (typeof result === 'string') return;
return result[0];
}
export async function pickComplex(manager: Manager, options: PickComplexOptions):
Promise<FileSystemConfig | Connection | SSHPseudoTerminal | undefined> {
return new Promise((resolve) => {
const { promptConnections, promptConfigs, promptTerminals, immediateReturn, nameFilter } = options;
return new Promise<any>((resolve) => {
const { promptConnections, promptConfigs, nameFilter } = options;
const items: QuickPickItemWithItem[] = [];
const toSelect: string[] = [];
if (promptConnections) {
@ -98,7 +115,7 @@ export async function pickComplex(manager: Manager, options: PickComplexOptions)
items.push(...configs.map(config => formatItem(config, true)));
toSelect.push('configuration');
}
if (promptTerminals) {
if (options.promptTerminals) {
let cons = manager.connectionManager.getActiveConnections();
if (typeof promptConnections === 'string') cons = cons.filter(con => con.actualConfig.name === promptConnections);
if (nameFilter) cons = cons.filter(con => con.actualConfig.name === nameFilter);
@ -106,7 +123,15 @@ export async function pickComplex(manager: Manager, options: PickComplexOptions)
items.push(...terminals.map(config => formatItem(config, true)));
toSelect.push('terminal');
}
if (immediateReturn && items.length <= 1) return resolve(items[0]?.item);
if (options.promptInstantConnection) {
items.unshift({
label: '$(terminal) Create instant connection',
detail: 'Opens an input box where you can type e.g. `user@host:/home/user`',
picked: true, alwaysShow: true,
item: inputInstantConnection,
});
}
if (options.immediateReturn && items.length <= 1) return resolve(items[0]?.item);
const quickPick = vscode.window.createQuickPick<QuickPickItemWithItem>();
quickPick.items = items;
quickPick.title = 'Select ' + toSelect.join(' / ');
@ -117,7 +142,7 @@ export async function pickComplex(manager: Manager, options: PickComplexOptions)
});
quickPick.onDidHide(() => resolve());
quickPick.show();
});
}).then(result => typeof result === 'function' ? result() : result);
}
export const pickConfig = (manager: Manager) => pickComplex(manager, { promptConfigs: true }) as Promise<FileSystemConfig | undefined>;

Loading…
Cancel
Save