Add support to connect in winscp throug tunnel. (#10829)
Package-Build / Lint (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Docs / build (push) Has been cancelled
Package-Build / macOS-Build (arm64, aarch64-apple-darwin) (push) Has been cancelled
Package-Build / macOS-Build (x86_64, x86_64-apple-darwin) (push) Has been cancelled
Package-Build / Linux-Build (amd64, x64, ubuntu-24.04, x86_64-unknown-linux-gnu) (push) Has been cancelled
Package-Build / Linux-Build (arm64, arm64, ubuntu-24.04-arm, aarch64-unknown-linux-gnu, aarch64-linux-gnu-) (push) Has been cancelled
Package-Build / Linux-Build (armhf, arm, ubuntu-24.04, arm-unknown-linux-gnueabihf, arm-linux-gnueabihf-) (push) Has been cancelled
Package-Build / Windows-Build (arm64, aarch64-pc-windows-msvc) (push) Has been cancelled
Package-Build / Windows-Build (x64, x86_64-pc-windows-msvc) (push) Has been cancelled

This commit is contained in:
Indigo.liu
2025-11-11 19:14:42 +08:00
committed by GitHub
parent 7a7d3d2b77
commit 2cf6ef0036
+83 -24
View File
@@ -27,18 +27,89 @@ export class SSHService {
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}`
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
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> {
@@ -46,38 +117,26 @@ export class SSHService {
if (!path) {
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
try {
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)
} finally {
tmpFile?.cleanup()
winscpParms.privateKeyFile?.cleanup()
}
}
}