Skip to content

Commit

Permalink
SDA-4737 - Add retry logic for browser login (#2232)
Browse files Browse the repository at this point in the history
* SDA-4737 - Add retry logic for browser login

* SDA-4725 - Update SDA title bar branding

* SDA-4737 - Add abort & handle network changed use case
  • Loading branch information
KiranNiranjan authored Dec 5, 2024
1 parent 4c366bc commit b18bb12
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 65 deletions.
1 change: 1 addition & 0 deletions config/Symphony.config
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"autoUpdateCheckInterval": "30",
"enableBrowserLogin": false,
"browserLoginAutoConnect": false,
"browserLoginRetryTimeout": "5",
"overrideUserAgent": false,
"minimizeOnClose" : "ENABLED",
"launchOnStartup" : "ENABLED",
Expand Down
4 changes: 4 additions & 0 deletions installer/mac/postinstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ bring_to_front=$(sed -n '6p' ${settingsFilePath});
dev_tools_enabled=$(sed -n '7p' ${settingsFilePath});
enable_browser_login=$(sed -n '8p' ${settingsFilePath});
browser_login_autoconnect=$(sed -n '9p' ${settingsFilePath});
browser_login_retry_timeout=$(sed -n '10p' ${settingsFilePath});

## If any of the above values turn out to be empty, set default values ##
if [ "$pod_url" = "" ]; then pod_url="https://my.symphony.com"; fi
Expand All @@ -44,6 +45,7 @@ if [ "$bring_to_front" = "" ] || [ "$bring_to_front" = 'false' ]; then bring_to_
if [ "$dev_tools_enabled" = "" ]; then dev_tools_enabled=true; fi
if [ "$enable_browser_login" = "" ]; then enable_browser_login=false; fi
if [ "$browser_login_autoconnect" = "" ]; then browser_login_autoconnect=false; fi
if [ "$browser_login_retry_timeout" = "" ]; then browser_login_retry_timeout='5'; fi


## Add settings force auto update
Expand Down Expand Up @@ -86,6 +88,7 @@ if [ "$EUID" -ne 0 ]; then
defaults write "$plistFilePath" autoUpdateCheckInterval -string "30"
defaults write "$plistFilePath" enableBrowserLogin -bool "$enable_browser_login"
defaults write "$plistFilePath" browserLoginAutoConnect -bool "$browser_login_autoconnect"
defaults write "$plistFilePath" browserLoginRetryTimeout -string "$browser_login_retry_timeout"
defaults write "$plistFilePath" overrideUserAgent -bool false
defaults write "$plistFilePath" minimizeOnClose -string "$minimize_on_close"
defaults write "$plistFilePath" launchOnStartup -string "$launch_on_startup"
Expand Down Expand Up @@ -130,6 +133,7 @@ else
sudo -u "$userName" defaults write "$plistFilePath" autoUpdateCheckInterval -string "30"
sudo -u "$userName" defaults write "$plistFilePath" enableBrowserLogin -bool "$enable_browser_login"
sudo -u "$userName" defaults write "$plistFilePath" browserLoginAutoConnect -bool "$browser_login_autoconnect"
sudo -u "$userName" defaults write "$plistFilePath" browserLoginRetryTimeout -string "$browser_login_retry_timeout"
sudo -u "$userName" defaults write "$plistFilePath" overrideUserAgent -bool false
sudo -u "$userName" defaults write "$plistFilePath" minimizeOnClose -string "$minimize_on_close"
sudo -u "$userName" defaults write "$plistFilePath" launchOnStartup -string "$launch_on_startup"
Expand Down
1 change: 1 addition & 0 deletions spec/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe('config', () => {
'browserLoginAutoConnect',
'latestAutoUpdateChannelEnabled',
'betaAutoUpdateChannelEnabled',
'browserLoginRetryTimeout',
];
const globalConfig: object = { url: 'test' };
const userConfig: object = { configVersion: '4.0.1' };
Expand Down
1 change: 1 addition & 0 deletions spec/plistHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('Plist Handler', () => {
betaAutoUpdateChannelEnabled: undefined,
bringToFront: undefined,
browserLoginAutoConnect: undefined,
browserLoginRetryTimeout: undefined,
customFlags: {
authNegotiateDelegateWhitelist: undefined,
authServerWhitelist: undefined,
Expand Down
2 changes: 2 additions & 0 deletions src/app/config-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const ConfigFieldsDefaultValues: Partial<IConfig> = {
browserLoginAutoConnect: false,
latestAutoUpdateChannelEnabled: true,
betaAutoUpdateChannelEnabled: true,
browserLoginRetryTimeout: '5',
};

export const ConfigFieldsToRestart = new Set([
Expand Down Expand Up @@ -85,6 +86,7 @@ export interface IConfig {
startedAfterAutoUpdate?: boolean;
enableBrowserLogin?: boolean;
browserLoginAutoConnect?: boolean;
browserLoginRetryTimeout?: string;
betaAutoUpdateChannelEnabled?: boolean;
latestAutoUpdateChannelEnabled?: boolean;
forceAutoUpdate?: boolean;
Expand Down
228 changes: 163 additions & 65 deletions src/app/main-api-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ let loginUrl = '';
let formattedPodUrl = '';
let credentialsPromise;
const credentialsPromiseRefHolder: { [key: string]: any } = {};
const BROWSER_LOGIN_RETRY = 15 * 1000; // 15sec
const BROWSER_LOGIN_ABORT_TIMEOUT = 10 * 1000; // 10sec

/**
* Handle API related ipc messages from renderers. Only messages from windows
Expand Down Expand Up @@ -467,7 +469,10 @@ ipcMain.on(
? userConfigURL
: globalConfigURL;
const { subdomain, domain, tld } = whitelistHandler.parseDomain(podUrl);
const localConfig = config.getConfigFields(['enableBrowserLogin']);
const localConfig = config.getConfigFields([
'enableBrowserLogin',
'browserLoginRetryTimeout',
]);

formattedPodUrl = `https://${subdomain}.${domain}${tld}`;
loginUrl = getBrowserLoginUrl(formattedPodUrl);
Expand All @@ -483,7 +488,10 @@ ipcMain.on(
'check if sso is enabled for the pod',
formattedPodUrl,
);
loadPodUrl(false);
const timeout = localConfig.browserLoginRetryTimeout
? parseInt(localConfig.browserLoginRetryTimeout, 10)
: 0;
loadPodUrl(false, timeout);
} else {
logger.info(
'main-api-handler:',
Expand Down Expand Up @@ -739,72 +747,162 @@ const logApiCallParams = (arg: any) => {
}
};

const loadPodUrl = (proxyLogin = false) => {
logger.info('loading pod URL. Proxy: ', proxyLogin);
let onLogin = {};
if (proxyLogin) {
onLogin = {
async onLogin(authInfo) {
// this 'authInfo' is the one received by the 'login' event. See https://www.electronjs.org/docs/latest/api/client-request#event-login
proxyDetails.hostname = authInfo.host || authInfo.realm;
await credentialsPromise;
return Promise.resolve({
username: proxyDetails.username,
password: proxyDetails.password,
});
},
};
}
fetch(`${formattedPodUrl}${AUTH_STATUS_PATH}`, onLogin)
.then(async (response) => {
const authResponse = (await response.json()) as IAuthResponse;
logger.info('main-api-handler:', 'check auth response', authResponse);
if (authResponse.authenticationType === 'sso') {
logger.info(
'main-api-handler: browser login is enabled - logging in',
loginUrl,
);
await shell.openExternal(loginUrl);
} else {
logger.info(
'main-api-handler: no SSO - loading main window with',
formattedPodUrl,
);
const mainWebContents = windowHandler.getMainWebContents();
if (mainWebContents && !mainWebContents.isDestroyed()) {
windowHandler.setMainWindowOrigin(formattedPodUrl);
mainWebContents.loadURL(formattedPodUrl);
}
/**
* Loads the Pod URL and handles potential authentication challenges.
*
* This function attempts to fetch the Pod URL and handles various authentication scenarios:
* - Standard login (no proxy)
* - Proxy login with authentication window
* - Login retry logic for failed attempts
*
* @param {boolean} [proxyLogin=false] - Whether to use a proxy for the request. Defaults to false.
* @param {number} [retryDurationInMinutes=0] - The duration (in minutes) for the retry logic. Defaults to 0 (no retries).
*/
const loadPodUrl = (() => {
let isRetryInProgress: boolean = false;
let retryTimeoutId: NodeJS.Timeout | null = null;

return (proxyLogin = false, retryDurationInMinutes = 0) => {
logger.info('main-api-handler: loading pod URL. Proxy: ', proxyLogin);

const maxRetries = Math.floor(
(retryDurationInMinutes * 60 * 1000) / BROWSER_LOGIN_RETRY,
);
let retryCount = 0;

// Function to attempt fetching the endpoint
const attemptFetch = async () => {
if (retryTimeoutId) {
clearTimeout(retryTimeoutId); // Clear any existing timeout to avoid overlaps
}
})
.catch(async (error) => {
if (
(error.type === 'proxy' && error.code === 'PROXY_AUTH_FAILED') ||
(error.code === 'ERR_TOO_MANY_RETRIES' && proxyLogin)
) {
credentialsPromise = new Promise((res, _rej) => {
credentialsPromiseRefHolder.resolutionCallback = res;
});
const welcomeWindow =
windowHandler.getMainWindow() as ICustomBrowserWindow;
windowHandler.createBasicAuthWindow(
welcomeWindow,
proxyDetails.hostname,
proxyDetails.retries === 0,
undefined,
(username, password) => {
proxyDetails.username = username;
proxyDetails.password = password;
credentialsPromiseRefHolder.resolutionCallback(true);
loadPodUrl(true);

logger.info(
'main-api-handler: Attempting to fetch the pod URL. Attempt:',
retryCount + 1,
);

let onLogin = {};
if (proxyLogin) {
onLogin = {
async onLogin(authInfo) {
// this 'authInfo' is the one received by the 'login' event. See https://www.electronjs.org/docs/latest/api/client-request#event-login
proxyDetails.hostname = authInfo.host || authInfo.realm;
await credentialsPromise;
return Promise.resolve({
username: proxyDetails.username,
password: proxyDetails.password,
});
},
);
proxyDetails.retries += 1;
};
}
logger.error(
'main-api-handler: browser login error. Details: ',
error.type,
error.code,

const controller = new AbortController();
const timeout = setTimeout(
() => controller.abort(),
BROWSER_LOGIN_ABORT_TIMEOUT,
);
try {
const response = await fetch(`${formattedPodUrl}${AUTH_STATUS_PATH}`, {
...onLogin,
signal: controller.signal,
});
const authResponse = (await response.json()) as IAuthResponse;
logger.info('main-api-handler: check auth response', authResponse);

if (authResponse.authenticationType === 'sso') {
logger.info(
'main-api-handler: browser login is enabled - logging in',
loginUrl,
);
await shell.openExternal(loginUrl);
} else {
logger.info(
'main-api-handler: no SSO - loading main window with',
formattedPodUrl,
);
const mainWebContents = windowHandler.getMainWebContents();
if (mainWebContents && !mainWebContents.isDestroyed()) {
windowHandler.setMainWindowOrigin(formattedPodUrl);
mainWebContents.loadURL(formattedPodUrl);
}
}

isRetryInProgress = false;
setLoginRetryState(isRetryInProgress);
retryTimeoutId = null;
} catch (error: any) {
if (
(error.type === 'proxy' && error.code === 'PROXY_AUTH_FAILED') ||
(error.code === 'ERR_TOO_MANY_RETRIES' && proxyLogin)
) {
credentialsPromise = new Promise((res, _rej) => {
credentialsPromiseRefHolder.resolutionCallback = res;
});
const welcomeWindow =
windowHandler.getMainWindow() as ICustomBrowserWindow;
windowHandler.createBasicAuthWindow(
welcomeWindow,
proxyDetails.hostname,
proxyDetails.retries === 0,
undefined,
(username, password) => {
proxyDetails.username = username;
proxyDetails.password = password;
credentialsPromiseRefHolder.resolutionCallback(true);
loadPodUrl(true);
},
);
proxyDetails.retries += 1;
} else {
logger.error(
'main-api-handler: browser login error. Details: ',
error.type,
error.code,
);
retryCount++;
if (retryCount < maxRetries || error.code === 'ERR_NETWORK_CHANGED') {
retryTimeoutId = setTimeout(attemptFetch, BROWSER_LOGIN_RETRY);
} else {
logger.error(
'main-api-handler: Retry attempts exhausted. Endpoint unreachable.',
);
isRetryInProgress = false;
setLoginRetryState(isRetryInProgress);
}
}
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
};

// Start the retry logic only if it's not already in progress
if (!isRetryInProgress) {
isRetryInProgress = true;
setLoginRetryState(isRetryInProgress);
attemptFetch();
} else {
logger.info(
'main-api-handler: Retry logic already in progress. Ignoring duplicate call.',
);
}
};
})();

/**
* Updates the login retry state in the main web content.
*
* Sends a message to the main web content indicating whether a login retry is in progress.
* This message is used to update the UI accordingly.
*
* @param {boolean} isRetryInProgress - A boolean indicating whether a login retry is in progress.
*/
const setLoginRetryState = (isRetryInProgress: boolean) => {
const mainWebContents = windowHandler.getMainWebContents();
if (mainWebContents && !mainWebContents.isDestroyed()) {
mainWebContents.send('welcome', {
isRetryInProgress,
});
}
};
1 change: 1 addition & 0 deletions src/app/plist-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const GENERAL_SETTINGS = {
autoUpdateCheckInterval: 'string',
enableBrowserLogin: 'boolean',
browserLoginAutoConnect: 'boolean',
browserLoginRetryTimeout: 'string',
overrideUserAgent: 'boolean',
minimizeOnClose: 'string',
launchOnStartup: 'string',
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface IState {
isBrowserLoginEnabled: boolean;
browserLoginAutoConnect: boolean;
isLoading: boolean;
isRetryInProgress: boolean;
}

const WELCOME_NAMESPACE = 'Welcome';
Expand All @@ -37,6 +38,7 @@ export default class Welcome extends React.Component<{}, IState> {
isBrowserLoginEnabled: true,
browserLoginAutoConnect: false,
isLoading: false,
isRetryInProgress: false,
};
this.updateState = this.updateState.bind(this);
}
Expand All @@ -52,6 +54,7 @@ export default class Welcome extends React.Component<{}, IState> {
isLoading,
isBrowserLoginEnabled,
isFirstTimeLaunch,
isRetryInProgress,
} = this.state;
return (
<div className='Welcome' lang={i18n.getLocale()}>
Expand Down Expand Up @@ -126,6 +129,7 @@ export default class Welcome extends React.Component<{}, IState> {
<button
className='Welcome-retry-button'
onClick={this.eventHandlers.onLogin}
disabled={isRetryInProgress}
>
{i18n.t('Retry', WELCOME_NAMESPACE)()}
</button>
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/styles/welcome.less
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ body {

&-continue-button-loading:disabled {
background-color: @electricity-ui-50;
cursor: not-allowed;
}

&-redirect-info-text-container {
Expand Down Expand Up @@ -271,6 +272,11 @@ body {
color: @electricity-ui-30;
}

&-retry-button:disabled {
cursor: not-allowed;
color: @graphite-60;
}

&-auto-connect-wrapper {
display: flex;
margin-top: 18px;
Expand Down

0 comments on commit b18bb12

Please sign in to comment.