From c16d632b318e65a4355bbe2e064fbb825a7e8055 Mon Sep 17 00:00:00 2001 From: LuanRT Date: Thu, 28 Jul 2022 05:11:10 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20race=20condition=20causing=20=E2=80=9Cup?= =?UTF-8?q?date-credentials=E2=80=9D=20to=20fire=20multiple=20times?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/OAuth.ts | 45 ++++++++++++++++++----------------------- src/core/Player.ts | 6 +++--- src/core/Session.ts | 2 +- src/utils/HTTPClient.ts | 3 +-- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/core/OAuth.ts b/src/core/OAuth.ts index c3595264..365a74cc 100644 --- a/src/core/OAuth.ts +++ b/src/core/OAuth.ts @@ -43,20 +43,21 @@ class OAuth { */ async init(credentials?: Credentials) { this.#credentials = credentials; + if (this.validateCredentials()) { - this.#session.emit('auth', { - credentials: this.#credentials, - status: 'SUCCESS' - }); + if (!this.has_access_token_expired) + this.#session.emit('auth', { + credentials: this.#credentials, + status: 'SUCCESS' + }); } else if (!(await this.#loadCachedCredentials())) { await this.#getUserCode(); } } async cacheCredentials() { - const str = JSON.stringify(this.#credentials); const encoder = new TextEncoder(); - const data = encoder.encode(str); + const data = encoder.encode(JSON.stringify(this.#credentials)); await this.#session.cache?.set('youtubei_oauth_credentials', data.buffer); } @@ -67,18 +68,21 @@ class OAuth { async #loadCachedCredentials() { const data = await this.#session.cache?.get('youtubei_oauth_credentials'); if (!data) return false; + const decoder = new TextDecoder(); - const str = decoder.decode(data); - const credentials = JSON.parse(str); + const credentials = JSON.parse(decoder.decode(data)); + this.#credentials = { access_token: credentials.access_token, refresh_token: credentials.refresh_token, expires: new Date(credentials.expires) }; + this.#session.emit('auth', { credentials: this.#credentials, status: 'SUCCESS' }); + return true; } @@ -170,21 +174,16 @@ class OAuth { } /** - * Refreshes the access token if necessary. + * Refresh access token if the same has expired. */ - async checkAccessTokenValidity() { - const timestamp = this.#credentials ? new Date(this.#credentials.expires).getTime() : -Infinity; - if (new Date().getTime() > timestamp) { + async refreshIfRequired() { + if (this.has_access_token_expired) { await this.#refreshAccessToken(); } } - /** - * Retrieves a new access token using the refresh token. - */ async #refreshAccessToken() { if (!this.#credentials) return; - this.#identity = await this.#getClientIdentity(); const data = { @@ -201,12 +200,6 @@ class OAuth { } }); - if (response instanceof Error) { - const error = new OAuthError('Could not refresh access token.', { status: 'FAILED' }); - this.#session.emit('update-credentials', error); - throw error; - } - const response_data = await response.json(); const expiration_date = new Date(new Date().getTime() + response_data.expires_in * 1000); @@ -222,9 +215,6 @@ class OAuth { }); } - /** - * Revokes credentials. - */ async revokeCredentials() { if (!this.#credentials) return; await this.removeCache(); @@ -264,6 +254,11 @@ class OAuth { return this.#credentials; } + get has_access_token_expired(): boolean { + const timestamp = this.#credentials ? new Date(this.#credentials.expires).getTime() : -Infinity; + return new Date().getTime() > timestamp; + } + validateCredentials(): this is this & { credentials: Credentials } { return this.#credentials && Reflect.has(this.#credentials, 'access_token') && diff --git a/src/core/Player.ts b/src/core/Player.ts index af55f5d6..a38ff58c 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -115,9 +115,9 @@ export default class Player { // We have the playerID now we can check if we have a cached player if (cache) { - const cachedPlayer = await Player.fromCache(cache, player_id); - if (cachedPlayer) - return cachedPlayer; + const cached_player = await Player.fromCache(cache, player_id); + if (cached_player) + return cached_player; } const player_url = new URL(`/s/player/${player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE); diff --git a/src/core/Session.ts b/src/core/Session.ts index 901f2d11..22b55c54 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -119,7 +119,7 @@ export default class Session extends EventEmitterLike { await this.oauth.init(credentials); if (this.oauth.validateCredentials()) { - await this.oauth.checkAccessTokenValidity(); + await this.oauth.refreshIfRequired(); this.logged_in = true; resolve(); } diff --git a/src/utils/HTTPClient.ts b/src/utils/HTTPClient.ts index e63b4733..17e33126 100644 --- a/src/utils/HTTPClient.ts +++ b/src/utils/HTTPClient.ts @@ -92,8 +92,7 @@ export default class HTTPClient { const oauth = this.#session.oauth; if (oauth.validateCredentials()) { - // Check if the access token is valid to avoid authorization errors. - await oauth.checkAccessTokenValidity(); + await oauth.refreshIfRequired(); request_headers.set('authorization', `Bearer ${oauth.credentials.access_token}`);