Skip to content

Commit 2cf6ef0

Browse files
authored
Add support to connect in winscp throug tunnel. (#10829)
1 parent 7a7d3d2 commit 2cf6ef0

File tree

1 file changed

+83
-24
lines changed

1 file changed

+83
-24
lines changed

tabby-ssh/src/services/ssh.service.ts

Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,57 +27,116 @@ export class SSHService {
2727
return this.detectedWinSCPPath ?? this.config.store.ssh.winSCPPath
2828
}
2929

30-
async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<string> {
30+
async generateWinSCPXTunnelURI (jumpHostProfile: SSHProfile|null): Promise<{ uri: string|null, privateKeyFile?: tmp.FileResult|null }> {
31+
let uri = ''
32+
let tmpFile: tmp.FileResult|null = null
33+
if (jumpHostProfile) {
34+
uri += ';x-tunnel=1'
35+
const jumpHostname = jumpHostProfile.options.host
36+
uri += `;x-tunnelhostname=${jumpHostname}`
37+
const jumpPort = jumpHostProfile.options.port ?? 22
38+
uri += `;x-tunnelportnumber=${jumpPort}`
39+
const jumpUsername = jumpHostProfile.options.user
40+
uri += `;x-tunnelusername=${jumpUsername}`
41+
if (jumpHostProfile.options.auth === 'password') {
42+
const jumpPassword = await this.passwordStorage.loadPassword(jumpHostProfile, jumpUsername)
43+
if (jumpPassword) {
44+
uri += `;x-tunnelpasswordplain=${encodeURIComponent(jumpPassword)}`
45+
}
46+
}
47+
if (jumpHostProfile.options.auth === 'publicKey' && jumpHostProfile.options.privateKeys && jumpHostProfile.options.privateKeys.length > 0) {
48+
const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(jumpHostProfile)
49+
tmpFile = privateKeyPairs.privateKeyFile
50+
if (tmpFile) {
51+
uri += `;x-tunnelpublickeyfile=${encodeURIComponent(tmpFile.path)}`
52+
}
53+
if (privateKeyPairs.passphrase != null) {
54+
uri += `;x-tunnelpassphraseplain=${encodeURIComponent(privateKeyPairs.passphrase)}`
55+
}
56+
}
57+
}
58+
return { uri: uri, privateKeyFile: tmpFile?? null }
59+
}
60+
61+
async getWinSCPURI (profile: SSHProfile, cwd?: string, username?: string): Promise<{ uri: string, privateKeyFile?: tmp.FileResult|null }> {
3162
let uri = `scp://${username ?? profile.options.user}`
3263
const password = await this.passwordStorage.loadPassword(profile, username)
3364
if (password) {
3465
uri += ':' + encodeURIComponent(password)
3566
}
67+
let tmpFile: tmp.FileResult|null = null
68+
if (profile.options.jumpHost) {
69+
const jumpHostProfile = this.config.store.profiles.find(x => x.id === profile.options.jumpHost) ?? null
70+
const xTunnelParams = await this.generateWinSCPXTunnelURI(jumpHostProfile)
71+
uri += xTunnelParams.uri ?? ''
72+
tmpFile = xTunnelParams.privateKeyFile ?? null
73+
}
3674
if (profile.options.host.includes(':')) {
3775
uri += `@[${profile.options.host}]:${profile.options.port}${cwd ?? '/'}`
3876
}else {
3977
uri += `@${profile.options.host}:${profile.options.port}${cwd ?? '/'}`
4078
}
41-
return uri
79+
return { uri, privateKeyFile: tmpFile?? null }
80+
}
81+
82+
async convertPrivateKeyFileToPuTTYFormat (profile: SSHProfile): Promise<{ passphrase: string|null, privateKeyFile: tmp.FileResult|null }> {
83+
if (!profile.options.privateKeys || profile.options.privateKeys.length === 0) {
84+
throw new Error('No private keys in profile')
85+
}
86+
const path = this.getWinSCPPath()
87+
if (!path) {
88+
throw new Error('WinSCP not found')
89+
}
90+
let tmpPrivateKeyFile: tmp.FileResult|null = null
91+
let passphrase: string|null = null
92+
const tmpFile: tmp.FileResult = await tmp.file()
93+
for (const pk of profile.options.privateKeys) {
94+
let privateKeyContent: string|null = null
95+
const buffer = await this.fileProviders.retrieveFile(pk)
96+
privateKeyContent = buffer.toString()
97+
await fs.writeFile(tmpFile.path, privateKeyContent)
98+
const keyHash = crypto.createHash('sha512').update(privateKeyContent).digest('hex')
99+
// need to pass an default passphrase, otherwise it might get stuck at the passphrase input
100+
const curPassphrase = await this.passwordStorage.loadPrivateKeyPassword(keyHash) ?? 'tabby'
101+
const winSCPcom = path.slice(0, -3) + 'com'
102+
try {
103+
await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, '-o', tmpFile.path, '--old-passphrase', curPassphrase])
104+
} catch (error) {
105+
console.warn('Could not convert private key ', error)
106+
continue
107+
}
108+
tmpPrivateKeyFile = tmpFile
109+
passphrase = curPassphrase
110+
break
111+
}
112+
return { passphrase, privateKeyFile: tmpPrivateKeyFile }
42113
}
43114

44115
async launchWinSCP (session: SSHSession): Promise<void> {
45116
const path = this.getWinSCPPath()
46117
if (!path) {
47118
return
48119
}
49-
const args = [await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)]
120+
const winscpParms = await this.getWinSCPURI(session.profile, undefined, session.authUsername ?? undefined)
121+
const args = [winscpParms.uri]
50122

51123
let tmpFile: tmp.FileResult|null = null
52124
try {
53125
if (session.activePrivateKey && session.profile.options.privateKeys && session.profile.options.privateKeys.length > 0) {
54-
tmpFile = await tmp.file()
55-
let passphrase: string|null = null
56-
for (const pk of session.profile.options.privateKeys) {
57-
let privateKeyContent: string|null = null
58-
const buffer = await this.fileProviders.retrieveFile(pk)
59-
privateKeyContent = buffer.toString()
60-
await fs.writeFile(tmpFile.path, privateKeyContent)
61-
const keyHash = crypto.createHash('sha512').update(privateKeyContent).digest('hex')
62-
// need to pass an default passphrase, otherwise it might get stuck at the passphrase input
63-
passphrase = await this.passwordStorage.loadPrivateKeyPassword(keyHash) ?? 'tabby'
64-
const winSCPcom = path.slice(0, -3) + 'com'
65-
try {
66-
await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, '-o', tmpFile.path, '--old-passphrase', passphrase])
67-
} catch (error) {
68-
console.warn('Could not convert private key ', error)
69-
continue
70-
}
71-
break
126+
const profile = session.profile
127+
const privateKeyPairs = await this.convertPrivateKeyFileToPuTTYFormat(profile)
128+
tmpFile = privateKeyPairs.privateKeyFile
129+
if (tmpFile) {
130+
args.push(`/privatekey=${tmpFile.path}`)
72131
}
73-
args.push(`/privatekey=${tmpFile.path}`)
74-
if (passphrase != null) {
75-
args.push(`/passphrase=${passphrase}`)
132+
if (privateKeyPairs.passphrase != null) {
133+
args.push(`/passphrase=${privateKeyPairs.passphrase}`)
76134
}
77135
}
78136
await this.platform.exec(path, args)
79137
} finally {
80138
tmpFile?.cleanup()
139+
winscpParms.privateKeyFile?.cleanup()
81140
}
82141
}
83142
}

0 commit comments

Comments
 (0)