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 { atom } from "jotai";
|
||||||
import authService from "../services/authService";
|
import authService from "../services/authService";
|
||||||
import portalService, { Prelogin } from "../services/portalService";
|
import portalService, {
|
||||||
import { loginPortalAtom } from "./loginPortal";
|
Prelogin,
|
||||||
|
SamlPrelogin,
|
||||||
|
} from "../services/portalService";
|
||||||
|
import logger from "../utils/logger";
|
||||||
|
import { AbnormalPortalConfigError, loginPortalAtom } from "./loginPortal";
|
||||||
import { notifyErrorAtom } from "./notification";
|
import { notifyErrorAtom } from "./notification";
|
||||||
import { launchPasswordLoginAtom } from "./passwordLogin";
|
import { launchPasswordLoginAtom } from "./passwordLogin";
|
||||||
import { currentPortalDataAtom, portalAddressAtom } from "./portal";
|
import { currentPortalDataAtom, portalAddressAtom } from "./portal";
|
||||||
import { launchSamlLoginAtom, retrySamlLoginAtom } from "./samlLogin";
|
import { launchSamlLoginAtom, retrySamlLoginAtom } from "./samlLogin";
|
||||||
import { isProcessingAtom, statusAtom } from "./status";
|
import { isProcessingAtom, statusAtom } from "./status";
|
||||||
|
|
||||||
|
const MAX_ATTEMPTS = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to the portal, workflow:
|
* Connect to the portal, workflow:
|
||||||
* 1. Portal prelogin to get the prelogin data
|
* 1. Portal prelogin to get the prelogin data
|
||||||
@ -31,10 +37,10 @@ export const connectPortalAtom = atom(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
set(statusAtom, "prelogin");
|
set(statusAtom, "prelogin");
|
||||||
const prelogin = await portalService.prelogin(portal);
|
let prelogin = await portalService.prelogin(portal);
|
||||||
const isProcessing = get(isProcessingAtom);
|
const isProcessing = get(isProcessingAtom);
|
||||||
if (!isProcessing) {
|
if (!isProcessing) {
|
||||||
console.info("Request cancelled");
|
logger.info("Operation cancelled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +50,31 @@ export const connectPortalAtom = atom(
|
|||||||
} catch {
|
} catch {
|
||||||
// Otherwise, login with SAML or the password
|
// Otherwise, login with SAML or the password
|
||||||
if (prelogin.isSamlAuth) {
|
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 {
|
} else {
|
||||||
set(launchPasswordLoginAtom, prelogin);
|
set(launchPasswordLoginAtom, prelogin);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import gatewayService from "../services/gatewayService";
|
import gatewayService from "../services/gatewayService";
|
||||||
|
import logger from "../utils/logger";
|
||||||
import { isProcessingAtom, statusAtom } from "./status";
|
import { isProcessingAtom, statusAtom } from "./status";
|
||||||
import { connectVpnAtom } from "./vpn";
|
import { connectVpnAtom } from "./vpn";
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ export const loginGatewayAtom = atom(
|
|||||||
set(statusAtom, "gateway-login");
|
set(statusAtom, "gateway-login");
|
||||||
let token: string;
|
let token: string;
|
||||||
try {
|
try {
|
||||||
|
logger.info(`Logging in to gateway ${gateway}...`);
|
||||||
token = await gatewayService.login(gateway, credential);
|
token = await gatewayService.login(gateway, credential);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error("Failed to login to gateway");
|
throw new Error("Failed to login to gateway");
|
||||||
@ -26,7 +28,7 @@ export const loginGatewayAtom = atom(
|
|||||||
|
|
||||||
const isProcessing = get(isProcessingAtom);
|
const isProcessing = get(isProcessingAtom);
|
||||||
if (!isProcessing) {
|
if (!isProcessing) {
|
||||||
console.info("Request cancelled");
|
logger.info("Operation cancelled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,11 +4,23 @@ import portalService, {
|
|||||||
PortalCredential,
|
PortalCredential,
|
||||||
Prelogin,
|
Prelogin,
|
||||||
} from "../services/portalService";
|
} from "../services/portalService";
|
||||||
|
import logger from "../utils/logger";
|
||||||
|
import { redact } from "../utils/redact";
|
||||||
import { selectedGatewayAtom } from "./gateway";
|
import { selectedGatewayAtom } from "./gateway";
|
||||||
import { loginGatewayAtom } from "./loginGateway";
|
import { loginGatewayAtom } from "./loginGateway";
|
||||||
import { portalAddressAtom, updatePortalDataAtom } from "./portal";
|
import { portalAddressAtom, updatePortalDataAtom } from "./portal";
|
||||||
import { isProcessingAtom, statusAtom } from "./status";
|
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
|
// Indicates whether the portal config is being fetched
|
||||||
// This is mainly used to show the loading indicator in the password login form
|
// This is mainly used to show the loading indicator in the password login form
|
||||||
const portalConfigLoadingAtom = atom(false);
|
const portalConfigLoadingAtom = atom(false);
|
||||||
@ -42,23 +54,30 @@ export const loginPortalAtom = atom(
|
|||||||
try {
|
try {
|
||||||
portalConfig = await portalService.fetchConfig(portalAddress, credential);
|
portalConfig = await portalService.fetchConfig(portalAddress, credential);
|
||||||
configFetched?.();
|
configFetched?.();
|
||||||
|
} catch (err) {
|
||||||
|
const isProcessing = get(isProcessingAtom);
|
||||||
|
if (!isProcessing) {
|
||||||
|
logger.info("Operation cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
set(portalConfigLoadingAtom, false);
|
set(portalConfigLoadingAtom, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProcessing = get(isProcessingAtom);
|
const isProcessing = get(isProcessingAtom);
|
||||||
if (!isProcessing) {
|
if (!isProcessing) {
|
||||||
console.info("Request cancelled");
|
logger.info("Operation cancelled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { gateways, userAuthCookie, prelogonUserAuthCookie } = portalConfig;
|
const { gateways, userAuthCookie, prelogonUserAuthCookie } = portalConfig;
|
||||||
if (!gateways.length) {
|
if (!gateways.length) {
|
||||||
throw new Error("No gateway found");
|
throw new AbnormalPortalConfigError("No gateway found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userAuthCookie === "empty" || prelogonUserAuthCookie === "empty") {
|
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
|
// Here, we have got the portal config successfully, refresh the cached portal data
|
||||||
@ -87,6 +106,8 @@ export const loginPortalAtom = atom(
|
|||||||
preferredGateway: previousSelectedGateway,
|
preferredGateway: previousSelectedGateway,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.info(`Found the preferred gateway: ${name} (${redact(address)})`);
|
||||||
|
|
||||||
// Log in to the gateway
|
// Log in to the gateway
|
||||||
await set(loginGatewayAtom, address, {
|
await set(loginGatewayAtom, address, {
|
||||||
user: credential.user,
|
user: credential.user,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import authService, { AuthData } from "../services/authService";
|
import authService, { AuthData } from "../services/authService";
|
||||||
import portalService, { SamlPrelogin } from "../services/portalService";
|
import portalService, { SamlPrelogin } from "../services/portalService";
|
||||||
|
import logger from "../utils/logger";
|
||||||
|
import { redact } from "../utils/redact";
|
||||||
import { loginPortalAtom } from "./loginPortal";
|
import { loginPortalAtom } from "./loginPortal";
|
||||||
import { clearCookiesAtom, portalAddressAtom } from "./portal";
|
import { clearCookiesAtom, portalAddressAtom } from "./portal";
|
||||||
import { statusAtom } from "./status";
|
import { statusAtom } from "./status";
|
||||||
import { unwrap } from "./unwrap";
|
|
||||||
|
|
||||||
export const launchSamlLoginAtom = atom(
|
export const launchSamlLoginAtom = atom(
|
||||||
null,
|
null,
|
||||||
@ -38,7 +39,11 @@ export const launchSamlLoginAtom = atom(
|
|||||||
"prelogin-cookie": authData.prelogin_cookie,
|
"prelogin-cookie": authData.prelogin_cookie,
|
||||||
"portal-userauthcookie": authData.portal_userauthcookie,
|
"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);
|
await set(loginPortalAtom, credential, prelogin);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import vpnService from "../services/vpnService";
|
import vpnService from "../services/vpnService";
|
||||||
|
import logger from "../utils/logger";
|
||||||
|
import { redact } from "../utils/redact";
|
||||||
import { notifyErrorAtom } from "./notification";
|
import { notifyErrorAtom } from "./notification";
|
||||||
import { statusAtom } from "./status";
|
import { statusAtom } from "./status";
|
||||||
|
|
||||||
@ -8,6 +10,11 @@ export const connectVpnAtom = atom(
|
|||||||
async (_get, set, vpnAddress: string, token: string) => {
|
async (_get, set, vpnAddress: string, token: string) => {
|
||||||
try {
|
try {
|
||||||
set(statusAtom, "connecting");
|
set(statusAtom, "connecting");
|
||||||
|
logger.info(
|
||||||
|
`Connecting to VPN ${redact(vpnAddress)} with token ${redact(
|
||||||
|
token
|
||||||
|
)} ...`
|
||||||
|
);
|
||||||
await vpnService.connect(vpnAddress, token);
|
await vpnService.connect(vpnAddress, token);
|
||||||
set(statusAtom, "connected");
|
set(statusAtom, "connected");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -20,6 +27,7 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|||||||
export const disconnectVpnAtom = atom(null, async (get, set) => {
|
export const disconnectVpnAtom = atom(null, async (get, set) => {
|
||||||
try {
|
try {
|
||||||
set(statusAtom, "disconnecting");
|
set(statusAtom, "disconnecting");
|
||||||
|
logger.info("Disconnecting from VPN...");
|
||||||
await vpnService.disconnect();
|
await vpnService.disconnect();
|
||||||
// Sleep a short time, so that the client can receive the service's disconnected event.
|
// Sleep a short time, so that the client can receive the service's disconnected event.
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
|
@ -7,7 +7,7 @@ export default function useGlobalTheme() {
|
|||||||
() =>
|
() =>
|
||||||
createTheme({
|
createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: prefersDarkMode ? "light" : "light",
|
mode: prefersDarkMode ? "dark" : "light",
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
|
@ -6,6 +6,7 @@ import { useSnackbar } from "notistack";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { saveSettingsAtom } from "../../atoms/settings";
|
import { saveSettingsAtom } from "../../atoms/settings";
|
||||||
import settingsService, { TabValue } from "../../services/settingsService";
|
import settingsService, { TabValue } from "../../services/settingsService";
|
||||||
|
import logger from "../../utils/logger";
|
||||||
import OpenConnect from "./OpenConnect";
|
import OpenConnect from "./OpenConnect";
|
||||||
import OpenSSL from "./OpenSSL";
|
import OpenSSL from "./OpenSSL";
|
||||||
import Simulation from "./Simulation";
|
import Simulation from "./Simulation";
|
||||||
@ -33,7 +34,7 @@ export default function SettingsPanel() {
|
|||||||
enqueueSnackbar("Settings saved", { variant: "success" });
|
enqueueSnackbar("Settings saved", { variant: "success" });
|
||||||
await closeWindow();
|
await closeWindow();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Failed to save settings", err);
|
logger.warn(`Failed to save settings, ${err}`);
|
||||||
enqueueSnackbar("Failed to save settings", { variant: "error" });
|
enqueueSnackbar("Failed to save settings", { variant: "error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ class AuthService {
|
|||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
await listen("auth-error", (evt) => {
|
await listen("auth-error", (evt) => {
|
||||||
console.error("auth-error", evt);
|
console.warn("auth-error", evt);
|
||||||
this.authErrorCallback?.();
|
this.authErrorCallback?.();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Body, ResponseType, fetch } from "@tauri-apps/api/http";
|
import { Body, ResponseType, fetch } from "@tauri-apps/api/http";
|
||||||
import ErrorWithTitle from "../utils/ErrorWithTitle";
|
import ErrorWithTitle from "../utils/ErrorWithTitle";
|
||||||
|
import logger from "../utils/logger";
|
||||||
import { parseXml } from "../utils/parseXml";
|
import { parseXml } from "../utils/parseXml";
|
||||||
import { Gateway } from "./types";
|
|
||||||
import settingsService from "./settingsService";
|
import settingsService from "./settingsService";
|
||||||
|
import { Gateway } from "./types";
|
||||||
|
|
||||||
export type SamlPrelogin = {
|
export type SamlPrelogin = {
|
||||||
isSamlAuth: true;
|
isSamlAuth: true;
|
||||||
@ -43,6 +44,17 @@ class PortalService {
|
|||||||
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
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, {
|
response = await fetch<string>(preloginUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -52,16 +64,7 @@ class PortalService {
|
|||||||
query: {
|
query: {
|
||||||
"kerberos-support": "yes",
|
"kerberos-support": "yes",
|
||||||
},
|
},
|
||||||
body: Body.form({
|
body,
|
||||||
tmp: "tmp",
|
|
||||||
clientVer: "4100",
|
|
||||||
clientos: clientOS,
|
|
||||||
"os-version": osVersion,
|
|
||||||
"ipv6-support": "yes",
|
|
||||||
"default-browser": "0",
|
|
||||||
"cas-support": "yes",
|
|
||||||
// "host-id": "TODO, mac address?",
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (
|
if (
|
||||||
@ -73,7 +76,7 @@ class PortalService {
|
|||||||
"Unsafe Legacy Renegotiation disabled"
|
"Unsafe Legacy Renegotiation disabled"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.error("prelogin error", err);
|
logger.warn(`Failed to fetch prelogin: ${err}`);
|
||||||
throw new Error("Network error");
|
throw new Error("Network error");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +141,7 @@ class PortalService {
|
|||||||
prot: "https:",
|
prot: "https:",
|
||||||
inputStr: "",
|
inputStr: "",
|
||||||
jnlpReady: "jnlpReady",
|
jnlpReady: "jnlpReady",
|
||||||
computer: "Linux", // TODO
|
computer: clientOS, // TODO
|
||||||
clientos: clientOS,
|
clientos: clientOS,
|
||||||
ok: "Login",
|
ok: "Login",
|
||||||
direct: "yes",
|
direct: "yes",
|
||||||
@ -165,7 +168,6 @@ class PortalService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(response);
|
|
||||||
throw new Error(`Failed to fetch portal config: ${response.status}`);
|
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 { UserAttentionType, WebviewWindow } from "@tauri-apps/api/window";
|
||||||
import invokeCommand from "../utils/invokeCommand";
|
import invokeCommand from "../utils/invokeCommand";
|
||||||
|
import logger from "../utils/logger";
|
||||||
import { appStorage } from "./storageService";
|
import { appStorage } from "./storageService";
|
||||||
import { fs } from "@tauri-apps/api";
|
|
||||||
import { Command } from "@tauri-apps/api/shell";
|
|
||||||
|
|
||||||
export type TabValue = "simulation" | "openssl" | "openconnect";
|
export type TabValue = "simulation" | "openssl" | "openconnect";
|
||||||
const SETTINGS_WINDOW_LABEL = "settings";
|
const SETTINGS_WINDOW_LABEL = "settings";
|
||||||
@ -136,13 +134,15 @@ async function getOpenconnectConfig(): Promise<string> {
|
|||||||
const content = await invokeCommand<string>("openconnect_config");
|
const content = await invokeCommand<string>("openconnect_config");
|
||||||
return content;
|
return content;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
logger.warn(`Failed to read /etc/gpservice/gp.conf: ${e}`);
|
||||||
return "# Failed to read /etc/gpservice/gp.conf";
|
return "# Failed to read /etc/gpservice/gp.conf";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateOpenconnectConfig(content: string) {
|
async function updateOpenconnectConfig(content: string) {
|
||||||
const exitCode = await invokeCommand("update_openconnect_config", { content });
|
const exitCode = await invokeCommand("update_openconnect_config", {
|
||||||
|
content,
|
||||||
|
});
|
||||||
if (exitCode) {
|
if (exitCode) {
|
||||||
throw new Error(`Failed to update openconnect config: ${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