@@ -141,55 +141,44 @@ const newConfig = {
141
141
}
142
142
} ;
143
143
144
- /**
145
- * Returns a promise which waits until the secret store `gitpod.authSessions` item changes.
146
- * @returns a promise that resolves with newest added `vscode.AuthenticationSession`, or if no session is found, `null`
147
- */
148
- async function waitForAuthenticationSession ( context : vscode . ExtensionContext ) : Promise < vscode . AuthenticationSession | null > {
149
- console . log ( 'Waiting for the onchange event' ) ;
150
-
144
+ async function waitForAuthenticationSession ( context : vscode . ExtensionContext ) : Promise < vscode . AuthenticationSession > {
151
145
// Wait until a session is added to the context's secret store
152
- const authPromise = promiseFromEvent ( context . secrets . onDidChange , ( changeEvent : vscode . SecretStorageChangeEvent , resolve ) : void => {
146
+ await promiseFromEvent ( context . secrets . onDidChange , ( changeEvent : vscode . SecretStorageChangeEvent , resolve ) : void => {
153
147
if ( changeEvent . key === 'gitpod.authSessions' ) {
154
148
resolve ( changeEvent . key ) ;
155
149
}
156
- } ) ;
157
- const data : any = await authPromise . promise ;
150
+ } ) . promise ;
158
151
159
- console . log ( data ) ;
160
-
161
- console . log ( 'Retrieving the session' ) ;
162
-
163
- const currentSessions = await getValidSessions ( context ) ;
164
- if ( currentSessions . length > 0 ) {
165
- return currentSessions [ currentSessions . length - 1 ] ;
166
- } else {
167
- vscode . window . showErrorMessage ( 'Couldn\'t find any auth sessions' ) ;
168
- return null ;
152
+ const currentSessions = await readSessions ( context ) ;
153
+ if ( ! currentSessions . length ) {
154
+ throw new Error ( 'Not found' ) ;
169
155
}
156
+ return currentSessions [ currentSessions . length - 1 ] ;
170
157
}
171
158
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 ) ;
159
+ export async function readSessions ( context : vscode . ExtensionContext ) : Promise < vscode . AuthenticationSession [ ] > {
160
+ let sessions = await getAuthSessions ( context ) ;
161
+ sessions = sessions . filter ( session => validateSession ( session ) ) ;
162
+ await storeAuthSessions ( sessions , context ) ;
163
+ return sessions ;
164
+ }
180
165
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 ] ;
166
+ export async function validateSession ( session : vscode . AuthenticationSession ) : Promise < boolean > {
167
+ try {
168
+ const hash = crypto . createHash ( 'sha256' ) . update ( session . accessToken , 'utf8' ) . digest ( 'hex' ) ;
169
+ const tokenScopes = new Set ( await withServerApi ( session . accessToken , service => service . server . getGitpodTokenScopes ( hash ) ) ) ;
170
+ for ( const scope of gitpodScopes ) {
171
+ if ( ! tokenScopes . has ( scope ) ) {
172
+ return false ;
173
+ }
185
174
}
175
+ return true ;
176
+ } catch ( e ) {
177
+ if ( e . message !== unauthorizedErr ) {
178
+ console . error ( 'gitpod: invalid session:' , e ) ;
179
+ }
180
+ return false ;
186
181
}
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
182
}
194
183
195
184
/**
@@ -232,16 +221,23 @@ export async function setSettingsSync(enabled?: boolean): Promise<void> {
232
221
}
233
222
}
234
223
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 ( ) => {
224
+ class GitpodServerApi extends vscode . Disposable {
225
+
226
+ readonly service : GitpodConnection ;
227
+ private readonly socket : ReconnectingWebSocket ;
228
+ private readonly onWillCloseEmitter = new vscode . EventEmitter < number | undefined > ( ) ;
229
+ readonly onWillClose = this . onWillCloseEmitter . event ;
230
+
231
+ constructor ( accessToken : string ) {
232
+ super ( ( ) => {
233
+ this . close ( ) ;
234
+ this . onWillCloseEmitter . dispose ( ) ;
235
+ } ) ;
236
+ const factory = new JsonRpcProxyFactory < GitpodServer > ( ) ;
237
+ this . service = new GitpodServiceImpl < GitpodClient , GitpodServer > ( factory . createProxy ( ) ) ;
238
+
239
+ let retry = 1 ;
240
+ const maxRetries = 3 ;
245
241
class GitpodServerWebSocket extends WebSocket {
246
242
constructor ( address : string , protocols ?: string | string [ ] ) {
247
243
super ( address , protocols , {
@@ -250,35 +246,63 @@ async function createApiWebSocket(accessToken: string): Promise<{ gitpodService:
250
246
'Authorization' : `Bearer ${ accessToken } `
251
247
}
252
248
} ) ;
249
+ this . on ( 'unexpected-response' , ( _ , resp ) => {
250
+ this . terminate ( ) ;
251
+
252
+ // if mal-formed handshake request (unauthorized, forbidden) or client actions (redirect) are required then fail immediately
253
+ // otherwise try several times and fail, maybe temporarily unavailable, like server restart
254
+ if ( retry ++ >= maxRetries || ( typeof resp . statusCode === 'number' && 300 <= resp . statusCode && resp . statusCode < 500 ) ) {
255
+ socket . close ( resp . statusCode ) ;
256
+ }
257
+ } ) ;
253
258
}
254
259
}
255
- const webSocketMaxRetries = 3 ;
256
- const webSocket = new ReconnectingWebSocket ( ` ${ getBaseURL ( ) . replace ( 'https' , 'wss' ) } /api/v1` , undefined , {
260
+ const socket = new ReconnectingWebSocket ( ` ${ getBaseURL ( ) . replace ( 'https' , 'wss' ) } /api/v1` , undefined , {
261
+ maxReconnectionDelay : 10000 ,
257
262
minReconnectionDelay : 1000 ,
263
+ reconnectionDelayGrowFactor : 1.5 ,
258
264
connectionTimeout : 10000 ,
259
- maxRetries : webSocketMaxRetries - 1 ,
265
+ maxRetries : Infinity ,
260
266
debug : false ,
261
267
startClosed : false ,
262
268
WebSocket : GitpodServerWebSocket
263
269
} ) ;
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
- }
270
+ socket . onerror = e => {
271
+ console . error ( 'gitpod: server api: failed to open socket:' , e ) ;
271
272
} ;
272
273
273
274
doListen ( {
274
- webSocket,
275
+ webSocket : socket ,
275
276
logger : new ConsoleLogger ( ) ,
276
277
onConnection : connection => factory . listen ( connection ) ,
277
278
} ) ;
278
- return webSocket ;
279
- } ) ( ) ;
279
+ this . socket = socket ;
280
+ }
281
+
282
+ private close ( statusCode ?: number ) : void {
283
+ this . onWillCloseEmitter . fire ( statusCode ) ;
284
+ try {
285
+ this . socket . close ( ) ;
286
+ } catch ( e ) {
287
+ console . error ( 'gitpod: server api: failed to close socket:' , e ) ;
288
+ }
289
+ }
280
290
281
- return { gitpodService, pendignWebSocket } ;
291
+ }
292
+
293
+ const unauthorizedErr = 'unauthorized' ;
294
+ function withServerApi < T > ( accessToken : string , cb : ( service : GitpodConnection ) => Promise < T > ) : Promise < T > {
295
+ const api = new GitpodServerApi ( accessToken ) ;
296
+ return Promise . race ( [
297
+ cb ( api . service ) ,
298
+ new Promise < T > ( ( _ , reject ) => api . onWillClose ( statusCode => {
299
+ if ( statusCode === 401 ) {
300
+ reject ( new Error ( unauthorizedErr ) ) ;
301
+ } else {
302
+ reject ( new Error ( 'closed' ) ) ;
303
+ }
304
+ } ) )
305
+ ] ) . finally ( ( ) => api . dispose ( ) ) ;
282
306
}
283
307
284
308
interface ExchangeTokenResponse {
@@ -315,13 +339,10 @@ export async function resolveAuthenticationSession(scopes: readonly string[], co
315
339
}
316
340
317
341
const exchangeTokenData : ExchangeTokenResponse = await exchangeTokenResponse . json ( ) ;
318
- console . log ( exchangeTokenData ) ;
319
342
const jwtToken = exchangeTokenData . access_token ;
320
343
const accessToken = JSON . parse ( Buffer . from ( jwtToken . split ( '.' ) [ 1 ] , 'base64' ) . toString ( ) ) [ 'jti' ] ;
321
344
322
- const { gitpodService, pendignWebSocket } = await createApiWebSocket ( accessToken ) ;
323
- const user = await gitpodService . server . getLoggedInUser ( ) ;
324
- ( await pendignWebSocket ) . close ( ) ;
345
+ const user = await withServerApi ( accessToken , service => service . server . getLoggedInUser ( ) ) ;
325
346
return {
326
347
id : 'gitpod.user' ,
327
348
account : {
@@ -337,22 +358,6 @@ export async function resolveAuthenticationSession(scopes: readonly string[], co
337
358
}
338
359
}
339
360
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
361
/**
357
362
* Creates a URL to be opened for the whole OAuth2 flow to kick-off
358
363
* @returns a `URL` string containing the whole auth URL
@@ -361,8 +366,6 @@ async function createOauth2URL(context: vscode.ExtensionContext, options: { auth
361
366
const { authorizationURI, clientID, redirectURI, scopes } = options ;
362
367
const { codeChallenge, codeVerifier } : { codeChallenge : string , codeVerifier : string } = create ( ) ;
363
368
364
- console . log ( `Verifier: ${ codeVerifier } ` ) ;
365
-
366
369
let query = '' ;
367
370
function set ( field : string , value : string ) : void {
368
371
if ( query ) {
@@ -403,36 +406,31 @@ async function askToEnable(context: vscode.ExtensionContext): Promise<void> {
403
406
* @returns a promise which resolves to an `AuthenticationSession`
404
407
*/
405
408
export async function createSession ( scopes : readonly string [ ] , context : vscode . ExtensionContext ) : Promise < vscode . AuthenticationSession > {
406
- const callbackUri = await vscode . env . asExternalUri ( vscode . Uri . parse ( `${ vscode . env . uriScheme } ://gitpod.gitpod-desktop/complete-gitpod-auth` ) ) ;
407
409
if ( scopes . some ( scope => ! gitpodScopes . has ( scope ) ) ) {
408
- throw new Error ( 'invalid scopes ' ) ;
410
+ throw new Error ( 'Auth failed ' ) ;
409
411
}
410
412
413
+ const callbackUri = await vscode . env . asExternalUri ( vscode . Uri . parse ( `${ vscode . env . uriScheme } ://gitpod.gitpod-desktop/complete-gitpod-auth` ) ) ;
414
+
411
415
const gitpodAuth = await createOauth2URL ( context , {
412
416
clientID : `${ vscode . env . uriScheme } -gitpod` ,
413
417
authorizationURI : `${ getBaseURL ( ) } /api/oauth/authorize` ,
414
418
redirectURI : callbackUri ,
415
419
scopes : [ ...gitpodScopes ] ,
416
420
} ) ;
417
421
418
- const timeoutPromise = new Promise ( ( _ : ( value : vscode . AuthenticationSession ) => void , reject ) : void => {
419
- const wait = setTimeout ( ( ) => {
420
- clearTimeout ( wait ) ;
421
- vscode . window . showErrorMessage ( 'Login timed out, please try to sign in again.' ) ;
422
- reject ( 'Login timed out.' ) ;
423
- } , 1000 * 60 * 5 ) ; // 5 minutes
424
- } ) ;
425
- console . log ( gitpodAuth ) ;
426
422
const opened = await vscode . env . openExternal ( gitpodAuth as any ) ;
427
423
if ( ! opened ) {
428
424
const selected = await vscode . window . showErrorMessage ( `Couldn't open ${ gitpodAuth } automatically, please copy and paste it to your browser manually.` , 'Copy' , 'Cancel' ) ;
429
425
if ( selected === 'Copy' ) {
430
426
vscode . env . clipboard . writeText ( gitpodAuth ) ;
431
- console . log ( 'Copied auth URL' ) ;
432
427
}
433
428
}
434
429
435
- return Promise . race ( [ timeoutPromise , ( await waitForAuthenticationSession ( context ) ) ! ] ) ;
430
+ return Promise . race ( [
431
+ waitForAuthenticationSession ( context ) ,
432
+ new Promise < vscode . AuthenticationSession > ( ( _ , reject ) => setTimeout ( ( ) => reject ( new Error ( 'Login timed out.' ) ) , 1000 * 60 * 5 ) )
433
+ ] ) ;
436
434
}
437
435
438
436
/**
0 commit comments