@@ -15,7 +15,8 @@ import Log from './common/logger';
1515import { Disposable } from './common/dispose' ;
1616import { withServerApi } from './internalApi' ;
1717import TelemetryReporter from './telemetryReporter' ;
18- import { addHostToHostFile , checkNewHostInHostkeys } from './common/hostfile' ;
18+ import { addHostToHostFile , checkNewHostInHostkeys } from './ssh/hostfile' ;
19+ import { checkDefaultIdentityFiles } from './ssh/identityFiles' ;
1920
2021interface SSHConnectionParams {
2122 workspaceId : string ;
@@ -413,7 +414,7 @@ export default class RemoteConnector extends Disposable {
413414 }
414415 }
415416
416- private async getWorkspaceSSHDestination ( workspaceId : string , gitpodHost : string ) : Promise < string > {
417+ private async getWorkspaceSSHDestination ( workspaceId : string , gitpodHost : string ) : Promise < { destination : string ; password ?: string } > {
417418 const session = await vscode . authentication . getSession (
418419 'gitpod' ,
419420 [ 'function:getWorkspace' , 'function:getOwnerToken' , 'function:getLoggedInUser' , 'resource:default' ] ,
@@ -440,8 +441,9 @@ export default class RemoteConnector extends Disposable {
440441
441442 const ownerToken = await withServerApi ( session . accessToken , serviceUrl . toString ( ) , service => service . server . getOwnerToken ( workspaceId ) , this . logger ) ;
442443
444+ let password : string | undefined = ownerToken ;
443445 const sshDestInfo = {
444- user : ` ${ workspaceId } # ${ ownerToken } ` ,
446+ user : workspaceId ,
445447 // See https://github.com/gitpod-io/gitpod/pull/9786 for reasoning about `.ssh` suffix
446448 hostName : workspaceUrl . host . replace ( workspaceId , `${ workspaceId } .ssh` )
447449 } ;
@@ -487,10 +489,45 @@ export default class RemoteConnector extends Disposable {
487489 this . logger . info ( `'${ sshDestInfo . hostName } ' host added to known_hosts file` ) ;
488490 }
489491 } 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 ) ;
491493 }
492494
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+ } ;
494531 }
495532
496533 private async getWorkspaceLocalAppSSHDestination ( params : SSHConnectionParams ) : Promise < { localAppSSHDest : string ; localAppSSHConfigPath : string } > {
@@ -571,13 +608,31 @@ export default class RemoteConnector extends Disposable {
571608 return true ;
572609 }
573610
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+
574629 public async handleUri ( uri : vscode . Uri ) {
575630 if ( uri . path === RemoteConnector . AUTH_COMPLETE_PATH ) {
576631 this . logger . info ( 'auth completed' ) ;
577632 return ;
578633 }
579634
580- const isRemoteSSHExtInstalled = this . ensureRemoteSSHExtInstalled ( ) ;
635+ const isRemoteSSHExtInstalled = await this . ensureRemoteSSHExtInstalled ( ) ;
581636 if ( ! isRemoteSSHExtInstalled ) {
582637 return ;
583638 }
@@ -605,24 +660,29 @@ export default class RemoteConnector extends Disposable {
605660 try {
606661 this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'gateway' , status : 'connecting' , ...params } ) ;
607662
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+ }
609669
610670 this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'gateway' , status : 'connected' , ...params } ) ;
611671 } catch ( e ) {
612672 this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'gateway' , status : 'failed' , reason : e . toString ( ) , ...params } ) ;
613673 if ( e instanceof NoSSHGatewayError ) {
614- this . logger . error ( 'No SSH gateway' , e ) ;
674+ this . logger . error ( 'No SSH gateway: ' , e ) ;
615675 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.` ) ;
616676 // Do nothing and continue execution
617677 } else if ( e instanceof NoRunningInstanceError ) {
618- this . logger . error ( 'No Running instance' , e ) ;
678+ this . logger . error ( 'No Running instance: ' , e ) ;
619679 vscode . window . showErrorMessage ( `Failed to connect to ${ e . workspaceId } Gitpod workspace: workspace not running` ) ;
620680 return ;
621681 } else {
622682 if ( e instanceof SSHError ) {
623- this . logger . error ( 'SSH test connection error' , e ) ;
683+ this . logger . error ( 'SSH test connection error: ' , e ) ;
624684 } 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 ) ;
626686 }
627687 const seeLogs = 'See Logs' ;
628688 const showTroubleshooting = 'Show Troubleshooting' ;
@@ -649,7 +709,7 @@ export default class RemoteConnector extends Disposable {
649709
650710 this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'local-app' , status : 'connected' , ...params } ) ;
651711 } 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 ) ;
653713 if ( e instanceof LocalAppError ) {
654714 this . telemetry . sendTelemetryEvent ( 'vscode_desktop_ssh' , { kind : 'local-app' , status : 'failed' , reason : e . toString ( ) , ...params } ) ;
655715 const seeLogs = 'See Logs' ;
@@ -692,7 +752,7 @@ export default class RemoteConnector extends Disposable {
692752 ) ;
693753 } ) ;
694754 } catch ( e ) {
695- this . logger . error ( 'failed to disable auto tunneling' , e ) ;
755+ this . logger . error ( 'Failed to disable auto tunneling: ' , e ) ;
696756 }
697757 }
698758}
0 commit comments