/
githubmirror
/
tabby
Обзор
Документация
Войти
/
githubmirror
/
tabby
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tabby-ssh/src/services/ssh.service.ts
142 строки
6 KB
Eugene
clarify partial/full profile types
24 янв 2026, 19:30
Не верифицирован
24 янв 2026, 19:30
438ac9a
Код
Авторство
О чём код?
import * as fs from 'fs/promises' import * as crypto from 'crypto' import * as tmp from 'tmp-promise' import { Injectable } from '@angular/core' import { ConfigService, FileProvidersService, HostAppService, Platform, PlatformService } from 'tabby-core' import { SSHSession } from '../session/ssh' import { SSHProfile } from '../api' import { PasswordStorageService } from './passwordStorage.service' @Injectable({ providedIn: 'root' }) export class SSHService { private detectedWinSCPPath: string | null private constructor ( private passwordStorage: PasswordStorageService, private config: ConfigService, hostApp: HostAppService, private platform: PlatformService, private fileProviders: FileProvidersService, ) { if (hostApp.platform === Platform.Windows) { this.detectedWinSCPPath = platform.getWinSCPPath() } } getWinSCPPath (): string|undefined { return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath } async generateWinSCPXTunnelURI (jumpHostProfile: SSHProfile|null): Promise<{ uri: string|null, privateKeyFile?: tmp.FileResult|null }> { let uri = '' let tmpFile: tmp.FileResult|null = null if (jumpHostProfile) { uri += ';x-tunnel=1' const jumpHostname = jumpHostProfile.options.host uri += `;x-tunnelhostname=${jumpHostname}` const jumpPort = jumpHostProfile.options.port ?? 22 uri += `;x-tunnelportnumber=${jumpPort}` const jumpUsername = jumpHostProfile.options.user uri += `;x-tunnelusername=${jumpUsername}` if (jumpHostProfile.options.auth === 'password') { const jumpPassword = await this.passwordStorage.loadPassword(jumpHostProfile, jumpUsername) if (jumpPassword) { uri += `;x-tunnelpasswordplain=${encodeURIComponent(jumpPassword)}` } } if (jumpHostProfile.options.auth === 'publicKey' && jumpHostProfile.options.privateKeys.length > 0) { const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(jumpHostProfile) tmpFile = privateKeyPairs.privateKeyFile if (tmpFile) { uri += `;x-tunnelpublickeyfile=${encodeURIComponent(tmpFile.path)}` } if (privateKeyPairs.passphrase != null) { uri += `;x-tunnelpassphraseplain=${encodeURIComponent(privateKeyPairs.passphrase)}` } } } return { uri: uri, privateKeyFile: tmpFile?? null } } async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<{ uri: string, privateKeyFile?: tmp.FileResult|null }> { let uri = `scp://${username ?? profile.options.user}` const password = await this.passwordStorage.loadPassword(profile, username) if (password) { uri += ':' + encodeURIComponent(password) } let tmpFile: tmp.FileResult|null = null if (profile.options.jumpHost) { const jumpHostProfile = this.config.store.profiles.find(x => x.id === profile.options.jumpHost) ?? null const xTunnelParams = await this.generateWinSCPXTunnelURI(jumpHostProfile) uri += xTunnelParams.uri ?? '' tmpFile = xTunnelParams.privateKeyFile ?? null } if (profile.options.host.includes(':')) { uri += `@[${profile.options.host}]:${profile.options.port}${cwd ?? '/'}` }else { uri += `@${profile.options.host}:${profile.options.port}${cwd ?? '/'}` } return { uri, privateKeyFile: tmpFile?? null } } async convertPrivateKeyFileToPuTTYFormat (profile: SSHProfile): Promise<{ passphrase: string|null, privateKeyFile: tmp.FileResult|null }> { if (profile.options.privateKeys.length === 0) { throw new Error('No private keys in profile') } const path = this.getWinSCPPath() if (!path) { throw new Error('WinSCP not found') } let tmpPrivateKeyFile: tmp.FileResult|null = null let passphrase: string|null = null const tmpFile: tmp.FileResult = await tmp.file() for (const pk of profile.options.privateKeys) { let privateKeyContent: string|null = null const buffer = await this.fileProviders.retrieveFile(pk) privateKeyContent = buffer.toString() await fs.writeFile(tmpFile.path, privateKeyContent) const keyHash = crypto.createHash('sha512').update(privateKeyContent).digest('hex') // need to pass an default passphrase, otherwise it might get stuck at the passphrase input const curPassphrase = await this.passwordStorage.loadPrivateKeyPassword(keyHash) ?? 'tabby' const winSCPcom = path.slice(0, -3) + 'com' try { await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, '-o', tmpFile.path, '--old-passphrase', curPassphrase]) } catch (error) { console.warn('Could not convert private key ', error) continue } tmpPrivateKeyFile = tmpFile passphrase = curPassphrase break } return { passphrase, privateKeyFile: tmpPrivateKeyFile } } async launchWinSCP (session: SSHSession): Promise<void> { const path = this.getWinSCPPath() if (!path) { return } const winscpParms = await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined) const args = [winscpParms.uri] let tmpFile: tmp.FileResult|null = null try { if (session.activePrivateKey && session.profile.options.privateKeys.length > 0) { const profile = session.profile const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(profile) tmpFile = privateKeyPairs.privateKeyFile if (tmpFile) { args.push(`/privatekey=${tmpFile.path}`) } if (privateKeyPairs.passphrase != null) { args.push(`/passphrase=${privateKeyPairs.passphrase}`) } } await this.platform.exec(path, args) } finally { tmpFile?.cleanup() winscpParms.privateKeyFile?.cleanup() } } }