|
4 | 4 | *--------------------------------------------------------------------------------------------*/
|
5 | 5 |
|
6 | 6 | 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'; |
8 | 8 | import { importKey, importKeyBytes } from '@microsoft/dev-tunnels-ssh-keys';
|
9 | 9 | import { ExtensionServiceDefinition, GetWorkspaceAuthInfoResponse, SendErrorReportRequest, SendLocalSSHUserFlowStatusRequest_Code, SendLocalSSHUserFlowStatusRequest_ConnType, SendLocalSSHUserFlowStatusRequest_Status } from '../proto/typescript/ipc/v1/ipc';
|
10 | 10 | import { Client, ClientError, Status, createChannel, createClient } from 'nice-grpc';
|
11 | 11 | import { retryWithStop } from '../common/async';
|
12 |
| -import { TunnelPortRequest } from '@gitpod/supervisor-api-grpc/lib/port_pb'; |
13 | 12 | import { WebSocket } from 'ws';
|
14 | 13 | import * as stream from 'stream';
|
15 | 14 | import { ILogService } from '../services/logService';
|
@@ -58,28 +57,6 @@ class AuthenticationError extends Error {
|
58 | 57 | }
|
59 | 58 | }
|
60 | 59 |
|
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 |
| - |
83 | 60 | // TODO(local-ssh): Remove me after direct ssh works with @microsft/dev-tunnels-ssh
|
84 | 61 | const FORCE_TUNNEL = true;
|
85 | 62 |
|
@@ -162,14 +139,17 @@ class WebSocketSSHProxy {
|
162 | 139 | if (FORCE_TUNNEL) {
|
163 | 140 | return this.getTunnelSSHConfig(workspaceInfo);
|
164 | 141 | }
|
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 | + } |
170 | 150 | }
|
171 | 151 |
|
172 |
| - private async tryDirectSSH(workspaceInfo: GetWorkspaceAuthInfoResponse): Promise<SshClientSession | undefined> { |
| 152 | + private async tryDirectSSH(workspaceInfo: GetWorkspaceAuthInfoResponse): Promise<SshClientSession> { |
173 | 153 | try {
|
174 | 154 | const connConfig = {
|
175 | 155 | host: `${workspaceInfo.workspaceId}.ssh.${workspaceInfo.workspaceHost}`,
|
@@ -200,18 +180,44 @@ class WebSocketSSHProxy {
|
200 | 180 | daemonVersion: getDaemonVersion(),
|
201 | 181 | connType: SendLocalSSHUserFlowStatusRequest_ConnType.CONN_TYPE_SSH,
|
202 | 182 | });
|
| 183 | + throw e; |
203 | 184 | }
|
204 |
| - return; |
205 | 185 | }
|
206 | 186 |
|
207 | 187 | 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 | + |
208 | 213 | try {
|
209 |
| - const connConfig = await this.establishTunnel(workspaceInfo); |
210 | 214 | const config = new SshSessionConfiguration();
|
211 | 215 | const session = new SshClientSession(config);
|
212 | 216 | 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)] }); |
215 | 221 | if (!ok) {
|
216 | 222 | throw new AuthenticationError();
|
217 | 223 | }
|
@@ -248,63 +254,6 @@ class WebSocketSSHProxy {
|
248 | 254 | }, 200, 50);
|
249 | 255 | }
|
250 | 256 |
|
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 |
| - |
308 | 257 | async sendErrorReport(gitpodHost: string, userId: string, workspaceId: string | undefined, instanceId: string | undefined, err: Error | any, message: string) {
|
309 | 258 | const request: Partial<SendErrorReportRequest> = {
|
310 | 259 | gitpodHost,
|
|
0 commit comments