Skip to content

Commit 15a8665

Browse files
committed
Use /tunnel/ssh endpoint
1 parent 3429f4f commit 15a8665

File tree

1 file changed

+40
-91
lines changed

1 file changed

+40
-91
lines changed

src/local-ssh/proxy.ts

Lines changed: 40 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { SshClient } from '@microsoft/dev-tunnels-ssh-tcp';
7-
import { ChannelOpenMessage, NodeStream, SshClientCredentials, SshClientSession, SshDataWriter, SshDisconnectReason, SshServerSession, SshSessionConfiguration, SshStream, Stream, WebSocketStream } from '@microsoft/dev-tunnels-ssh';
7+
import { NodeStream, SshClientCredentials, SshClientSession, SshDisconnectReason, SshServerSession, SshSessionConfiguration, Stream, WebSocketStream } from '@microsoft/dev-tunnels-ssh';
88
import { importKey, importKeyBytes } from '@microsoft/dev-tunnels-ssh-keys';
99
import { ExtensionServiceDefinition, GetWorkspaceAuthInfoResponse, SendErrorReportRequest, SendLocalSSHUserFlowStatusRequest_Code, SendLocalSSHUserFlowStatusRequest_ConnType, SendLocalSSHUserFlowStatusRequest_Status } from '../proto/typescript/ipc/v1/ipc';
1010
import { Client, ClientError, Status, createChannel, createClient } from 'nice-grpc';
1111
import { retryWithStop } from '../common/async';
12-
import { TunnelPortRequest } from '@gitpod/supervisor-api-grpc/lib/port_pb';
1312
import { WebSocket } from 'ws';
1413
import * as stream from 'stream';
1514
import { ILogService } from '../services/logService';
@@ -58,28 +57,6 @@ class AuthenticationError extends Error {
5857
}
5958
}
6059

61-
class SupervisorPortTunnelMessage extends ChannelOpenMessage {
62-
constructor(private clientId: string, private remotePort: number, channelType: string) {
63-
super();
64-
this.channelType = channelType;
65-
}
66-
67-
override onWrite(writer: SshDataWriter): void {
68-
super.onWrite(writer);
69-
const req = new TunnelPortRequest();
70-
req.setClientId(this.clientId);
71-
req.setTargetPort(this.remotePort);
72-
req.setPort(this.remotePort);
73-
74-
let bytes = req.serializeBinary();
75-
writer.write(Buffer.from(bytes));
76-
}
77-
78-
override toString() {
79-
return `${super.toString()}`;
80-
}
81-
}
82-
8360
// TODO(local-ssh): Remove me after direct ssh works with @microsft/dev-tunnels-ssh
8461
const FORCE_TUNNEL = true;
8562

@@ -162,14 +139,17 @@ class WebSocketSSHProxy {
162139
if (FORCE_TUNNEL) {
163140
return this.getTunnelSSHConfig(workspaceInfo);
164141
}
165-
const session = await this.tryDirectSSH(workspaceInfo);
166-
if (!session) {
167-
return this.getTunnelSSHConfig(workspaceInfo);
168-
}
169-
return session;
142+
143+
try {
144+
const session = await this.tryDirectSSH(workspaceInfo);
145+
return session;
146+
} catch (e) {
147+
this.logService.error('failed to connect with direct ssh, going to try tunnel');
148+
return this.getTunnelSSHConfig(workspaceInfo);
149+
}
170150
}
171151

