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:
parent
3098d1170f
commit
3674a28dee
@ -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)}>`;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user