Skip to content

Commit 93102f7

Browse files
authored
Add backwards compatibility (#8)
1 parent df8736a commit 93102f7

File tree

4 files changed

+130
-53
lines changed

4 files changed

+130
-53
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"@types/google-protobuf": "^3.7.4",
112112
"@types/node": "16.x",
113113
"@types/node-fetch": "^2.5.12",
114+
"@types/semver": "^7.3.10",
114115
"@types/ssh2": "^0.5.52",
115116
"@types/tmp": "^0.2.1",
116117
"@types/uuid": "8.0.0",
@@ -136,6 +137,7 @@
136137
"analytics-node": "^6.0.0",
137138
"node-fetch": "2.6.7",
138139
"pkce-challenge": "^3.0.0",
140+
"semver": "^7.3.7",
139141
"ssh2": "^1.10.0",
140142
"tmp": "^0.2.1",
141143
"uuid": "8.1.0",

src/featureSupport.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import * as semver from 'semver';
6+
import fetch from 'node-fetch';
7+
8+
type Feature = |
9+
'SSHPublicKeys' |
10+
'localHeartbeat';
11+
12+
const DEFAULT_VERSION = '9999.99.99';
13+
let cacheGitpodVersion: { host: string; version: string } | undefined;
14+
export async function getGitpodVersion(gitpodHost: string) {
15+
const serviceUrl = new URL(gitpodHost).toString().replace(/\/$/, '');
16+
if (serviceUrl === 'https://gitpod.io') {
17+
return DEFAULT_VERSION;
18+
}
19+
20+
if (serviceUrl === cacheGitpodVersion?.host) {
21+
return cacheGitpodVersion.version;
22+
}
23+
24+
let gitpodVersion: string | null;
25+
try {
26+
const versionEndPoint = `${serviceUrl}/api/version`;
27+
const versionResponse = await fetch(versionEndPoint);
28+
if (!versionResponse.ok) {
29+
return DEFAULT_VERSION;
30+
}
31+
32+
gitpodVersion = await versionResponse.text();
33+
} catch (e) {
34+
return DEFAULT_VERSION;
35+
}
36+
37+
gitpodVersion = gitpodVersion.replace('release-', '');
38+
gitpodVersion = gitpodVersion.replace(/\.\d+$/, '');
39+
40+
// Remove leading zeros to make it a valid semver
41+
const [yy, mm, dd] = gitpodVersion.split('.');
42+
gitpodVersion = `${parseInt(yy, 10)}.${parseInt(mm, 10)}.${parseInt(dd, 10)}`;
43+
44+
gitpodVersion = semver.valid(gitpodVersion);
45+
if (!gitpodVersion) {
46+
return DEFAULT_VERSION;
47+
}
48+
49+
cacheGitpodVersion = {
50+
host: serviceUrl,
51+
version: gitpodVersion
52+
};
53+
54+
return cacheGitpodVersion.version;
55+
}
56+
57+
export function isFeatureSupported(gitpodVersion: string, feature: Feature) {
58+
switch (feature) {
59+
case 'SSHPublicKeys':
60+
case 'localHeartbeat':
61+
return semver.gte(gitpodVersion, '2022.7.0'); // Don't use leading zeros
62+
}
63+
}

src/remoteConnector.ts

Lines changed: 60 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import TelemetryReporter from './telemetryReporter';
2424
import { addHostToHostFile, checkNewHostInHostkeys } from './ssh/hostfile';
2525
import { checkDefaultIdentityFiles } from './ssh/identityFiles';
2626
import { HeartbeatManager } from './heartbeat';
27+
import { getGitpodVersion, isFeatureSupported } from './featureSupport';
2728

2829
interface SSHConnectionParams {
2930
workspaceId: string;
@@ -430,11 +431,12 @@ export default class RemoteConnector extends Disposable {
430431

431432
private async getWorkspaceSSHDestination(accessToken: string, { workspaceId, gitpodHost }: SSHConnectionParams): Promise<{ destination: string; password?: string }> {
432433
const serviceUrl = new URL(gitpodHost);
434+
const gitpodVersion = await getGitpodVersion(gitpodHost);
433435

434436
const [workspaceInfo, ownerToken, registeredSSHKeys] = await withServerApi(accessToken, serviceUrl.toString(), service => Promise.all([
435437
service.server.getWorkspace(workspaceId),
436438
service.server.getOwnerToken(workspaceId),
437-
service.server.getSSHPublicKeys()
439+
isFeatureSupported(gitpodVersion, 'SSHPublicKeys') ? service.server.getSSHPublicKeys() : undefined
438440
]), this.logger);
439441

440442
if (workspaceInfo.latestInstance?.status?.phase !== 'running') {
@@ -505,47 +507,32 @@ export default class RemoteConnector extends Disposable {
505507
let identityFilePaths = await checkDefaultIdentityFiles();
506508
this.logger.trace(`Default identity files:`, identityFilePaths.length ? identityFilePaths.toString() : 'None');
507509

508-
const keyFingerprints = registeredSSHKeys.map(i => i.fingerprint);
509-
const publickKeyFiles = await Promise.allSettled(identityFilePaths.map(path => fs.promises.readFile(path + '.pub')));
510-
identityFilePaths = identityFilePaths.filter((_, index) => {
511-
const result = publickKeyFiles[index];
512-
if (result.status === 'rejected') {
513-
return false;
514-
}
510+
if (registeredSSHKeys) {
511+
const keyFingerprints = registeredSSHKeys.map(i => i.fingerprint);
512+
const publickKeyFiles = await Promise.allSettled(identityFilePaths.map(path => fs.promises.readFile(path + '.pub')));
513+
identityFilePaths = identityFilePaths.filter((_, index) => {
514+
const result = publickKeyFiles[index];
515+
if (result.status === 'rejected') {
516+
return false;
517+
}
515518

516-
const parsedResult = sshUtils.parseKey(result.value);
517-
if (parsedResult instanceof Error || !parsedResult) {
518-
this.logger.error(`Error while parsing SSH public key${identityFilePaths[index] + '.pub'}:`, parsedResult);
519-
return false;
520-
}
519+
const parsedResult = sshUtils.parseKey(result.value);
520+
if (parsedResult instanceof Error || !parsedResult) {
521+
this.logger.error(`Error while parsing SSH public key${identityFilePaths[index] + '.pub'}:`, parsedResult);
522+
return false;
523+
}
521524

522-
const parsedKey = Array.isArray(parsedResult) ? parsedResult[0] : parsedResult;
523-
const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64');
524-
return keyFingerprints.includes(fingerprint);
525-
});
526-
this.logger.trace(`Registered public keys in Gitpod account:`, identityFilePaths.length ? identityFilePaths.toString() : 'None');
527-
528-
// Commented this for now as `checkDefaultIdentityFiles` seems enough
529-
// Connect to the OpenSSH agent and check for registered keys
530-
// let sshKeys: ParsedKey[] | undefined;
531-
// try {
532-
// if (process.env['SSH_AUTH_SOCK']) {
533-
// sshKeys = await new Promise<ParsedKey[]>((resolve, reject) => {
534-
// const sshAgent = new OpenSSHAgent(process.env['SSH_AUTH_SOCK']!);
535-
// sshAgent.getIdentities((err, publicKeys) => {
536-
// if (err) {
537-
// reject(err);
538-
// } else {
539-
// resolve(publicKeys!);
540-
// }
541-
// });
542-
// });
543-
// } else {
544-
// this.logger.error(`'SSH_AUTH_SOCK' env variable not defined, cannot connect to OpenSSH agent`);
545-
// }
546-
// } catch (e) {
547-
// this.logger.error(`Couldn't get identities from OpenSSH agent`, e);
548-
// }
525+
const parsedKey = Array.isArray(parsedResult) ? parsedResult[0] : parsedResult;
526+
const fingerprint = crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64');
527+
return keyFingerprints.includes(fingerprint);
528+
});
529+
this.logger.trace(`Registered public keys in Gitpod account:`, identityFilePaths.length ? identityFilePaths.toString() : 'None');
530+
} else {
531+
if (identityFilePaths.length) {
532+
sshDestInfo.user = `${workspaceId}#${ownerToken}`;
533+
}
534+
this.logger.warn(`Registered SSH public keys not supported in ${gitpodHost}, using version ${gitpodVersion}`);
535+
}
549536

550537
return {
551538
destination: Buffer.from(JSON.stringify(sshDestInfo), 'utf8').toString('hex'),
@@ -664,9 +651,17 @@ export default class RemoteConnector extends Disposable {
664651
this.logger.info(`Updated 'gitpod.host' setting to '${gitpodHost}' while trying to connect to a Gitpod workspace`);
665652
}
666653

654+
const gitpodVersion = await getGitpodVersion(gitpodHost);
655+
const sessionScopes = ['function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'resource:default'];
656+
if (isFeatureSupported(gitpodVersion, 'SSHPublicKeys') /* && isFeatureSupported('', 'sendHeartBeat') */) {
657+
sessionScopes.push('function:getSSHPublicKeys', 'function:sendHeartBeat');
658+
} else {
659+
this.logger.warn(`function:getSSHPublicKeys and function:sendHeartBeat session scopes not supported in ${gitpodHost}, using version ${gitpodVersion}`);
660+
}
661+
667662
return vscode.authentication.getSession(
668663
'gitpod',
669-
['function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'function:getSSHPublicKeys', 'function:sendHeartBeat', 'resource:default'],
664+
sessionScopes,
670665
{ createIfNone: true }
671666
);
672667
}
@@ -829,19 +824,31 @@ export default class RemoteConnector extends Disposable {
829824

830825
await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false });
831826

832-
// gitpod remote extension installation is async so sometimes gitpod-desktop will activate before gitpod-remote
833-
// let's wait a few seconds for it to finish install
834-
setTimeout(async () => {
835-
// Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time
836-
const isGitpodRemoteHeartbeatCancelled = await cancelGitpodRemoteHeartbeat();
837-
if (isGitpodRemoteHeartbeatCancelled) {
838-
const session = await this.getGitpodSession(connectionInfo.gitpodHost);
839-
if (session) {
840-
this.startHeartBeat(session.accessToken, connectionInfo);
827+
const gitpodVersion = await getGitpodVersion(connectionInfo.gitpodHost);
828+
if (isFeatureSupported(gitpodVersion, 'localHeartbeat')) {
829+
// gitpod remote extension installation is async so sometimes gitpod-desktop will activate before gitpod-remote
830+
// let's try a few times for it to finish install
831+
let retryCount = 10;
832+
const tryStartHeartbeat = async () => {
833+
// Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time
834+
const isGitpodRemoteHeartbeatCancelled = await cancelGitpodRemoteHeartbeat();
835+
if (isGitpodRemoteHeartbeatCancelled) {
836+
const session = await this.getGitpodSession(connectionInfo.gitpodHost);
837+
if (session) {
838+
this.startHeartBeat(session.accessToken, connectionInfo);
839+
}
840+
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(!!this.heartbeatManager), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId });
841+
} else if (retryCount > 0) {
842+
retryCount--;
843+
setTimeout(tryStartHeartbeat, 3000);
844+
} else {
845+
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(false), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId });
841846
}
842-
}
843-
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(!!this.heartbeatManager), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId });
844-
}, 7000);
847+
};
848+
tryStartHeartbeat();
849+
} else {
850+
this.logger.warn(`Local heatbeat not supported in ${connectionInfo.gitpodHost}, using version ${gitpodVersion}`);
851+
}
845852
}
846853

847854
public override async dispose(): Promise<void> {

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@
219219
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
220220
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
221221

222+
"@types/semver@^7.3.10":
223+
version "7.3.10"
224+
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.10.tgz#5f19ee40cbeff87d916eedc8c2bfe2305d957f73"
225+
integrity sha512-zsv3fsC7S84NN6nPK06u79oWgrPVd0NvOyqgghV1haPaFcVxIrP4DLomRwGAXk0ui4HZA7mOcSFL98sMVW9viw==
226+
222227
"@types/ssh2-streams@*":
223228
version "0.1.9"
224229
resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz#8ca51b26f08750a780f82ee75ff18d7160c07a87"

0 commit comments

Comments
 (0)