From 3674a28deed80183abbdb05226ca8f5ecdf32381 Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Thu, 31 Aug 2023 21:18:30 +0800 Subject: [PATCH] refactor: auto retry the auth flow --- gpgui/src/atoms/connectPortal.ts | 40 ++++++++++++++++--- gpgui/src/atoms/loginGateway.ts | 4 +- gpgui/src/atoms/loginPortal.ts | 27 +++++++++++-- gpgui/src/atoms/samlLogin.ts | 9 ++++- gpgui/src/atoms/vpn.ts | 8 ++++ .../src/components/AppShell/useGlobalTheme.ts | 2 +- gpgui/src/components/settings/index.tsx | 3 +- gpgui/src/services/authService.ts | 2 +- gpgui/src/services/portalService.ts | 30 +++++++------- gpgui/src/services/settingsService.ts | 10 ++--- gpgui/src/utils/logger.ts | 22 ++++++++++ gpgui/src/utils/redact.ts | 11 +++++ 12 files changed, 135 insertions(+), 33 deletions(-) create mode 100644 gpgui/src/utils/logger.ts create mode 100644 gpgui/src/utils/redact.ts diff --git a/gpgui/src/atoms/connectPortal.ts b/gpgui/src/atoms/connectPortal.ts index 81fa416..aaab248 100644 --- a/gpgui/src/atoms/connectPortal.ts +++ b/gpgui/src/atoms/connectPortal.ts @@ -1,13 +1,19 @@ import { atom } from "jotai"; import authService from "../services/authService"; -import portalService, { Prelogin } from "../services/portalService"; -import { loginPortalAtom } from "./loginPortal"; +import portalService, { + Prelogin, + SamlPrelogin, +} from "../services/portalService"; +import logger from "../utils/logger"; +import { AbnormalPortalConfigError, loginPortalAtom } from "./loginPortal"; import { notifyErrorAtom } from "./notification"; import { launchPasswordLoginAtom } from "./passwordLogin"; import { currentPortalDataAtom, portalAddressAtom } from "./portal"; import { launchSamlLoginAtom, retrySamlLoginAtom } from "./samlLogin"; import { isProcessingAtom, statusAtom } from "./status"; +const MAX_ATTEMPTS = 10; + /** * Connect to the portal, workflow: * 1. Portal prelogin to get the prelogin data @@ -31,10 +37,10 @@ export const connectPortalAtom = atom( try { set(statusAtom, "prelogin"); - const prelogin = await portalService.prelogin(portal); + let prelogin = await portalService.prelogin(portal); const isProcessing = get(isProcessingAtom); if (!isProcessing) { - console.info("Request cancelled"); + logger.info("Operation cancelled"); return; } @@ -44,7 +50,31 @@ export const connectPortalAtom = atom( } catch { // Otherwise, login with SAML or the password if (prelogin.isSamlAuth) { - await set(launchSamlLoginAtom, prelogin); + let attemptCount = 0; + while (true) { + try { + attemptCount++; + logger.info( + `(${attemptCount}/${MAX_ATTEMPTS}) Launching SAML login...` + ); + await set(launchSamlLoginAtom, prelogin as SamlPrelogin); + break; + } catch (err) { + if (attemptCount >= MAX_ATTEMPTS) { + throw err; + } + + if (err instanceof AbnormalPortalConfigError) { + logger.info( + `Got abnormal portal config: ${err.message}, retrying...` + ); + prelogin = await portalService.prelogin(portal); + continue; + } else { + throw err; + } + } + } } else { set(launchPasswordLoginAtom, prelogin); } diff --git a/gpgui/src/atoms/loginGateway.ts b/gpgui/src/atoms/loginGateway.ts index f4a5361..4d79c40 100644 --- a/gpgui/src/atoms/loginGateway.ts +++ b/gpgui/src/atoms/loginGateway.ts @@ -1,5 +1,6 @@ import { atom } from "jotai"; import gatewayService from "../services/gatewayService"; +import logger from "../utils/logger"; import { isProcessingAtom, statusAtom } from "./status"; import { connectVpnAtom } from "./vpn"; @@ -19,6 +20,7 @@ export const loginGatewayAtom = atom( set(statusAtom, "gateway-login"); let token: string; try { + logger.info(`Logging in to gateway ${gateway}...`); token = await gatewayService.login(gateway, credential); } catch (err) { throw new Error("Failed to login to gateway"); @@ -26,7 +28,7 @@ export const loginGatewayAtom = atom( const isProcessing = get(isProcessingAtom); if (!isProcessing) { - console.info("Request cancelled"); + logger.info("Operation cancelled"); return; } diff --git a/gpgui/src/atoms/loginPortal.ts b/gpgui/src/atoms/loginPortal.ts index fd90fb1..a388399 100644 --- a/gpgui/src/atoms/loginPortal.ts +++ b/gpgui/src/atoms/loginPortal.ts @@ -4,11 +4,23 @@ import portalService, { PortalCredential, Prelogin, } from "../services/portalService"; +import logger from "../utils/logger"; +import { redact } from "../utils/redact"; import { selectedGatewayAtom } from "./gateway"; import { loginGatewayAtom } from "./loginGateway"; import { portalAddressAtom, updatePortalDataAtom } from "./portal"; import { isProcessingAtom, statusAtom } from "./status"; +/** + * The error thrown when the portal config is abnormal, can back to normal after retrying + */ +export class AbnormalPortalConfigError extends Error { + constructor(message: string) { + super(message); + this.name = "AbnormalPortalConfigError"; + } +} + // Indicates whether the portal config is being fetched // This is mainly used to show the loading indicator in the password login form const portalConfigLoadingAtom = atom(false); @@ -42,23 +54,30 @@ export const loginPortalAtom = atom( try { portalConfig = await portalService.fetchConfig(portalAddress, credential); configFetched?.(); + } catch (err) { + const isProcessing = get(isProcessingAtom); + if (!isProcessing) { + logger.info("Operation cancelled"); + return; + } + throw err; } finally { set(portalConfigLoadingAtom, false); } const isProcessing = get(isProcessingAtom); if (!isProcessing) { - console.info("Request cancelled"); + logger.info("Operation cancelled"); return; } const { gateways, userAuthCookie, prelogonUserAuthCookie } = portalConfig; if (!gateways.length) { - throw new Error("No gateway found"); + throw new AbnormalPortalConfigError("No gateway found"); } if (userAuthCookie === "empty" || prelogonUserAuthCookie === "empty") { - throw new Error("Got empty user auth cookie"); + throw new AbnormalPortalConfigError("Empty user auth cookie"); } // Here, we have got the portal config successfully, refresh the cached portal data @@ -87,6 +106,8 @@ export const loginPortalAtom = atom( preferredGateway: previousSelectedGateway, }); + logger.info(`Found the preferred gateway: ${name} (${redact(address)})`); + // Log in to the gateway await set(loginGatewayAtom, address, { user: credential.user, diff --git a/gpgui/src/atoms/samlLogin.ts b/gpgui/src/atoms/samlLogin.ts index cbc7271..d4113ed 100644 --- a/gpgui/src/atoms/samlLogin.ts +++ b/gpgui/src/atoms/samlLogin.ts @@ -1,10 +1,11 @@ import { atom } from "jotai"; import authService, { AuthData } from "../services/authService"; import portalService, { SamlPrelogin } from "../services/portalService"; +import logger from "../utils/logger"; +import { redact } from "../utils/redact"; import { loginPortalAtom } from "./loginPortal"; import { clearCookiesAtom, portalAddressAtom } from "./portal"; import { statusAtom } from "./status"; -import { unwrap } from "./unwrap"; export const launchSamlLoginAtom = atom( null, @@ -38,7 +39,11 @@ export const launchSamlLoginAtom = atom( "prelogin-cookie": authData.prelogin_cookie, "portal-userauthcookie": authData.portal_userauthcookie, }; - + logger.info( + `SAML login succeeded, prelogin-cookie: ${redact( + authData.prelogin_cookie + )}, portal-userauthcookie: ${redact(authData.portal_userauthcookie)}` + ); await set(loginPortalAtom, credential, prelogin); } ); diff --git a/gpgui/src/atoms/vpn.ts b/gpgui/src/atoms/vpn.ts index 6e9ad05..239e65e 100644 --- a/gpgui/src/atoms/vpn.ts +++ b/gpgui/src/atoms/vpn.ts @@ -1,5 +1,7 @@ import { atom } from "jotai"; import vpnService from "../services/vpnService"; +import logger from "../utils/logger"; +import { redact } from "../utils/redact"; import { notifyErrorAtom } from "./notification"; import { statusAtom } from "./status"; @@ -8,6 +10,11 @@ export const connectVpnAtom = atom( async (_get, set, vpnAddress: string, token: string) => { try { set(statusAtom, "connecting"); + logger.info( + `Connecting to VPN ${redact(vpnAddress)} with token ${redact( + token + )} ...` + ); await vpnService.connect(vpnAddress, token); set(statusAtom, "connected"); } catch (err) { @@ -20,6 +27,7 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const disconnectVpnAtom = atom(null, async (get, set) => { try { set(statusAtom, "disconnecting"); + logger.info("Disconnecting from VPN..."); await vpnService.disconnect(); // Sleep a short time, so that the client can receive the service's disconnected event. await sleep(100); diff --git a/gpgui/src/components/AppShell/useGlobalTheme.ts b/gpgui/src/components/AppShell/useGlobalTheme.ts index 2521f5a..4627be7 100644 --- a/gpgui/src/components/AppShell/useGlobalTheme.ts +++ b/gpgui/src/components/AppShell/useGlobalTheme.ts @@ -7,7 +7,7 @@ export default function useGlobalTheme() { () => createTheme({ palette: { - mode: prefersDarkMode ? "light" : "light", + mode: prefersDarkMode ? "dark" : "light", }, components: { MuiButton: { diff --git a/gpgui/src/components/settings/index.tsx b/gpgui/src/components/settings/index.tsx index f0db823..eae33b4 100644 --- a/gpgui/src/components/settings/index.tsx +++ b/gpgui/src/components/settings/index.tsx @@ -6,6 +6,7 @@ import { useSnackbar } from "notistack"; import { useState } from "react"; import { saveSettingsAtom } from "../../atoms/settings"; import settingsService, { TabValue } from "../../services/settingsService"; +import logger from "../../utils/logger"; import OpenConnect from "./OpenConnect"; import OpenSSL from "./OpenSSL"; import Simulation from "./Simulation"; @@ -33,7 +34,7 @@ export default function SettingsPanel() { enqueueSnackbar("Settings saved", { variant: "success" }); await closeWindow(); } catch (err) { - console.warn("Failed to save settings", err); + logger.warn(`Failed to save settings, ${err}`); enqueueSnackbar("Failed to save settings", { variant: "error" }); } }; diff --git a/gpgui/src/services/authService.ts b/gpgui/src/services/authService.ts index cebdc7d..c7deb86 100644 --- a/gpgui/src/services/authService.ts +++ b/gpgui/src/services/authService.ts @@ -17,7 +17,7 @@ class AuthService { private async init() { await listen("auth-error", (evt) => { - console.error("auth-error", evt); + console.warn("auth-error", evt); this.authErrorCallback?.(); }); } diff --git a/gpgui/src/services/portalService.ts b/gpgui/src/services/portalService.ts index 57e871f..b122004 100644 --- a/gpgui/src/services/portalService.ts +++ b/gpgui/src/services/portalService.ts @@ -1,8 +1,9 @@ import { Body, ResponseType, fetch } from "@tauri-apps/api/http"; import ErrorWithTitle from "../utils/ErrorWithTitle"; +import logger from "../utils/logger"; import { parseXml } from "../utils/parseXml"; -import { Gateway } from "./types"; import settingsService from "./settingsService"; +import { Gateway } from "./types"; export type SamlPrelogin = { isSamlAuth: true; @@ -43,6 +44,17 @@ class PortalService { let response; try { + const body = Body.form({ + tmp: "tmp", + clientVer: "4100", + clientos: clientOS, + "os-version": osVersion, + "ipv6-support": "yes", + "default-browser": "0", + "cas-support": "yes", + // "host-id": "TODO, mac address?", + }); + response = await fetch(preloginUrl, { method: "POST", headers: { @@ -52,16 +64,7 @@ class PortalService { query: { "kerberos-support": "yes", }, - body: Body.form({ - tmp: "tmp", - clientVer: "4100", - clientos: clientOS, - "os-version": osVersion, - "ipv6-support": "yes", - "default-browser": "0", - "cas-support": "yes", - // "host-id": "TODO, mac address?", - }), + body, }); } catch (err) { if ( @@ -73,7 +76,7 @@ class PortalService { "Unsafe Legacy Renegotiation disabled" ); } - console.error("prelogin error", err); + logger.warn(`Failed to fetch prelogin: ${err}`); throw new Error("Network error"); } @@ -138,7 +141,7 @@ class PortalService { prot: "https:", inputStr: "", jnlpReady: "jnlpReady", - computer: "Linux", // TODO + computer: clientOS, // TODO clientos: clientOS, ok: "Login", direct: "yes", @@ -165,7 +168,6 @@ class PortalService { }); if (!response.ok) { - console.error(response); throw new Error(`Failed to fetch portal config: ${response.status}`); } diff --git a/gpgui/src/services/settingsService.ts b/gpgui/src/services/settingsService.ts index 35ff8e5..066c47d 100644 --- a/gpgui/src/services/settingsService.ts +++ b/gpgui/src/services/settingsService.ts @@ -1,9 +1,7 @@ -import { tempdir } from "@tauri-apps/api/os"; import { UserAttentionType, WebviewWindow } from "@tauri-apps/api/window"; import invokeCommand from "../utils/invokeCommand"; +import logger from "../utils/logger"; import { appStorage } from "./storageService"; -import { fs } from "@tauri-apps/api"; -import { Command } from "@tauri-apps/api/shell"; export type TabValue = "simulation" | "openssl" | "openconnect"; const SETTINGS_WINDOW_LABEL = "settings"; @@ -136,13 +134,15 @@ async function getOpenconnectConfig(): Promise { const content = await invokeCommand("openconnect_config"); return content; } catch (e) { - console.error(e); + logger.warn(`Failed to read /etc/gpservice/gp.conf: ${e}`); return "# Failed to read /etc/gpservice/gp.conf"; } } async function updateOpenconnectConfig(content: string) { - const exitCode = await invokeCommand("update_openconnect_config", { content }); + const exitCode = await invokeCommand("update_openconnect_config", { + content, + }); if (exitCode) { throw new Error(`Failed to update openconnect config: ${exitCode}`); } diff --git a/gpgui/src/utils/logger.ts b/gpgui/src/utils/logger.ts new file mode 100644 index 0000000..a9b2273 --- /dev/null +++ b/gpgui/src/utils/logger.ts @@ -0,0 +1,22 @@ +import * as log from "tauri-plugin-log-api"; + +const methods = ["trace", "debug", "info", "warn", "error"] as const; +type Logger = { + [key in (typeof methods)[number]]: ( + message: string, + options?: log.LogOptions + ) => Promise; +}; + +function createLogger() { + const logger: Logger = {} as Logger; + for (const method of methods) { + logger[method] = async (message, options) => { + console[method](message); + await log[method](message, options); + }; + } + return logger; +} + +export default createLogger(); diff --git a/gpgui/src/utils/redact.ts b/gpgui/src/utils/redact.ts new file mode 100644 index 0000000..8852762 --- /dev/null +++ b/gpgui/src/utils/redact.ts @@ -0,0 +1,11 @@ +export function redact(data?: string) { + if (!data) { + return `<${data}>`; + } + + if (data.length <= 8) { + return `<${data.replace(/./g, "*")}>`; + } + + return `<${data.slice(0, 2)}...${data.slice(-2)}>`; +}