@@ -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