mirror of
https://github.com/Eugeny/tabby
synced 2024-11-15 01:17:14 +00:00
split openssh-importer into tabby-electron, support tilde in private key paths - fixes #5627
This commit is contained in:
parent
b469dd603b
commit
9e9066d3cd
6 changed files with 147 additions and 129 deletions
|
@ -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
tabby-electron/src/openSSHImport.ts
Normal file
128
tabby-electron/src/openSSHImport.ts
Normal file
|
@ -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
tabby-ssh/src/api/importer.ts
Normal file
6
tabby-ssh/src/api/importer.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { PartialProfile } from 'tabby-core'
|
||||
import { SSHProfile } from './interfaces'
|
||||
|
||||
export abstract class SSHProfileImporter {
|
||||
abstract getProfiles (): Promise<PartialProfile<SSHProfile>[]>
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from './contextMenu'
|
||||
export * from './interfaces'
|
||||
export * from './importer'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})),
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue