From 9e9066d3cd0307ccd05fa0fc3e9424924c1d02f3 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Fri, 28 Jan 2022 22:49:52 +0100 Subject: [PATCH] split openssh-importer into tabby-electron, support tilde in private key paths - fixes #5627 --- tabby-electron/src/index.ts | 4 +- tabby-electron/src/openSSHImport.ts | 128 ++++++++++++++++++++++++++++ tabby-ssh/src/api/importer.ts | 6 ++ tabby-ssh/src/api/index.ts | 1 + tabby-ssh/src/openSSHImport.ts | 121 -------------------------- tabby-ssh/src/profiles.ts | 16 ++-- 6 files changed, 147 insertions(+), 129 deletions(-) create mode 100644 tabby-electron/src/openSSHImport.ts create mode 100644 tabby-ssh/src/api/importer.ts delete mode 100644 tabby-ssh/src/openSSHImport.ts diff --git a/tabby-electron/src/index.ts b/tabby-electron/src/index.ts index 3d0b6b81..13874c8b 100644 --- a/tabby-electron/src/index.ts +++ b/tabby-electron/src/index.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core' import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider, FileProvider } from 'tabby-core' import { TerminalColorSchemeProvider } from 'tabby-terminal' -import { SFTPContextMenuItemProvider } from 'tabby-ssh' +import { SFTPContextMenuItemProvider, SSHProfileImporter } from 'tabby-ssh' import { auditTime } from 'rxjs' import { HyperColorSchemes } from './colorSchemes' @@ -17,6 +17,7 @@ import { ElectronService } from './services/electron.service' import { ElectronHotkeyProvider } from './hotkeys' import { ElectronConfigProvider } from './config' import { EditSFTPContextMenu } from './sftpContextMenu' +import { OpenSSHImporter } from './openSSHImport' @NgModule({ providers: [ @@ -31,6 +32,7 @@ import { EditSFTPContextMenu } from './sftpContextMenu' { provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true }, { provide: FileProvider, useClass: ElectronFileProvider, multi: true }, { provide: SFTPContextMenuItemProvider, useClass: EditSFTPContextMenu, multi: true }, + { provide: SSHProfileImporter, useClass: OpenSSHImporter, multi: true }, ], }) export default class ElectronModule { diff --git a/tabby-electron/src/openSSHImport.ts b/tabby-electron/src/openSSHImport.ts new file mode 100644 index 00000000..6ef02b0c --- /dev/null +++ b/tabby-electron/src/openSSHImport.ts @@ -0,0 +1,128 @@ +import * as fs from 'fs/promises' +import * as path from 'path' +import slugify from 'slugify' +import { PartialProfile } from 'tabby-core' +import { SSHProfileImporter, PortForwardType, SSHProfile, SSHProfileOptions } from 'tabby-ssh' + +function deriveID (name: string): string { + return 'openssh-config:' + slugify(name) +} + +export class OpenSSHImporter extends SSHProfileImporter { + async getProfiles (): Promise[]> { + const results: PartialProfile[] = [] + const configPath = path.join(process.env.HOME ?? '~', '.ssh', 'config') + try { + const lines = (await fs.readFile(configPath, 'utf8')).split('\n') + const globalOptions: Partial = {} + let currentProfile: PartialProfile|null = null + for (let line of lines) { + if (line.trim().startsWith('#') || !line.trim()) { + continue + } + if (line.startsWith('Host ')) { + if (currentProfile) { + results.push(currentProfile) + } + const name = line.substr(5).trim() + currentProfile = { + id: deriveID(name), + name: `${name} (.ssh/config)`, + type: 'ssh', + group: 'Imported from .ssh/config', + options: { + ...globalOptions, + host: name, + }, + } + } else { + const target: Partial = currentProfile?.options ?? globalOptions + line = line.trim() + const idx = /\s/.exec(line)?.index ?? -1 + if (idx === -1) { + continue + } + const key = line.substr(0, idx).trim() + const value = line.substr(idx + 1).trim() + + if (key === 'IdentityFile') { + target.privateKeys = value.split(',').map(s => s.trim()).map(s => { + if (s.startsWith('~')) { + s = path.join(process.env.HOME ?? '~', s.slice(2)) + } + return s + }) + } else if (key === 'RemoteForward') { + const bind = value.split(/\s/)[0].trim() + const tgt = value.split(/\s/)[1].trim() + target.forwardedPorts ??= [] + target.forwardedPorts.push({ + type: PortForwardType.Remote, + description: value, + host: bind.split(':')[0] ?? '127.0.0.1', + port: parseInt(bind.split(':')[1] ?? bind), + targetAddress: tgt.split(':')[0], + targetPort: parseInt(tgt.split(':')[1]), + }) + } else if (key === 'LocalForward') { + const bind = value.split(/\s/)[0].trim() + const tgt = value.split(/\s/)[1].trim() + target.forwardedPorts ??= [] + target.forwardedPorts.push({ + type: PortForwardType.Local, + description: value, + host: bind.split(':')[0] ?? '127.0.0.1', + port: parseInt(bind.split(':')[1] ?? bind), + targetAddress: tgt.split(':')[0], + targetPort: parseInt(tgt.split(':')[1]), + }) + } else if (key === 'DynamicForward') { + const bind = value.trim() + target.forwardedPorts ??= [] + target.forwardedPorts.push({ + type: PortForwardType.Dynamic, + description: value, + host: bind.split(':')[0] ?? '127.0.0.1', + port: parseInt(bind.split(':')[1] ?? bind), + targetAddress: '', + targetPort: 22, + }) + } else { + const mappedKey = { + Hostname: 'host', + Port: 'port', + User: 'user', + ForwardX11: 'x11', + ServerAliveInterval: 'keepaliveInterval', + ServerAliveCountMax: 'keepaliveCountMax', + ProxyCommand: 'proxyCommand', + ProxyJump: 'jumpHost', + }[key] + if (mappedKey) { + target[mappedKey] = value + } + } + } + } + if (currentProfile) { + results.push(currentProfile) + } + for (const p of results) { + if (p.options?.proxyCommand) { + p.options.proxyCommand = p.options.proxyCommand + .replace('%h', p.options.host ?? '') + .replace('%p', (p.options.port ?? 22).toString()) + } + if (p.options?.jumpHost) { + p.options.jumpHost = deriveID(p.options.jumpHost) + } + } + return results + } catch (e) { + if (e.code === 'ENOENT') { + return [] + } + throw e + } + } +} diff --git a/tabby-ssh/src/api/importer.ts b/tabby-ssh/src/api/importer.ts new file mode 100644 index 00000000..4b7c6d36 --- /dev/null +++ b/tabby-ssh/src/api/importer.ts @@ -0,0 +1,6 @@ +import { PartialProfile } from 'tabby-core' +import { SSHProfile } from './interfaces' + +export abstract class SSHProfileImporter { + abstract getProfiles (): Promise[]> +} diff --git a/tabby-ssh/src/api/index.ts b/tabby-ssh/src/api/index.ts index d71080a4..f7d1429e 100644 --- a/tabby-ssh/src/api/index.ts +++ b/tabby-ssh/src/api/index.ts @@ -1,2 +1,3 @@ export * from './contextMenu' export * from './interfaces' +export * from './importer' diff --git a/tabby-ssh/src/openSSHImport.ts b/tabby-ssh/src/openSSHImport.ts deleted file mode 100644 index 072c25fa..00000000 --- a/tabby-ssh/src/openSSHImport.ts +++ /dev/null @@ -1,121 +0,0 @@ -import * as fs from 'fs/promises' -import * as path from 'path' -import slugify from 'slugify' -import { PortForwardType, SSHProfile, SSHProfileOptions } from './api/interfaces' -import { PartialProfile } from 'tabby-core' - -function deriveID (name: string): string { - return 'openssh-config:' + slugify(name) -} - -export async function parseOpenSSHProfiles (): Promise[]> { - const results: PartialProfile[] = [] - const configPath = path.join(process.env.HOME ?? '~', '.ssh', 'config') - try { - const lines = (await fs.readFile(configPath, 'utf8')).split('\n') - const globalOptions: Partial = {} - let currentProfile: PartialProfile|null = null - for (let line of lines) { - if (line.trim().startsWith('#') || !line.trim()) { - continue - } - if (line.startsWith('Host ')) { - if (currentProfile) { - results.push(currentProfile) - } - const name = line.substr(5).trim() - currentProfile = { - id: deriveID(name), - name, - type: 'ssh', - group: 'Imported from .ssh/config', - options: { - ...globalOptions, - host: name, - }, - } - } else { - const target: Partial = currentProfile?.options ?? globalOptions - line = line.trim() - const idx = /\s/.exec(line)?.index ?? -1 - if (idx === -1) { - continue - } - const key = line.substr(0, idx).trim() - const value = line.substr(idx + 1).trim() - - if (key === 'IdentityFile') { - target.privateKeys = value.split(',').map(s => s.trim()) - } else if (key === 'RemoteForward') { - const bind = value.split(/\s/)[0].trim() - const tgt = value.split(/\s/)[1].trim() - target.forwardedPorts ??= [] - target.forwardedPorts.push({ - type: PortForwardType.Remote, - description: value, - host: bind.split(':')[0] ?? '127.0.0.1', - port: parseInt(bind.split(':')[1] ?? bind), - targetAddress: tgt.split(':')[0], - targetPort: parseInt(tgt.split(':')[1]), - }) - } else if (key === 'LocalForward') { - const bind = value.split(/\s/)[0].trim() - const tgt = value.split(/\s/)[1].trim() - target.forwardedPorts ??= [] - target.forwardedPorts.push({ - type: PortForwardType.Local, - description: value, - host: bind.split(':')[0] ?? '127.0.0.1', - port: parseInt(bind.split(':')[1] ?? bind), - targetAddress: tgt.split(':')[0], - targetPort: parseInt(tgt.split(':')[1]), - }) - } else if (key === 'DynamicForward') { - const bind = value.trim() - target.forwardedPorts ??= [] - target.forwardedPorts.push({ - type: PortForwardType.Dynamic, - description: value, - host: bind.split(':')[0] ?? '127.0.0.1', - port: parseInt(bind.split(':')[1] ?? bind), - targetAddress: '', - targetPort: 22, - }) - } else { - const mappedKey = { - Hostname: 'host', - Port: 'port', - User: 'user', - ForwardX11: 'x11', - ServerAliveInterval: 'keepaliveInterval', - ServerAliveCountMax: 'keepaliveCountMax', - ProxyCommand: 'proxyCommand', - ProxyJump: 'jumpHost', - }[key] - if (mappedKey) { - target[mappedKey] = value - } - } - } - } - if (currentProfile) { - results.push(currentProfile) - } - for (const p of results) { - if (p.options?.proxyCommand) { - p.options.proxyCommand = p.options.proxyCommand - .replace('%h', p.options.host ?? '') - .replace('%p', (p.options.port ?? 22).toString()) - } - if (p.options?.jumpHost) { - p.options.jumpHost = deriveID(p.options.jumpHost) - } - } - return results - } catch (e) { - if (e.code === 'ENOENT') { - return [] - } - throw e - } -} diff --git a/tabby-ssh/src/profiles.ts b/tabby-ssh/src/profiles.ts index c34c0e6d..f43bbca2 100644 --- a/tabby-ssh/src/profiles.ts +++ b/tabby-ssh/src/profiles.ts @@ -1,11 +1,11 @@ -import { Injectable } from '@angular/core' +import { Inject, Injectable, Optional } from '@angular/core' import { ProfileProvider, NewTabParameters, PartialProfile, TranslateService } from 'tabby-core' import * as ALGORITHMS from 'ssh2/lib/protocol/constants' import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component' import { SSHTabComponent } from './components/sshTab.component' import { PasswordStorageService } from './services/passwordStorage.service' import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api' -import { parseOpenSSHProfiles } from './openSSHImport' +import { SSHProfileImporter } from './api/importer' @Injectable({ providedIn: 'root' }) export class SSHProfilesService extends ProfileProvider { @@ -47,6 +47,7 @@ export class SSHProfilesService extends ProfileProvider { constructor ( private passwordStorage: PasswordStorageService, private translate: TranslateService, + @Inject(SSHProfileImporter) @Optional() private importers: SSHProfileImporter[]|null, ) { super() for (const k of Object.values(SSHAlgorithmType)) { @@ -63,10 +64,12 @@ export class SSHProfilesService extends ProfileProvider { async getBuiltinProfiles (): Promise[]> { let imported: PartialProfile[] = [] - try { - imported = await parseOpenSSHProfiles() - } catch (e) { - console.warn('Could not parse OpenSSH config:', e) + for (const importer of this.importers ?? []) { + try { + imported = imported.concat(await importer.getProfiles()) + } catch (e) { + console.warn('Could not parse OpenSSH config:', e) + } } return [ { @@ -85,7 +88,6 @@ export class SSHProfilesService extends ProfileProvider { }, ...imported.map(p => ({ ...p, - name: p.name + ' (.ssh/config)', isBuiltin: true, })), ]