@@ -146,21 +146,15 @@ const newConfig = {
146
146
* @returns a promise that resolves with newest added `vscode.AuthenticationSession`, or if no session is found, `null`
147
147
*/
148
148
async function waitForAuthenticationSession ( context : vscode . ExtensionContext ) : Promise < vscode . AuthenticationSession | null > {
149
- console . log ( 'Waiting for the onchange event' ) ;
150
-
151
149
// Wait until a session is added to the context's secret store
152
150
const authPromise = promiseFromEvent ( context . secrets . onDidChange , ( changeEvent : vscode . SecretStorageChangeEvent , resolve ) : void => {
153
151
if ( changeEvent . key === 'gitpod.authSessions' ) {
154
152
resolve ( changeEvent . key ) ;
155
153
}
156
154
} ) ;
157
- const data : any = await authPromise . promise ;
158
-
159
- console . log ( data ) ;
160
-
161
- console . log ( 'Retrieving the session' ) ;
155
+ await authPromise . promise ;
162
156
163
- const currentSessions = await getValidSessions ( context ) ;
157
+ const currentSessions = await readSessions ( context ) ;
164
158
if ( currentSessions . length > 0 ) {
165
159
return currentSessions [ currentSessions . length - 1 ] ;
166
160
} else {
@@ -169,27 +163,29 @@ async function waitForAuthenticationSession(context: vscode.ExtensionContext): P
169
163
}
170
164
}
171
165
172
- /**
173
- * Checks all stored auth sessions and returns all valid ones
174
- * @param context the VS Code extension context from which to get the sessions from
175
- * @param scopes optionally, you can specify scopes to check against
176
- * @returns a list of sessions that are valid
177
- */
178
- export async function getValidSessions ( context : vscode . ExtensionContext , scopes ?: readonly string [ ] ) : Promise < vscode . AuthenticationSession [ ] > {
179
- const sessions = await getAuthSessions ( context ) ;
166
+ export async function readSessions ( context : vscode . ExtensionContext ) : Promise < vscode . AuthenticationSession [ ] > {
167
+ let sessions = await getAuthSessions ( context ) ;
168
+ sessions = sessions . filter ( session => validateSession ( session ) ) ;
169
+ await storeAuthSessions ( sessions , context ) ;
170
+ return sessions ;
171
+ }
180
172
181
- for ( const [ index , session ] of sessions . entries ( ) ) {
182
- const availableScopes = await checkScopes ( session . accessToken ) ;
183
- if ( ! ( scopes || [ ...gitpodScopes ] ) . every ( ( scope ) => availableScopes . includes ( scope ) ) ) {
184
- delete sessions [ index ] ;
173
+ export async function validateSession ( session : vscode . AuthenticationSession ) : Promise < boolean > {
174
+ try {
175
+ const hash = crypto . createHash ( 'sha256' ) . update ( session . accessToken , 'utf8' ) . digest ( 'hex' ) ;
176
+ const tokenScopes = new Set ( await withServerApi ( session . accessToken , service => service . server . getGitpodTokenScopes ( hash ) ) ) ;
177
+ for ( const scope of gitpodScopes ) {
178
+ if ( ! tokenScopes . has ( scope ) ) {
179
+ return false ;
180
+ }
185
181
}
182
+ return true ;
183
+ } catch ( e ) {
184
+ if ( e . message !== unauthorizedErr ) {
185
+ console . error ( 'gitpod: invalid session:' , e ) ;
186
+ }
187
+ return false ;
186
188
}
187
-
188
- await storeAuthSessions ( sessions , context ) ;
189
- if ( sessions . length === 0 && ( await getAuthSessions ( context ) ) . length !== 0 ) {
190
- vscode . window . showErrorMessage ( 'Your login session with Gitpod has expired. You need to sign in again.' ) ;
191
- }
192
- return sessions ;
193
189
}
194
190
195
191
/**
@@ -232,16 +228,23 @@ export async function setSettingsSync(enabled?: boolean): Promise<void> {
232
228
}
233
229
}
234
230
235
- /**
236
- * Creates a WebSocket connection to Gitpod's API
237
- * @param accessToken an access token to create the WS connection with
238
- * @returns a tuple of `gitpodService` and `pendignWebSocket`
239
- */
240
- async function createApiWebSocket ( accessToken : string ) : Promise < { gitpodService : GitpodConnection ; pendignWebSocket : Promise < ReconnectingWebSocket > ; } > {
241
- const factory = new JsonRpcProxyFactory < GitpodServer > ( ) ;
242
- const gitpodService : GitpodConnection = new GitpodServiceImpl < GitpodClient , GitpodServer > ( factory . createProxy ( ) ) as any ;
243
- console . log ( `Using token: ${ accessToken } ` ) ;
244
- const pendignWebSocket = ( async ( ) => {
231
+ class GitpodServerApi extends vscode . Disposable {
232
+
233
+ readonly service : GitpodConnection ;
234
+ private readonly socket : ReconnectingWebSocket ;
235
+ private readonly onWillCloseEmitter = new vscode . EventEmitter < number | undefined > ( ) ;
236
+ readonly onWillClose = this . onWillCloseEmitter . event ;
237
+
238
+ constructor ( accessToken : string ) {
239
+ super ( ( ) => {
240
+ this . close ( ) ;
241
+ this . onWillCloseEmitter . dispose ( ) ;
242
+ } ) ;
243
+ const factory = new JsonRpcProxyFactory < GitpodServer > ( ) ;
244
+ this . service = new GitpodServiceImpl < GitpodClient , GitpodServer > ( factory . createProxy ( ) ) ;
245
+
246
+ let retry = 1 ;
247
+ const maxRetries = 3 ;
245
248
class GitpodServerWebSocket extends WebSocket {
246
249
constructor ( address : string , protocols ?: string | string [ ] ) {
247
250
super ( address , protocols , {
@@ -250,35 +253,63 @@ async function createApiWebSocket(accessToken: string): Promise<{ gitpodService:
250
253
'Authorization' : `Bearer ${ accessToken } `
251
254
}
252
255
} ) ;
256
+ this . on ( 'unexpected-response' , ( _ , resp ) => {
257
+ this . terminate ( ) ;
258
+
259
+ // if mal-formed handshake request (unauthorized, forbidden) or client actions (redirect) are required then fail immediately
260
+ // otherwise try several times and fail, maybe temporarily unavailable, like server restart
261
+ if ( retry ++ >= maxRetries || ( typeof resp . statusCode === 'number' && 300 <= resp . statusCode && resp . statusCode < 500 ) ) {
262
+ socket . close ( resp . statusCode ) ;
263
+ }
264
+ } ) ;
253
265
}
254
266
}
255
- const webSocketMaxRetries = 3 ;
256
- const webSocket = new ReconnectingWebSocket ( ` ${ getBaseURL ( ) . replace ( 'https' , 'wss' ) } /api/v1` , undefined , {
267
+ const socket = new ReconnectingWebSocket ( ` ${ getBaseURL ( ) . replace ( 'https' , 'wss' ) } /api/v1` , undefined , {
268
+ maxReconnectionDelay : 10000 ,
257
269
minReconnectionDelay : 1000 ,
270
+ reconnectionDelayGrowFactor : 1.5 ,
258
271
connectionTimeout : 10000 ,
259
- maxRetries : webSocketMaxRetries - 1 ,
272
+ maxRetries : Infinity ,
260
273
debug : false ,
261
274
startClosed : false ,
262
275
WebSocket : GitpodServerWebSocket
263
276
} ) ;
264
-
265
- let retry = 1 ;
266
- webSocket . onerror = ( err ) => {
267
- vscode . window . showErrorMessage ( `WebSocket error: ${ err . message } (#${ retry } /${ webSocketMaxRetries } )` ) ;
268
- if ( retry ++ === webSocketMaxRetries ) {
269
- throw new Error ( 'Maximum websocket connection retries exceeded' ) ;
270
- }
277
+ socket . onerror = e => {
278
+ console . error ( 'gitpod: server api: failed to open socket:' , e ) ;
271
279
} ;
272
280
273
281
doListen ( {
274
- webSocket,
282
+ webSocket : socket ,
275
283
logger : new ConsoleLogger ( ) ,
276
284
onConnection : connection => factory . listen ( connection ) ,
277
285
} ) ;
278
- return webSocket ;
279
- } ) ( ) ;
286
+ this . socket = socket ;
287
+ }
288
+
289
+ private close ( statusCode ?: number ) : void {
290
+ this . onWillCloseEmitter . fire ( statusCode ) ;
291
+ try {
292
+ this . socket . close ( ) ;
293
+ } catch ( e ) {
294
+ console . error ( 'gitpod: server api: failed to close socket:' , e ) ;
295
+ }
296
+ }
280
297
281
- return { gitpodService, pendignWebSocket } ;
298
+ }
299
+
300
+ const unauthorizedErr = 'unauthorized' ;
301
+ function withServerApi < T > ( accessToken : string , cb : ( service : GitpodConnection ) => Promise < T > ) : Promise < T > {
302
+ const api = new GitpodServerApi ( accessToken ) ;
303
+ return Promise . race ( [
304
+ cb ( api . service ) ,
305
+ new Promise < T > ( ( _ , reject ) => api . onWillClose ( statusCode => {
306
+ if ( statusCode === 401 ) {
307
+ reject ( new Error ( unauthorizedErr ) ) ;
308
+ } else {
309
+ reject ( new Error ( 'closed' ) ) ;
310
+ }
311
+ } ) )
312
+ ] ) . finally ( ( ) => api . dispose ( ) ) ;
282
313
}
283
314
284
315
interface ExchangeTokenResponse {
@@ -315,13 +346,10 @@ export async function resolveAuthenticationSession(scopes: readonly string[], co
315
346
}
316
347
317
348
const exchangeTokenData : ExchangeTokenResponse = await exchangeTokenResponse . json ( ) ;
318
- console . log ( exchangeTokenData ) ;
319
349
const jwtToken = exchangeTokenData . access_token ;
320
350
const accessToken = JSON . parse ( Buffer . from ( jwtToken . split ( '.' ) [ 1 ] , 'base64' ) . toString ( ) ) [ 'jti' ] ;
321
351
322
- const { gitpodService, pendignWebSocket } = await createApiWebSocket ( accessToken ) ;
323
- const user = await gitpodService . server . getLoggedInUser ( ) ;
324
- ( await pendignWebSocket ) . close ( ) ;
352
+ const user = await withServerApi ( accessToken , service => service . server . getLoggedInUser ( ) ) ;
325
353
return {
326
354
id : 'gitpod.user' ,
327
355
account : {
@@ -337,22 +365,6 @@ export async function resolveAuthenticationSession(scopes: readonly string[], co
337
365
}
338
366
}
339
367
340
- /**
341
- * @returns all of the scopes accessible for `accessToken`
342
- */
343
- export async function checkScopes ( accessToken : string ) : Promise < string [ ] > {
344
- try {
345
- const { gitpodService, pendignWebSocket } = await createApiWebSocket ( accessToken ) ;
346
- const hash = crypto . createHash ( 'sha256' ) . update ( accessToken , 'utf8' ) . digest ( 'hex' ) ;
347
- const scopes = await gitpodService . server . getGitpodTokenScopes ( hash ) ;
348
- ( await pendignWebSocket ) . close ( ) ;
349
- return scopes ;
350
- } catch ( e ) {
351
- vscode . window . showErrorMessage ( `Couldn't connect: ${ e } ` ) ;
352
- return [ ] ;
353
- }
354
- }
355
-
356
368
/**
357
369
* Creates a URL to be opened for the whole OAuth2 flow to kick-off
358
370
* @returns a `URL` string containing the whole auth URL
@@ -361,8 +373,6 @@ async function createOauth2URL(context: vscode.ExtensionContext, options: { auth
361
373
const { authorizationURI, clientID, redirectURI, scopes } = options ;
362
374
const { codeChallenge, codeVerifier } : { codeChallenge : string , codeVerifier : string } = create ( ) ;
363
375
364
- console . log ( `Verifier: ${ codeVerifier } ` ) ;
365
-
366
376
let query = '' ;
367
377
function set ( field : string , value : string ) : void {
368
378
if ( query ) {
@@ -422,13 +432,11 @@ export async function createSession(scopes: readonly string[], context: vscode.E
422
432
reject ( 'Login timed out.' ) ;
423
433
} , 1000 * 60 * 5 ) ; // 5 minutes
424
434
} ) ;
425
- console . log ( gitpodAuth ) ;
426
435
const opened = await vscode . env . openExternal ( gitpodAuth as any ) ;
427
436
if ( ! opened ) {
428
437
const selected = await vscode . window . showErrorMessage ( `Couldn't open ${ gitpodAuth } automatically, please copy and paste it to your browser manually.` , 'Copy' , 'Cancel' ) ;
429
438
if ( selected === 'Copy' ) {
430
439
vscode . env . clipboard . writeText ( gitpodAuth ) ;
431
- console . log ( 'Copied auth URL' ) ;
432
440
}
433
441
}
434
442
0 commit comments