From 3a41b74af58f36a64150baca7bd27fa65223f0a7 Mon Sep 17 00:00:00 2001 From: Jim Kroon Date: Fri, 12 Apr 2024 01:05:55 +0200 Subject: [PATCH] Fix xCloud error on startup when using the application in a non xCloud supported country #1193 --- main/application.ts | 72 ++++++++++---------------- main/authentication.ts | 95 ++++++++++++++++++++++++++-------- main/ipc/app.ts | 4 ++ main/ipc/base.ts | 2 +- main/ipc/xcloud.ts | 32 +++++++----- main/webui.ts | 2 +- renderer/components/header.tsx | 8 ++- 7 files changed, 130 insertions(+), 85 deletions(-) diff --git a/main/application.ts b/main/application.ts index 7d04c602..f6d0bf81 100644 --- a/main/application.ts +++ b/main/application.ts @@ -153,67 +153,47 @@ export default class Application { _xCloudApi:xCloudApi _xboxWorker:xboxWorker - authenticationCompleted(){ + authenticationCompleted(streamingTokens, webToken){ this.log('electron', __filename+'[authenticationCompleted()] authenticationCompleted called') // const tokens = this._authentication._tokens + this._xHomeApi = new xCloudApi(this, streamingTokens.xHomeToken.getDefaultRegion().baseUri.substring(8), streamingTokens.xHomeToken.data.gsToken, 'home') - this._authentication._xal.getWebToken(this._authentication._tokenStore).then((webToken) => { - this.log('electron', __filename+'[authenticationCompleted()] getWebToken resolved:', webToken.data.Token, webToken.data.DisplayClaims.xui[0].uhs) + if(streamingTokens.xCloudToken !== null){ + this._xCloudApi = new xCloudApi(this, streamingTokens.xCloudToken.getDefaultRegion().baseUri.substring(8), streamingTokens.xCloudToken.data.gsToken, 'cloud') + } - this._webApi = new xboxWebApi({ - userToken: webToken.data.Token, - uhs: webToken.data.DisplayClaims.xui[0].uhs, - }) + this._webApi = new xboxWebApi({ + userToken: webToken.data.Token, + uhs: webToken.data.DisplayClaims.xui[0].uhs, + }) - console.log('xboxapi', this._webApi) + this._authentication._isAuthenticating = false + this._authentication._isAuthenticated = true - this._webApi.getProvider('profile').get('/users/me/profile/settings?settings=GameDisplayName,GameDisplayPicRaw,Gamerscore,Gamertag').then((result) => { - if(result.profileUsers.length > 0) { - for(const setting in result.profileUsers[0].settings){ + this._webApi.getProvider('profile').get('/users/me/profile/settings?settings=GameDisplayName,GameDisplayPicRaw,Gamerscore,Gamertag').then((result) => { + if(result.profileUsers.length > 0) { + for(const setting in result.profileUsers[0].settings){ - if(result.profileUsers[0].settings[setting].id === 'Gamertag'){ - this._store.set('user.gamertag', result.profileUsers[0].settings[setting].value) + if(result.profileUsers[0].settings[setting].id === 'Gamertag'){ + this._store.set('user.gamertag', result.profileUsers[0].settings[setting].value) - } else if(result.profileUsers[0].settings[setting].id === 'GameDisplayPicRaw'){ - this._store.set('user.gamerpic', result.profileUsers[0].settings[setting].value) + } else if(result.profileUsers[0].settings[setting].id === 'GameDisplayPicRaw'){ + this._store.set('user.gamerpic', result.profileUsers[0].settings[setting].value) - } else if(result.profileUsers[0].settings[setting].id === 'Gamerscore'){ - this._store.set('user.gamerscore', result.profileUsers[0].settings[setting].value) - } + } else if(result.profileUsers[0].settings[setting].id === 'Gamerscore'){ + this._store.set('user.gamerscore', result.profileUsers[0].settings[setting].value) } } + } - // Run workers - this._xboxWorker = new xboxWorker(this) - - }).catch((error) => { - this.log('electron', __filename+'[authenticationCompleted()] Failed to retrieve user profile:', error) - dialog.showMessageBox({ - message: 'Error: Failed to retrieve user profile:'+ JSON.stringify(error), - type: 'error', - }) - }) - }).catch((error) => { - this.log('electron', __filename+'[authenticationCompleted()] Failed to retrieve web tokens:', error) - dialog.showMessageBox({ - message: 'Error: Failed to retrieve web tokens:'+ JSON.stringify(error), - type: 'error', - }) - }) - - this._authentication._xal.getStreamingToken(this._authentication._tokenStore).then((streamingTokens) => { - this.log('electron', __filename+'[authenticationCompleted()] Using hosts for xCloud and xHome:') - this.log('electron', __filename+'[authenticationCompleted()] - xHome:', streamingTokens.xHomeToken.getDefaultRegion().baseUri.substring(8)) - this.log('electron', __filename+'[authenticationCompleted()] - xCloud:', streamingTokens.xCloudToken.getDefaultRegion().baseUri.substring(8)) - this._xHomeApi = new xCloudApi(this, streamingTokens.xHomeToken.getDefaultRegion().baseUri.substring(8), streamingTokens.xHomeToken.data.gsToken, 'home') - this._xCloudApi = new xCloudApi(this, streamingTokens.xCloudToken.getDefaultRegion().baseUri.substring(8), streamingTokens.xCloudToken.data.gsToken, 'cloud') - - // Let IPC know we are ready + // Run workers + this._xboxWorker = new xboxWorker(this) this._ipc.onUserLoaded() + }).catch((error) => { - this.log('electron', __filename+'[authenticationCompleted()] Failed to retrieve streaming tokens:', error) + this.log('electron', __filename+'[authenticationCompleted()] Failed to retrieve user profile:', error) dialog.showMessageBox({ - message: 'Error: Failed to retrieve streaming tokens:'+ JSON.stringify(error), + message: 'Error: Failed to retrieve user profile:'+ JSON.stringify(error), type: 'error', }) }) diff --git a/main/authentication.ts b/main/authentication.ts index dcad4810..dcbc1c76 100644 --- a/main/authentication.ts +++ b/main/authentication.ts @@ -1,7 +1,7 @@ import { session, dialog } from 'electron' import { createWindow } from './helpers' import Application from './application' -import { Xal } from 'xal-node' +import { Xal, TokenStore } from 'xal-node' import AuthTokenStore from './helpers/tokenstore' @@ -26,9 +26,9 @@ export default class Authentication { } checkAuthentication(){ - this._application.log('authenticationV2', __filename+'[checkAuthentication()] Starting token check...') + this._application.log('authenticationV2', '[checkAuthentication()] Starting token check...') if(this._tokenStore.hasValidAuthTokens()){ - this._application.log('authenticationV2', __filename+'[checkAuthentication()] Tokens are valid.') + this._application.log('authenticationV2', '[checkAuthentication()] Tokens are valid.') this.startSilentFlow() return true @@ -36,62 +36,87 @@ export default class Authentication { } else { if(this._tokenStore.getUserToken() !== undefined){ // We have a user token, lets try to refresh it. - this._application.log('authenticationV2', __filename+'[checkAuthentication()] Tokens are expired but we have a user token. Lets try to refresh the tokens.') + this._application.log('authenticationV2', '[checkAuthentication()] Tokens are expired but we have a user token. Lets try to refresh the tokens.') this.startSilentFlow() return true } else { - this._application.log('authenticationV2', __filename+'[checkAuthentication()] No tokens are present.') + this._application.log('authenticationV2', '[checkAuthentication()] No tokens are present.') return false } } } startSilentFlow(){ - this._application.log('authenticationV2', __filename+'[startSilentFlow()] Starting silent flow...') + this._application.log('authenticationV2', '[startSilentFlow()] Starting silent flow...') this._isAuthenticating = true - this._xal.refreshTokens(this._tokenStore).then((result) => { - this._application.log('authenticationV2', __filename+'[startSilentFlow()] Refreshed tokens:', result) + this._xal.refreshTokens(this._tokenStore).then(() => { + this._application.log('authenticationV2', '[startSilentFlow()] Tokens have been refreshed') - this._application.authenticationCompleted() - this._isAuthenticating = false - this._isAuthenticated = true - this._appLevel = 2 + this.getStreamingToken(this._tokenStore).then((streamingTokens) => { + if(streamingTokens.xCloudToken !== null){ + this._application.log('authenticationV2', '[startSilentFlow()] Retrieved both xHome and xCloud tokens') + this._appLevel = 2 + } else { + this._application.log('authenticationV2', '[startSilentFlow()] Retrieved xHome token only') + this._appLevel = 1 + } + + this._xal.getWebToken(this._tokenStore).then((webToken) => { + this._application.log('authenticationV2', __filename+'[startSilentFlow()] Web token received') + + this._application.authenticationCompleted(streamingTokens, webToken) + + }).catch((error) => { + this._application.log('authenticationV2', __filename+'[startSilentFlow()] Failed to retrieve web tokens:', error) + dialog.showMessageBox({ + message: 'Error: Failed to retrieve web tokens:'+ JSON.stringify(error), + type: 'error', + }) + }) + + }).catch((err) => { + this._application.log('authenticationV2', '[startSilentFlow()] Failed to retrieve streaming tokens:', err) + dialog.showMessageBox({ + message: 'Error: Failed to retrieve streaming tokens:'+ JSON.stringify(err), + type: 'error', + }) + }) }).catch((err) => { - this._application.log('authenticationV2', __filename+'[startSilentFlow()] Error refreshing tokens:', err) + this._application.log('authenticationV2', '[startSilentFlow()] Error refreshing tokens:', err) this._tokenStore.clear() }) } startAuthflow(){ - this._application.log('authenticationV2', __filename+'[startAuthflow()] Starting authentication flow') + this._application.log('authenticationV2', '[startAuthflow()] Starting authentication flow') this._xal.getRedirectUri().then((redirect) => { this.openAuthWindow(redirect.sisuAuth.MsaOauthRedirect) this._authCallback = (redirectUri) => { - this._application.log('authenticationV2', __filename+'[startAuthFlow()] Got redirect URI:', redirectUri) + this._application.log('authenticationV2', '[startAuthFlow()] Got redirect URI:', redirectUri) this._xal.authenticateUser(this._tokenStore, redirect, redirectUri).then((result) => { - this._application.log('authenticationV2', __filename+'[startAuthFlow()] Authenticated user:', result) + this._application.log('authenticationV2', '[startAuthFlow()] Authenticated user:', result) this.startSilentFlow() }).catch((err) => { - this._application.log('authenticationV2', __filename+'[startAuthFlow()] Error authenticating user:', err) + this._application.log('authenticationV2', '[startAuthFlow()] Error authenticating user:', err) dialog.showErrorBox('Error', 'Error authenticating user. Error details: '+JSON.stringify(err)) }) } }).catch((err) => { - this._application.log('authenticationV2', __filename+'[startAuthFlow()] Error getting redirect URI:', err) + this._application.log('authenticationV2', '[startAuthFlow()] Error getting redirect URI:', err) dialog.showErrorBox('Error', 'Error getting redirect URI. Error details: '+JSON.stringify(err)) }) } startWebviewHooks(){ - this._application.log('authenticationV2', __filename+'[startWebviewHooks()] Starting webview hooks') + this._application.log('authenticationV2', '[startWebviewHooks()] Starting webview hooks') session.defaultSession.webRequest.onHeadersReceived({ urls: [ @@ -101,13 +126,13 @@ export default class Authentication { }, (details, callback) => { if(details.responseHeaders.Location !== undefined && details.responseHeaders.Location[0].includes(this._xal._app.RedirectUri)){ - this._application.log('authenticationV2', __filename+'[startWebviewHooks()] Got redirect URI from OAUTH:', details.responseHeaders.Location[0]) + this._application.log('authenticationV2', '[startWebviewHooks()] Got redirect URI from OAUTH:', details.responseHeaders.Location[0]) this._authWindow.close() if(this._authCallback !== undefined){ this._authCallback(details.responseHeaders.Location[0]) } else { - this._application.log('authenticationV2', __filename+'[startWebviewHooks()] Authentication Callback is not defined:', this._authCallback) + this._application.log('authenticationV2', '[startWebviewHooks()] Authentication Callback is not defined:', this._authCallback) dialog.showErrorBox('Error', 'Authentication Callback is not defined. Error details: '+JSON.stringify(this._authCallback)) } @@ -129,10 +154,36 @@ export default class Authentication { this._authWindow = authWindow this._authWindow.on('close', () => { - this._application.log('authenticationV2', __filename+'[openAuthWindow()] Closed auth window') + this._application.log('authenticationV2', '[openAuthWindow()] Closed auth window') // @TODO: What to do? }) } + async getStreamingToken(tokenStore:TokenStore){ + const sisuToken = tokenStore.getSisuToken() + if(sisuToken === undefined) + throw new Error('Sisu token is missing. Please authenticate first') + + const xstsToken = await this._xal.doXstsAuthorization(sisuToken, 'http://gssv.xboxlive.com/') + + if(this._xal._xhomeToken === undefined || this._xal._xhomeToken.getSecondsValid() <= 60){ + this._xal._xhomeToken = await this._xal.getStreamToken(xstsToken, 'xhome') + } + + if(this._xal._xcloudToken === undefined || this._xal._xcloudToken.getSecondsValid() <= 60){ + try { + this._xal._xcloudToken = await this._xal.getStreamToken(xstsToken, 'xgpuweb') + } catch(error){ + try { + this._xal._xcloudToken = await this._xal.getStreamToken(xstsToken, 'xgpuwebf2p') + } catch(error){ + this._xal._xcloudToken = null + } + } + } + + return { xHomeToken: this._xal._xhomeToken, xCloudToken: this._xal._xcloudToken } + } + } diff --git a/main/ipc/app.ts b/main/ipc/app.ts index 3e3d85a8..2ef166ab 100644 --- a/main/ipc/app.ts +++ b/main/ipc/app.ts @@ -170,12 +170,16 @@ export default class IpcApp extends IpcBase { }) // Tokenstore values + const xCloudTokenValid = (this._application._authentication._xal._xcloudToken !== null) ? this._application._authentication._xal._xcloudToken.getSecondsValid() : 'None' returnValue.push({ name: 'XAL', data: [ { name: 'User token expires in', value: this._application._authentication._tokenStore.getUserToken().getSecondsValid() }, { name: 'Sisu token expires in', value: this._application._authentication._tokenStore.getSisuToken().getSecondsValid() }, { name: 'Authenticated user', value: this._application._authentication._tokenStore.getSisuToken().getGamertag() + ' ('+this._application._authentication._tokenStore.getSisuToken().getUserHash()+')' }, + { name: '', value: '' }, + { name: 'xHome Token validity', value: this._application._authentication._xal._xhomeToken.getSecondsValid() }, + { name: 'xCloud Token validity', value: xCloudTokenValid }, ], }) diff --git a/main/ipc/base.ts b/main/ipc/base.ts index f6e44907..35e01786 100644 --- a/main/ipc/base.ts +++ b/main/ipc/base.ts @@ -46,7 +46,7 @@ export default class IpcBase { } send(channel, args:EventArgs){ - this._application.log('Ipc:Send', 'Sending event: ['+channel+']', args) + this._application.log('Ipc:Send', 'Sending event: ['+channel+']', JSON.stringify(args)) this._application._mainWindow.webContents.send(channel, { action: args.action, id: args.id, diff --git a/main/ipc/xcloud.ts b/main/ipc/xcloud.ts index f8b134c4..9925456d 100644 --- a/main/ipc/xcloud.ts +++ b/main/ipc/xcloud.ts @@ -32,25 +32,29 @@ export default class IpcxCloud extends IpcBase { } onUserLoaded(){ - this._application._xCloudApi.getTitles().then((titles:any) => { - this._titleManager.setCloudTitles(titles).then(() => { + if(this._application._xCloudApi !== undefined){ + this._application._xCloudApi.getTitles().then((titles:any) => { + this._titleManager.setCloudTitles(titles).then(() => { - this._application.log('Ipc:xCloud', 'Titlemanager has loaded all titles.') - this._titlesAreLoaded = true + this._application.log('Ipc:xCloud', 'Titlemanager has loaded all titles.') + this._titlesAreLoaded = true - // Uncomment to delay the process of loading data - // setTimeout(() => { - // this._titlesAreLoaded = true - // }, 5000) + // Uncomment to delay the process of loading data + // setTimeout(() => { + // this._titlesAreLoaded = true + // }, 5000) + + }).catch((error) => { + this._application.log('Ipc:xCloud', 'Titlemanager is unable to load titles:', error) + console.log('Error setting xCloud titles:', error) + }) }).catch((error) => { - this._application.log('Ipc:xCloud', 'Titlemanager is unable to load titles:', error) - console.log('Error setting xCloud titles:', error) + this._application.log('Ipc:xCloud', 'Could not load recent titles:', error) }) - - }).catch((error) => { - this._application.log('Ipc:xCloud', 'Could not load recent titles:', error) - }) + } else { + this._application.log('Ipc:xCloud', 'xCloud IPC is not preloading titles as we dont have a valid token') + } } // Returns the last played titles (stream titles) diff --git a/main/webui.ts b/main/webui.ts index 10e24d94..08d4800b 100644 --- a/main/webui.ts +++ b/main/webui.ts @@ -20,7 +20,7 @@ export default class WebUI { const rawSettings = this._application._store.get('settings', defaultSettings) as object const settings = {...defaultSettings, ...rawSettings} - this._application.log('webui', 'Settings:', settings, rawSettings) + // this._application.log('webui', 'Settings:', settings, rawSettings) if(settings.webui_autostart === true){ this.startServer(settings.webui_port) diff --git a/renderer/components/header.tsx b/renderer/components/header.tsx index a52e4199..f2a46ae1 100644 --- a/renderer/components/header.tsx +++ b/renderer/components/header.tsx @@ -25,10 +25,12 @@ function Header({ name: 'My Consoles', title: 'View consoles', url: '/home', + active: false, }, { name: 'xCloud Library', title: 'Browse xCloud library', url: '/xcloud/home', + active: false, // },{ // name: 'Debug', // title: 'Debug page', @@ -37,25 +39,29 @@ function Header({ name: 'Settings', title: 'Change application settings', url: '/settings/home', + active: false, }, { name: gamertag, title: 'View profile', url: '/profile', + active: false, }, ] : [ { name: 'My Consoles', title: 'View consoles', url: '/home', - active: true, + active: false, }, { name: 'Settings', title: 'Change application settings', url: '/settings/home', + active: false, }, { name: gamertag, title: 'View profile', url: '/profile', + active: false, }, ] }