|
@@ -27,18 +27,89 @@ export class SSHService {
|
|
|
return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath
|
|
return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<string> {
|
|
|
|
|
|
|
+ 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 && 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}`
|
|
let uri = `scp://${username ?? profile.options.user}`
|
|
|
const password = await this.passwordStorage.loadPassword(profile, username)
|
|
const password = await this.passwordStorage.loadPassword(profile, username)
|
|
|
if (password) {
|
|
if (password) {
|
|
|
uri += ':' + encodeURIComponent(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(':')) {
|
|
if (profile.options.host.includes(':')) {
|
|
|
uri += `@[${profile.options.host}]:${profile.options.port}${cwd ?? '/'}`
|
|
uri += `@[${profile.options.host}]:${profile.options.port}${cwd ?? '/'}`
|
|
|
}else {
|
|
}else {
|
|
|
uri += `@${profile.options.host}:${profile.options.port}${cwd ?? '/'}`
|
|
uri += `@${profile.options.host}:${profile.options.port}${cwd ?? '/'}`
|
|
|
}
|
|
}
|
|
|
- return uri
|
|
|
|
|
|
|
+ return { uri, privateKeyFile: tmpFile?? null }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async convertPrivateKeyFileToPuTTYFormat (profile: SSHProfile): Promise<{ passphrase: string|null, privateKeyFile: tmp.FileResult|null }> {
|
|
|
|
|
+ if (!profile.options.privateKeys || 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> {
|
|
async launchWinSCP (session: SSHSession): Promise<void> {
|
|
@@ -46,38 +117,26 @@ export class SSHService {
|
|
|
if (!path) {
|
|
if (!path) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- const args = [await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)]
|
|
|
|
|
|
|
+ const winscpParms = await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)
|
|
|
|
|
+ const args = [winscpParms.uri]
|
|
|
|
|
|
|
|
let tmpFile: tmp.FileResult|null = null
|
|
let tmpFile: tmp.FileResult|null = null
|
|
|
try {
|
|
try {
|
|
|
if (session.activePrivateKey && session.profile.options.privateKeys && session.profile.options.privateKeys.length > 0) {
|
|
if (session.activePrivateKey && session.profile.options.privateKeys && session.profile.options.privateKeys.length > 0) {
|
|
|
- tmpFile = await tmp.file()
|
|
|
|
|
- let passphrase: string|null = null
|
|
|
|
|
- for (const pk of session.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
|
|
|
|
|
- passphrase = 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', passphrase])
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.warn('Could not convert private key ', error)
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ const profile = session.profile
|
|
|
|
|
+ const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(profile)
|
|
|
|
|
+ tmpFile = privateKeyPairs.privateKeyFile
|
|
|
|
|
+ if (tmpFile) {
|
|
|
|
|
+ args.push(`/privatekey=${tmpFile.path}`)
|
|
|
}
|
|
}
|
|
|
- args.push(`/privatekey=${tmpFile.path}`)
|
|
|
|
|
- if (passphrase != null) {
|
|
|
|
|
- args.push(`/passphrase=${passphrase}`)
|
|
|
|
|
|
|
+ if (privateKeyPairs.passphrase != null) {
|
|
|
|
|
+ args.push(`/passphrase=${privateKeyPairs.passphrase}`)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
await this.platform.exec(path, args)
|
|
await this.platform.exec(path, args)
|
|
|
} finally {
|
|
} finally {
|
|
|
tmpFile?.cleanup()
|
|
tmpFile?.cleanup()
|
|
|
|
|
+ winscpParms.privateKeyFile?.cleanup()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|