3
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
4
*--------------------------------------------------------------------------------------------*/
5
5
6
+ import * as os from 'os' ;
7
+ import * as path from 'path' ;
8
+
6
9
interface ClientOptions {
7
10
host : string ;
11
+ gitpodHost : string ;
8
12
extIpcPort : number ;
9
13
machineID : string ;
14
+ debug : boolean ;
10
15
}
11
16
12
17
function getClientOptions ( ) : ClientOptions {
13
18
const args = process . argv . slice ( 2 ) ;
14
19
// %h is in the form of <ws_id>.vss.<gitpod_host>'
15
20
// add `https://` prefix since our gitpodHost is actually a url not host
16
- const host = 'https://' + args [ 0 ] . split ( '.' ) . splice ( 2 ) . join ( '.' ) ;
21
+ const host = args [ 0 ] ;
22
+ const gitpodHost = 'https://' + args [ 0 ] . split ( '.' ) . splice ( 2 ) . join ( '.' ) ;
17
23
return {
18
24
host,
25
+ gitpodHost,
19
26
extIpcPort : Number . parseInt ( args [ 1 ] , 10 ) ,
20
27
machineID : args [ 2 ] ?? '' ,
28
+ debug : args [ 3 ] === 'debug' ,
21
29
} ;
22
30
}
23
31
@@ -26,26 +34,22 @@ if (!options) {
26
34
process . exit ( 1 ) ;
27
35
}
28
36
29
- import { NopeLogger } from './logger' ;
30
- const logService = new NopeLogger ( ) ;
31
-
32
- // DO NOT PUSH CHANGES BELOW TO PRODUCTION
33
- // import { DebugLogger } from './logger';
34
- // const logService = new DebugLogger();
37
+ import { NopeLogger , DebugLogger } from './logger' ;
38
+ const logService = options . debug ? new DebugLogger ( path . join ( os . tmpdir ( ) , `lssh-${ options . host } .log` ) ) : new NopeLogger ( ) ;
35
39
36
40
import { TelemetryService } from './telemetryService' ;
37
41
const telemetryService = new TelemetryService (
38
42
process . env . SEGMENT_KEY ! ,
39
43
options . machineID ,
40
44
process . env . EXT_NAME ! ,
41
45
process . env . EXT_VERSION ! ,
42
- options . host ,
46
+ options . gitpodHost ,
43
47
logService
44
48
) ;
45
49
46
50
const flow : SSHUserFlowTelemetry = {
47
51
flow : 'local_ssh' ,
48
- gitpodHost : options . host ,
52
+ gitpodHost : options . gitpodHost ,
49
53
workspaceId : '' ,
50
54
processId : process . pid ,
51
55
} ;
@@ -71,7 +75,7 @@ const exitProcess = async (forceExit: boolean, signal?: NodeJS.Signals) => {
71
75
} ;
72
76
73
77
import { SshClient } from '@microsoft/dev-tunnels-ssh-tcp' ;
74
- import { NodeStream , ObjectDisposedError , SshChannelError , SshChannelOpenFailureReason , SshClientCredentials , SshClientSession , SshConnectionError , SshDisconnectReason , SshReconnectError , SshReconnectFailureReason , SshServerSession , SshSessionConfiguration , Stream , WebSocketStream } from '@microsoft/dev-tunnels-ssh' ;
78
+ import { NodeStream , ObjectDisposedError , SshChannelError , SshChannelOpenFailureReason , SshClientCredentials , SshClientSession , SshConnectionError , SshDisconnectReason , SshReconnectError , SshReconnectFailureReason , SshServerSession , SshSessionConfiguration , Stream , TraceLevel , WebSocketStream } from '@microsoft/dev-tunnels-ssh' ;
75
79
import { importKey , importKeyBytes } from '@microsoft/dev-tunnels-ssh-keys' ;
76
80
import { ExtensionServiceDefinition , GetWorkspaceAuthInfoResponse } from '../proto/typescript/ipc/v1/ipc' ;
77
81
import { Client , ClientError , Status , createChannel , createClient } from 'nice-grpc' ;
@@ -165,6 +169,7 @@ class WebSocketSSHProxy {
165
169
// Seems there's a bug in the ssh library that could hang forever when the stream gets closed
166
170
// so the below `await pipePromise` will never return and the node process will never exit.
167
171
// So let's just force kill here
172
+ pipeSession ?. close ( SshDisconnectReason . byApplication ) ;
168
173
setTimeout ( ( ) => {
169
174
exitProcess ( true ) ;
170
175
} , 50 ) ;
@@ -180,17 +185,22 @@ class WebSocketSSHProxy {
180
185
const keys = await importKeyBytes ( getHostKey ( ) ) ;
181
186
const config = new SshSessionConfiguration ( ) ;
182
187
config . maxClientAuthenticationAttempts = 1 ;
183
- const session = new SshServerSession ( config ) ;
184
- session . credentials . publicKeys . push ( keys ) ;
188
+ const localSession = new SshServerSession ( config ) ;
189
+ localSession . credentials . publicKeys . push ( keys ) ;
190
+ localSession . trace = ( _ : TraceLevel , eventId : number , msg : string , err ?: Error ) => {
191
+ this . logService . trace ( `sshsession [local] eventId[${ eventId } ]` , msg , err ) ;
192
+ } ;
185
193
194
+ let pipeSession : SshClientSession | undefined ;
186
195
let pipePromise : Promise < void > | undefined ;
187
- session . onAuthenticating ( async ( e ) => {
196
+ localSession . onAuthenticating ( async ( e ) => {
188
197
this . flow . workspaceId = e . username ?? '' ;
189
198
this . sendUserStatusFlow ( 'connecting' ) ;
190
199
e . authenticationPromise = this . authenticateClient ( e . username ?? '' )
191
- . then ( async pipeSession => {
200
+ . then ( async session => {
192
201
this . sendUserStatusFlow ( 'connected' ) ;
193
- pipePromise = session . pipe ( pipeSession ) ;
202
+ pipeSession = session ;
203
+ pipePromise = localSession . pipe ( pipeSession ) ;
194
204
return { } ;
195
205
} ) . catch ( async err => {
196
206
this . logService . error ( 'failed to authenticate proxy with username: ' + e . username ?? '' , err ) ;
@@ -209,21 +219,21 @@ class WebSocketSSHProxy {
209
219
// Await a few seconds to delay showing ssh extension error modal dialog
210
220
await timeout ( 5000 ) ;
211
221
212
- await session . close ( SshDisconnectReason . byApplication , err . toString ( ) , err instanceof Error ? err : undefined ) ;
222
+ await localSession . close ( SshDisconnectReason . byApplication , err . toString ( ) , err instanceof Error ? err : undefined ) ;
213
223
return null ;
214
224
} ) ;
215
225
} ) ;
216
226
try {
217
- await session . connect ( new NodeStream ( sshStream ) ) ;
227
+ await localSession . connect ( new NodeStream ( sshStream ) ) ;
218
228
await pipePromise ;
219
229
} catch ( e ) {
220
- if ( session . isClosed ) {
230
+ if ( localSession . isClosed ) {
221
231
return ;
222
232
}
223
233
e = fixSSHErrorName ( e ) ;
224
234
this . logService . error ( e , 'failed to connect to client' ) ;
225
235
this . sendErrorReport ( this . flow , e , 'failed to connect to client' ) ;
226
- await session . close ( SshDisconnectReason . byApplication , e . toString ( ) , e instanceof Error ? e : undefined ) ;
236
+ await localSession . close ( SshDisconnectReason . byApplication , e . toString ( ) , e instanceof Error ? e : undefined ) ;
227
237
}
228
238
}
229
239
@@ -325,6 +335,9 @@ class WebSocketSSHProxy {
325
335
const config = new SshSessionConfiguration ( ) ;
326
336
const session = new SshClientSession ( config ) ;
327
337
session . onAuthenticating ( ( e ) => e . authenticationPromise = Promise . resolve ( { } ) ) ;
338
+ session . trace = ( _ : TraceLevel , eventId : number , msg : string , err ?: Error ) => {
339
+ this . logService . trace ( `sshsession [websocket] eventId[${ eventId } ]` , msg , err ) ;
340
+ } ;
328
341
329
342
await session . connect ( stream ) ;
330
343
@@ -340,7 +353,7 @@ class WebSocketSSHProxy {
340
353
341
354
async retryGetWorkspaceInfo ( username : string ) {
342
355
return retry ( async ( ) => {
343
- return this . extensionIpc . getWorkspaceAuthInfo ( { workspaceId : username , gitpodHost : this . options . host } ) . catch ( e => {
356
+ return this . extensionIpc . getWorkspaceAuthInfo ( { workspaceId : username , gitpodHost : this . options . gitpodHost } ) . catch ( e => {
344
357
let failureCode = 'FailedToGetAuthInfo' ;
345
358
if ( e instanceof ClientError ) {
346
359
if ( e . code === Status . FAILED_PRECONDITION && e . message . includes ( 'gitpod host mismatch' ) ) {
@@ -377,7 +390,7 @@ const metricsReporter = new LocalSSHMetricsReporter(logService);
377
390
const proxy = new WebSocketSSHProxy ( options , telemetryService , metricsReporter , logService , flow ) ;
378
391
proxy . start ( ) . catch ( e => {
379
392
const err = new WrapError ( 'Uncaught exception on start method' , e ) ;
380
- telemetryService . sendTelemetryException ( err , { gitpodHost : options . host } ) ;
393
+ telemetryService . sendTelemetryException ( err , { gitpodHost : options . gitpodHost } ) ;
381
394
} ) ;
382
395
383
396
function fixSSHErrorName ( err : any ) {
0 commit comments