@@ -15,7 +15,8 @@ import Log from './common/logger';
15
15
import { Disposable } from './common/dispose' ;
16
16
import { withServerApi } from './internalApi' ;
17
17
import TelemetryReporter from './telemetryReporter' ;
18
- import { addHostToHostFile , checkNewHostInHostkeys } from './common/hostfile' ;
18
+ import { addHostToHostFile , checkNewHostInHostkeys } from './ssh/hostfile' ;
19
+ import { checkDefaultIdentityFiles } from './ssh/identityFiles' ;
19
20
20
21
interface SSHConnectionParams {
21
22
workspaceId : string ;
@@ -413,7 +414,7 @@ export default class RemoteConnector extends Disposable {
413
414
}
414
415
}
415
416
416
- private async getWorkspaceSSHDestination ( workspaceId : string , gitpodHost : string ) : Promise < string > {
417
+ private async getWorkspaceSSHDestination ( workspaceId : string , gitpodHost : string ) : Promise < { destination : string ; password ?: string } > {
417
418
const session = await vscode . authentication . getSession (
418
419
'gitpod' ,
419
420
[ 'function:getWorkspace' , 'function:getOwnerToken' , 'function:getLoggedInUser' , 'resource:default' ] ,
@@ -440,8 +441,9 @@ export default class RemoteConnector extends Disposable {
440
441
441
442
const ownerToken = await withServerApi ( session . accessToken , serviceUrl . toString ( ) , service => service . server . getOwnerToken ( workspaceId ) , this . logger ) ;
442
443
444
+ let password : string | undefined = ownerToken ;
443
445
const sshDestInfo = {
444
- user : ` ${ workspaceId } # ${ ownerToken } ` ,
446
+ user : workspaceId ,
445
447
// See https://github.com/gitpod-io/gitpod/pull/9786 for reasoning about `.ssh` suffix
446
448
hostName : workspaceUrl . host . replace ( workspaceId , `${ workspaceId } .ssh` )
447
449
} ;
@@ -487,10 +489,45 @@ export default class RemoteConnector extends Disposable {
487
489
this . logger . info ( `'${ sshDestInfo . hostName } ' host added to known_hosts file` ) ;
488
490
}
489
491
} catch ( e ) {
490
- this . logger . error ( `Couldn't write '${ sshDestInfo . hostName } ' host to known_hosts file` , e ) ;
492
+ this . logger . error ( `Couldn't write '${ sshDestInfo . hostName } ' host to known_hosts file: ` , e ) ;
491
493
}
492
494
493
- return Buffer . from ( JSON . stringify ( sshDestInfo ) , 'utf8' ) . toString ( 'hex' ) ;
495
+ const identityFiles = await checkDefaultIdentityFiles ( ) ;
496
+ this . logger . trace ( `Default identity files:` , identityFiles . length ? identityFiles . toString ( ) : 'None' ) ;
497
+
498
+ // Commented this for now as `checkDefaultIdentityFiles` seems enough
499
+ // Connect to the OpenSSH agent and check for registered keys
500
+ // let sshKeys: ParsedKey[] | undefined;
501
+ // try {
502
+ // if (process.env['SSH_AUTH_SOCK']) {
503
+ // sshKeys = await new Promise<ParsedKey[]>((resolve, reject) => {
504
+ // const sshAgent = new OpenSSHAgent(process.env['SSH_AUTH_SOCK']!);
505
+ // sshAgent.getIdentities((err, publicKeys) => {
506
+ // if (err) {
507
+ // reject(err);
508
+ // } else {
509
+ // resolve(publicKeys!);
510
+ // }
511
+ // });
512
+ // });
513
+ // } else {
514
+ // this.logger.error(`'SSH_AUTH_SOCK' env variable not defined, cannot connect to OpenSSH agent`);
515
+ // }
516
+ // } catch (e) {
517
+ // this.logger.error(`Couldn't get identities from OpenSSH agent`, e);
518
+ // }
519
+
520
+ // If user has default identity files or agent have registered keys,
521
+ // then use public key authentication
522
+ if ( identityFiles . length ) {
523
+ sshDestInfo . user = `${ workspaceId } #${ ownerToken } ` ;
524
+ password = undefined ;
525
+ }
526
+
527
+ return {
528
+ destination : Buffer . from ( JSON . stringify ( sshDestInfo ) , 'utf8' ) . toString ( 'hex' ) ,
529
+ password
530
+ } ;
494
531
}
495
532
496
533
private async getWorkspaceLocalAppSSHDestination ( params : SSHConnectionParams ) : Promise < { localAppSSHDest : string ; localAppSSHConfigPath : string } > {
@@ -571,13 +608,31 @@ export default class RemoteConnector extends Disposable {
571
608
return true ;
572
609
}
573
610
611
+ private async showSSHPasswordModal ( password : string ) {
612
+ const maskedPassword = '•' . repeat ( password . length - 3 ) + password . substring ( password . length - 3 ) ;
613
+
614
+ const copy = 'Copy' ;
615
+ const configureSSH = 'Configure SSH' ;
616
+ const action = await vscode . window . showInformationMessage ( `An SSH key is required for passwordless authentication.\nAlternatively, copy and use this password: ${ maskedPassword } ` , { modal : true } , copy , configureSSH ) ;
617
+ if ( action === copy ) {
618
+ await vscode . env . clipboard . writeText ( password ) ;
619
+ return ;
620
+ }
621
+ if ( action === configureSSH ) {
622
+ await vscode . env . openExternal ( vscode . Uri . parse ( 'https://www.gitpod.io/docs/configure/ssh#create-an-ssh-key' ) ) ;
623
+ throw new Error ( `SSH password modal dialog, ${ configureSSH } ` ) ;
624
+ }
625
+
626
+ throw new Error ( 'SSH password modal dialog, Canceled' ) ;
627
+ }
628
+
574
629
public async handleUri ( uri : vscode . Uri ) {
575
630
if ( uri . path === RemoteConnector . AUTH_COMPLETE_PATH ) {
576
631
this . logger . info ( 'auth completed' ) ;
577
632
return ;
578
633
}
579
634
580
- const isRemoteSSHExtInstalled = this . ensureRemoteSSHExtInstalled ( ) ;
635
+ const isRemoteSSHExtInstalled = await this . ensureRemoteSSHExtInstalled ( ) ;
581
636
if ( ! isRemoteSSHExtInstalled ) {
582
637
return ;
583
638
}
@@ -605,24 +660,29 @@ export default class RemoteConnector extends Disposable {
605
660
try {
606
661
this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'gateway' , status : 'connecting' , ...params } ) ;
607
662
608
- sshDestination = await this . getWorkspaceSSHDestination ( params . workspaceId , params . gitpodHost ) ;
663
+ const { destination, password } = await this . getWorkspaceSSHDestination ( params . workspaceId , params . gitpodHost ) ;
664
+ sshDestination = destination ;
665
+
666
+ if ( password ) {
667
+ await this . showSSHPasswordModal ( password ) ;
668
+ }
609
669
610
670
this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'gateway' , status : 'connected' , ...params } ) ;
611
671
} catch ( e ) {
612
672
this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'gateway' , status : 'failed' , reason : e . toString ( ) , ...params } ) ;
613
673
if ( e instanceof NoSSHGatewayError ) {
614
- this . logger . error ( 'No SSH gateway' , e ) ;
674
+ this . logger . error ( 'No SSH gateway: ' , e ) ;
615
675
vscode . window . showWarningMessage ( `${ e . host } does not support [direct SSH access](https://github.com/gitpod-io/gitpod/blob/main/install/installer/docs/workspace-ssh-access.md), connecting via the deprecated SSH tunnel over WebSocket.` ) ;
616
676
// Do nothing and continue execution
617
677
} else if ( e instanceof NoRunningInstanceError ) {
618
- this . logger . error ( 'No Running instance' , e ) ;
678
+ this . logger . error ( 'No Running instance: ' , e ) ;
619
679
vscode . window . showErrorMessage ( `Failed to connect to ${ e . workspaceId } Gitpod workspace: workspace not running` ) ;
620
680
return ;
621
681
} else {
622
682
if ( e instanceof SSHError ) {
623
- this . logger . error ( 'SSH test connection error' , e ) ;
683
+ this . logger . error ( 'SSH test connection error: ' , e ) ;
624
684
} else {
625
- this . logger . error ( `Failed to connect to ${ params . workspaceId } Gitpod workspace` , e ) ;
685
+ this . logger . error ( `Failed to connect to ${ params . workspaceId } Gitpod workspace: ` , e ) ;
626
686
}
627
687
const seeLogs = 'See Logs' ;
628
688
const showTroubleshooting = 'Show Troubleshooting' ;
@@ -649,7 +709,7 @@ export default class RemoteConnector extends Disposable {
649
709
650
710
this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'local-app' , status : 'connected' , ...params } ) ;
651
711
} catch ( e ) {
652
- this . logger . error ( `Failed to connect ${ params . workspaceId } Gitpod workspace` , e ) ;
712
+ this . logger . error ( `Failed to connect ${ params . workspaceId } Gitpod workspace: ` , e ) ;
653
713
if ( e instanceof LocalAppError ) {
654
714
this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'local-app' , status : 'failed' , reason : e . toString ( ) , ...params } ) ;
655
715
const seeLogs = 'See Logs' ;
@@ -692,7 +752,7 @@ export default class RemoteConnector extends Disposable {
692
752
) ;
693
753
} ) ;
694
754
} catch ( e ) {
695
- this . logger . error ( 'failed to disable auto tunneling' , e ) ;
755
+ this . logger . error ( 'Failed to disable auto tunneling: ' , e ) ;
696
756
}
697
757
}
698
758
}
0 commit comments