Skip to content

Commit bab83b6

Browse files
authored
Use new getSSHPublicKeys api method (#5)
1 parent 26ca9c3 commit bab83b6

File tree

4 files changed

+638
-125
lines changed

4 files changed

+638
-125
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@
116116
"@typescript-eslint/eslint-plugin": "^5.19.0",
117117
"@typescript-eslint/parser": "^5.19.0",
118118
"eslint": "^8.13.0",
119-
"eslint-plugin-jsdoc": "^19.1.0",
120119
"eslint-plugin-header": "3.1.1",
120+
"eslint-plugin-jsdoc": "^19.1.0",
121121
"minimist": "^1.2.6",
122122
"ts-loader": "^9.2.7",
123123
"typescript": "^4.6.3",

src/internalApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ReconnectingWebSocket from 'reconnecting-websocket';
1111
import * as vscode from 'vscode';
1212
import Log from './common/logger';
1313

14-
type UsedGitpodFunction = ['getLoggedInUser', 'getGitpodTokenScopes', 'getWorkspace', 'getOwnerToken'];
14+
type UsedGitpodFunction = ['getLoggedInUser', 'getGitpodTokenScopes', 'getWorkspace', 'getOwnerToken', 'getSSHPublicKeys'];
1515
type Union<Tuple extends any[], Union = never> = Tuple[number] | Union;
1616
export type GitpodConnection = Omit<GitpodServiceImpl<GitpodClient, GitpodServer>, 'server'> & {
1717
server: Pick<GitpodServer, Union<UsedGitpodFunction>>;

src/remoteConnector.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as cp from 'child_process';
1111
import * as fs from 'fs';
1212
import * as http from 'http';
1313
import * as net from 'net';
14+
import * as crypto from 'crypto';
1415
import fetch, { Response } from 'node-fetch';
1516
import { Client as sshClient, utils as sshUtils } from 'ssh2';
1617
import * as tmp from 'tmp';
@@ -419,13 +420,19 @@ export default class RemoteConnector extends Disposable {
419420
private async getWorkspaceSSHDestination(workspaceId: string, gitpodHost: string): Promise<{ destination: string; password?: string }> {
420421
const session = await vscode.authentication.getSession(
421422
'gitpod',
422-
['function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'resource:default'],
423+
['function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'function:getSSHPublicKeys', 'resource:default'],
423424
{ createIfNone: true }
424425
);
425426

426427
const serviceUrl = new URL(gitpodHost);
427428

428-
const workspaceInfo = await withServerApi(session.accessToken, serviceUrl.toString(), service => service.server.getWorkspace(workspaceId), this.logger);
429+
const [workspaceInfo, ownerToken, registeredSSHKeys] = await withServerApi(session.accessToken, serviceUrl.toString(), service => Promise.all([
430+
service.server.getWorkspace(workspaceId),
431+
service.server.getOwnerToken(workspaceId),
432+
service.server.getSSHPublicKeys()
433+
]), this.logger);
434+
435+
429436
if (workspaceInfo.latestInstance?.status?.phase !== 'running') {
430437
throw new NoRunningInstanceError(workspaceId);
431438
}
@@ -441,9 +448,6 @@ export default class RemoteConnector extends Disposable {
441448

442449
const sshHostKeys: { type: string; host_key: string }[] = await sshHostKeyResponse.json();
443450

444-
const ownerToken = await withServerApi(session.accessToken, serviceUrl.toString(), service => service.server.getOwnerToken(workspaceId), this.logger);
445-
446-
let password: string | undefined = ownerToken;
447451
const sshDestInfo = {
448452
user: workspaceId,
449453
// See https://github.com/gitpod-io/gitpod/pull/9786 for reasoning about `.ssh` suffix
@@ -494,8 +498,28 @@ export default class RemoteConnector extends Disposable {
494498
this.logger.error(`Couldn't write '${sshDestInfo.hostName}' host to known_hosts file:`, e);
495499
}
496500

497-
const identityFiles = await checkDefaultIdentityFiles();
498-
this.logger.trace(`Default identity files:`, identityFiles.length ? identityFiles.toString() : 'None');
501+
let identityFilePaths = await checkDefaultIdentityFiles();
502+
this.logger.trace(`Default identity files:`, identityFilePaths.length ? identityFilePaths.toString() : 'None');
503+
504+
const keyFingerprints = registeredSSHKeys.map(i => i.fingerprint);
505+
const publickKeyFiles = await Promise.allSettled(identityFilePaths.map(path => fs.promises.readFile(path + '.pub')));
506+
identityFilePaths = identityFilePaths.filter((_, index) => {
507+
const result = publickKeyFiles[index];
508+
if (result.status === 'rejected') {
509+
return false;
510+
}
511+
512+
const parsedResult = sshUtils.parseKey(result.value);
513+
if (parsedResult instanceof Error || !parsedResult) {
514+
this.logger.error(`Error while parsing SSH public key${identityFilePaths[index] + '.pub'}:`, parsedResult);
515+
return false;
516+
}
517+
518+
const parsedKey = Array.isArray(parsedResult) ? parsedResult[0] : parsedResult;
519+
const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64');
520+
return keyFingerprints.includes(fingerprint);
521+
});
522+
this.logger.trace(`Registered public keys in Gitpod account:`, identityFilePaths.length ? identityFilePaths.toString() : 'None');
499523

500524
// Commented this for now as `checkDefaultIdentityFiles` seems enough
501525
// Connect to the OpenSSH agent and check for registered keys
@@ -519,16 +543,9 @@ export default class RemoteConnector extends Disposable {
519543
// this.logger.error(`Couldn't get identities from OpenSSH agent`, e);
520544
// }
521545

522-
// If user has default identity files or agent have registered keys,
523-
// then use public key authentication
524-
if (identityFiles.length) {
525-
sshDestInfo.user = `${workspaceId}#${ownerToken}`;
526-
password = undefined;
527-
}
528-
529546
return {
530547
destination: Buffer.from(JSON.stringify(sshDestInfo), 'utf8').toString('hex'),
531-
password
548+
password: identityFilePaths.length === 0 ? ownerToken : undefined
532549
};
533550
}
534551

@@ -615,13 +632,13 @@ export default class RemoteConnector extends Disposable {
615632

616633
const copy = 'Copy';
617634
const configureSSH = 'Configure SSH';
618-
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);
635+
const action = await vscode.window.showWarningMessage(`You don't have registered any SSH public key for this machine in your Gitpod account.\nAlternatively, copy and use this temporary password until workspace restart: ${maskedPassword}`, { modal: true }, copy, configureSSH);
619636
if (action === copy) {
620637
await vscode.env.clipboard.writeText(password);
621638
return;
622639
}
623640
if (action === configureSSH) {
624-
await vscode.env.openExternal(vscode.Uri.parse('https://www.gitpod.io/docs/configure/ssh#create-an-ssh-key'));
641+
await vscode.env.openExternal(vscode.Uri.parse('https://gitpod.io/keys'));
625642
throw new Error(`SSH password modal dialog, ${configureSSH}`);
626643
}
627644

0 commit comments

Comments
 (0)