mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	refactor: auto retry the auth flow
This commit is contained in:
		| @@ -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); | ||||
|         } | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -7,7 +7,7 @@ export default function useGlobalTheme() { | ||||
|     () => | ||||
|       createTheme({ | ||||
|         palette: { | ||||
|           mode: prefersDarkMode ? "light" : "light", | ||||
|           mode: prefersDarkMode ? "dark" : "light", | ||||
|         }, | ||||
|         components: { | ||||
|           MuiButton: { | ||||
|   | ||||
| @@ -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" }); | ||||
|     } | ||||
|   }; | ||||
|   | ||||
| @@ -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?.(); | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -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<string>(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}`); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<string> { | ||||
|     const content = await invokeCommand<string>("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}`); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										22
									
								
								gpgui/src/utils/logger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								gpgui/src/utils/logger.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<void>; | ||||
| }; | ||||
|  | ||||
| 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(); | ||||
							
								
								
									
										11
									
								
								gpgui/src/utils/redact.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								gpgui/src/utils/redact.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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)}>`; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user