172-
private async tryDirectSSH(workspaceInfo: GetWorkspaceAuthInfoResponse): Promise<SshClientSession | undefined> {
152+
private async tryDirectSSH(workspaceInfo: GetWorkspaceAuthInfoResponse): Promise<SshClientSession> {
173153
try {
174154
const connConfig = {
175155
host: `${workspaceInfo.workspaceId}.ssh.${workspaceInfo.workspaceHost}`,
@@ -200,18 +180,44 @@ class WebSocketSSHProxy {
200180
daemonVersion: getDaemonVersion(),
201181
connType: SendLocalSSHUserFlowStatusRequest_ConnType.CONN_TYPE_SSH,
202182
});
183+
throw e;
203184
}
204-
return;
205185
}
206186

207187
private async getTunnelSSHConfig(workspaceInfo: GetWorkspaceAuthInfoResponse): Promise<SshClientSession> {
188+
const workspaceWSUrl = `wss://${workspaceInfo.workspaceId}.${workspaceInfo.workspaceHost}`;
189+
const socket = new WebSocket(workspaceWSUrl + '/_supervisor/tunnel/ssh', undefined, {
190+
headers: {
191+
'x-gitpod-owner-token': workspaceInfo.ownerToken
192+
}
193+
});
194+
socket.binaryType = 'arraybuffer';
195+
196+
const stream = await new Promise<Stream>((resolve, reject) => {
197+
socket.onopen = () => resolve(new WebSocketStream(socket as any));
198+
socket.onerror = (e) => reject(e);
199+
}).catch(e => {
200+
this.extensionIpc.sendLocalSSHUserFlowStatus({
201+
gitpodHost: workspaceInfo.gitpodHost,
202+
userId: workspaceInfo.userId,
203+
status: SendLocalSSHUserFlowStatusRequest_Status.STATUS_FAILURE,
204+
workspaceId: workspaceInfo.workspaceId,
205+
instanceId: workspaceInfo.instanceId,
206+
failureCode: SendLocalSSHUserFlowStatusRequest_Code.CODE_TUNNEL_CANNOT_CREATE_WEBSOCKET,
207+
daemonVersion: getDaemonVersion(),
208+
connType: SendLocalSSHUserFlowStatusRequest_ConnType.CONN_TYPE_TUNNEL,
209+
});
210+
throw e;
211+
});
212+
208213
try {
209-
const connConfig = await this.establishTunnel(workspaceInfo);
210214
const config = new SshSessionConfiguration();
211215
const session = new SshClientSession(config);
212216
session.onAuthenticating((e) => e.authenticationPromise = Promise.resolve({}));
213-
await session.connect(new NodeStream(connConfig.sock));
214-
const ok = await session.authenticate({ username: connConfig.username, publicKeys: [await importKey(workspaceInfo.sshkey)] });
217+
218+
await session.connect(stream);
219+
220+
const ok = await session.authenticate({ username: 'gitpod', publicKeys: [await importKey(workspaceInfo.sshkey)] });
215221
if (!ok) {
216222
throw new AuthenticationError();
217223
}
@@ -248,63 +254,6 @@ class WebSocketSSHProxy {
248254
}, 200, 50);
249255
}
250256

251-
async establishTunnel(workspaceInfo: GetWorkspaceAuthInfoResponse) {
252-
const workspaceWSUrl = `wss://${workspaceInfo.workspaceId}.${workspaceInfo.workspaceHost}`;
253-
const socket = new WebSocket(workspaceWSUrl + '/_supervisor/tunnel', undefined, {
254-
headers: {
255-
'x-gitpod-owner-token': workspaceInfo.ownerToken
256-
}
257-
});
258-
259-
socket.binaryType = 'arraybuffer';
260-
const stream = await new Promise<Stream>((resolve, reject) => {
261-
socket.onopen = () => {
262-
resolve(new WebSocketStream(socket as any));
263-
};
264-
socket.onerror = (e) => {
265-
this.extensionIpc.sendLocalSSHUserFlowStatus({
266-
gitpodHost: workspaceInfo.gitpodHost,
267-
userId: workspaceInfo.userId,
268-
status: SendLocalSSHUserFlowStatusRequest_Status.STATUS_FAILURE,
269-
workspaceId: workspaceInfo.workspaceId,
270-
instanceId: workspaceInfo.instanceId,
271-
failureCode: SendLocalSSHUserFlowStatusRequest_Code.CODE_TUNNEL_CANNOT_CREATE_WEBSOCKET,
272-
daemonVersion: getDaemonVersion(),
273-
connType: SendLocalSSHUserFlowStatusRequest_ConnType.CONN_TYPE_TUNNEL,
274-
});
275-
reject(e);
276-
};
277-
});
278-
279-
const config = new SshSessionConfiguration();
280-
const session = new SshClientSession(config);
281-
session.onAuthenticating((e) => e.authenticationPromise = Promise.resolve({}));
282-
await session.connect(stream);
283-
284-
const credentials: SshClientCredentials = { username: 'gitpodlocal' };
285-
const authenticated = await session.authenticate(credentials);
286-
if (!authenticated) {
287-
throw new AuthenticationError();
288-
}
289-
const clientID = 'tunnel_' + Math.random().toString(36).slice(2);
290-
const msg = new SupervisorPortTunnelMessage(clientID, 23001, 'tunnel');
291-
const channel = await session.openChannel(msg).catch(e => {
292-
this.logService.error(e, 'failed to open channel');
293-
this.extensionIpc.sendLocalSSHUserFlowStatus({
294-
gitpodHost: workspaceInfo.gitpodHost,
295-
userId: workspaceInfo.userId,
296-
status: SendLocalSSHUserFlowStatusRequest_Status.STATUS_FAILURE,
297-
workspaceId: workspaceInfo.workspaceId,
298-
instanceId: workspaceInfo.instanceId,
299-
failureCode: SendLocalSSHUserFlowStatusRequest_Code.CODE_TUNNEL_FAILED_FORWARD_SSH_PORT,
300-
daemonVersion: getDaemonVersion(),
301-
connType: SendLocalSSHUserFlowStatusRequest_ConnType.CONN_TYPE_TUNNEL,
302-
});
303-
throw e;
304-
});
305-
return { sock: new SshStream(channel), username: 'gitpod' };
306-
}
307-
308257
async sendErrorReport(gitpodHost: string, userId: string, workspaceId: string | undefined, instanceId: string | undefined, err: Error | any, message: string) {
309258
const request: Partial<SendErrorReportRequest> = {
310259
gitpodHost,

0 commit comments

Comments
 (0)