Browse Source

split openssh-importer into tabby-electron, support tilde in private key paths - fixes #5627

Eugene Pankov 4 years ago
parent
commit
9e9066d3cd

+ 3 - 1
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 {

+ 128 - 0
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<PartialProfile<SSHProfile>[]> {
+        const results: PartialProfile<SSHProfile>[] = []
+        const configPath = path.join(process.env.HOME ?? '~', '.ssh', 'config')
+        try {
+            const lines = (await fs.readFile(configPath, 'utf8')).split('\n')
+            const globalOptions: Partial<SSHProfileOptions> = {}
+            let currentProfile: PartialProfile<SSHProfile>|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<SSHProfileOptions> = 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
+        }
+    }
+}

+ 6 - 0
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<PartialProfile<SSHProfile>[]>
+}

+ 1 - 0
tabby-ssh/src/api/index.ts

@@ -1,2 +1,3 @@
 export * from './contextMenu'
 export * from './interfaces'
+export * from './importer'

+ 0 - 121
tabby-ssh/src/openSSHImport.ts

@@ -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<PartialProfile<SSHProfile>[]> {
-    const results: PartialProfile<SSHProfile>[] = []
-    const configPath = path.join(process.env.HOME ?? '~', '.ssh', 'config')
-    try {
-        const lines = (await fs.readFile(configPath, 'utf8')).split('\n')
-        const globalOptions: Partial<SSHProfileOptions> = {}
-        let currentProfile: PartialProfile<SSHProfile>|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<SSHProfileOptions> = 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
-    }
-}

+ 9 - 7
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<SSHProfile> {
@@ -47,6 +47,7 @@ export class SSHProfilesService extends ProfileProvider<SSHProfile> {
     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<SSHProfile> {
 
     async getBuiltinProfiles (): Promise<PartialProfile<SSHProfile>[]> {
         let imported: PartialProfile<SSHProfile>[] = []
-        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<SSHProfile> {
             },
             ...imported.map(p => ({
                 ...p,
-                name: p.name + ' (.ssh/config)',
                 isBuiltin: true,
             })),
         ]