mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-04-02 18:31:50 -04:00
refactor: get portal config
This commit is contained in:
parent
c74ce52c2d
commit
d975f981cc
@ -131,7 +131,7 @@ fn setup_webview(
|
|||||||
) -> tauri::Result<()> {
|
) -> tauri::Result<()> {
|
||||||
window.with_webview(move |wv| {
|
window.with_webview(move |wv| {
|
||||||
let wv = wv.inner();
|
let wv = wv.inner();
|
||||||
let event_tx = event_tx.clone();
|
let event_tx_clone = event_tx.clone();
|
||||||
|
|
||||||
if clear_cookies {
|
if clear_cookies {
|
||||||
clear_webview_cookies(&wv);
|
clear_webview_cookies(&wv);
|
||||||
@ -146,25 +146,22 @@ fn setup_webview(
|
|||||||
// Empty URI indicates that an error occurred
|
// Empty URI indicates that an error occurred
|
||||||
if uri.is_empty() {
|
if uri.is_empty() {
|
||||||
warn!("Empty URI loaded");
|
warn!("Empty URI loaded");
|
||||||
if let Err(err) = event_tx.blocking_send(AuthEvent::Error(AuthError::TokenInvalid))
|
send_auth_error(&event_tx_clone, AuthError::TokenInvalid);
|
||||||
{
|
|
||||||
warn!("Error sending event: {}", err);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO, redact URI
|
// TODO, redact URI
|
||||||
debug!("Loaded URI: {}", uri);
|
debug!("Loaded URI: {}", uri);
|
||||||
|
|
||||||
if let Some(main_res) = wv.main_resource() {
|
if let Some(main_res) = wv.main_resource() {
|
||||||
parse_auth_data(&main_res, event_tx.clone());
|
parse_auth_data(&main_res, event_tx_clone.clone());
|
||||||
} else {
|
} else {
|
||||||
warn!("No main_resource");
|
warn!("No main_resource");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wv.connect_load_failed(|_wv, event, err_msg, err| {
|
wv.connect_load_failed(move |_wv, event, _uri, err| {
|
||||||
warn!("Load failed: {:?}, {}, {:?}", event, err_msg, err);
|
warn!("Load failed: {:?}, {:?}", event, err);
|
||||||
|
send_auth_error(&event_tx, AuthError::TokenInvalid);
|
||||||
false
|
false
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -175,9 +172,7 @@ fn setup_window(window: &Window, event_tx: mpsc::Sender<AuthEvent>) -> EventHand
|
|||||||
window.on_window_event(move |event| {
|
window.on_window_event(move |event| {
|
||||||
if let CloseRequested { api, .. } = event {
|
if let CloseRequested { api, .. } = event {
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
if let Err(err) = event_tx_clone.blocking_send(AuthEvent::Cancel) {
|
send_auth_event(&event_tx_clone, AuthEvent::Cancel);
|
||||||
warn!("Error sending event: {}", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -335,9 +330,7 @@ fn parse_auth_data(main_res: &WebResource, event_tx: mpsc::Sender<AuthEvent>) {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
debug!("Error reading auth data from HTML: {:?}", err);
|
debug!("Error reading auth data from HTML: {:?}", err);
|
||||||
if let Err(err) = event_tx.blocking_send(AuthEvent::Error(err)) {
|
send_auth_error(&event_tx, err);
|
||||||
warn!("Error sending event: {}", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -395,7 +388,15 @@ fn parse_xml_tag(html: &str, tag: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn send_auth_data(event_tx: &mpsc::Sender<AuthEvent>, auth_data: AuthData) {
|
fn send_auth_data(event_tx: &mpsc::Sender<AuthEvent>, auth_data: AuthData) {
|
||||||
if let Err(err) = event_tx.blocking_send(AuthEvent::Success(auth_data)) {
|
send_auth_event(event_tx, AuthEvent::Success(auth_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_auth_error(event_tx: &mpsc::Sender<AuthEvent>, err: AuthError) {
|
||||||
|
send_auth_event(event_tx, AuthEvent::Error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_auth_event(event_tx: &mpsc::Sender<AuthEvent>, auth_event: AuthEvent) {
|
||||||
|
if let Err(err) = event_tx.blocking_send(auth_event) {
|
||||||
warn!("Error sending event: {}", err)
|
warn!("Error sending event: {}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,16 +37,16 @@ export default function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
authService.onAuthSuccess((data) => {});
|
|
||||||
authService.onAuthError(async () => {
|
authService.onAuthError(async () => {
|
||||||
const preloginResponse = await portalService.prelogin(portalAddress);
|
const preloginResponse = await portalService.prelogin(portalAddress);
|
||||||
// Retry SAML login when auth error occurs
|
if (portalService.isSamlAuth(preloginResponse)) {
|
||||||
authService.emitAuthRequest({
|
// Retry SAML login when auth error occurs
|
||||||
samlBinding: preloginResponse.samlAuthMethod!,
|
await authService.emitAuthRequest({
|
||||||
samlRequest: preloginResponse.samlAuthRequest!,
|
samlBinding: preloginResponse.samlAuthMethod,
|
||||||
});
|
samlRequest: preloginResponse.samlAuthRequest,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
authService.onAuthCancel(() => {});
|
|
||||||
}, [portalAddress]);
|
}, [portalAddress]);
|
||||||
|
|
||||||
function closeNotification() {
|
function closeNotification() {
|
||||||
@ -70,18 +70,33 @@ export default function App() {
|
|||||||
async function handleConnect(e: FormEvent<HTMLFormElement>) {
|
async function handleConnect(e: FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
setProcessing(true);
|
// setProcessing(true);
|
||||||
|
setStatus("processing");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await portalService.prelogin(portalAddress);
|
const response = await portalService.prelogin(portalAddress);
|
||||||
|
|
||||||
if (portalService.isSamlAuth(response)) {
|
if (portalService.isSamlAuth(response)) {
|
||||||
const { samlAuthMethod, samlAuthRequest } = response;
|
const { samlAuthMethod, samlAuthRequest } = response;
|
||||||
|
setStatus("authenticating");
|
||||||
const authData = await authService.samlLogin(
|
const authData = await authService.samlLogin(
|
||||||
samlAuthMethod,
|
samlAuthMethod,
|
||||||
samlAuthRequest
|
samlAuthRequest
|
||||||
);
|
);
|
||||||
console.log("authData", authData);
|
if (!authData) {
|
||||||
|
throw new Error("User cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
const portalConfigResponse = await portalService.fetchConfig(
|
||||||
|
portalAddress,
|
||||||
|
{
|
||||||
|
user: authData.username,
|
||||||
|
"prelogin-cookie": authData.prelogin_cookie,
|
||||||
|
"portal-userauthcookie": authData.portal_userauthcookie,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("portalConfigResponse", portalConfigResponse);
|
||||||
} else if (portalService.isPasswordAuth(response)) {
|
} else if (portalService.isPasswordAuth(response)) {
|
||||||
setPasswordAuthOpen(true);
|
setPasswordAuthOpen(true);
|
||||||
setPasswordAuth({
|
setPasswordAuth({
|
||||||
@ -94,17 +109,17 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setProcessing(false);
|
setStatus("disconnected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
// TODO cancel the request first
|
// TODO cancel the request first
|
||||||
setProcessing(false);
|
setStatus("disconnected");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDisconnect() {
|
async function handleDisconnect() {
|
||||||
setProcessing(true);
|
setStatus("processing");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await vpnService.disconnect();
|
await vpnService.disconnect();
|
||||||
@ -116,18 +131,20 @@ export default function App() {
|
|||||||
message: err.message,
|
message: err.message,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setStatus("disconnected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePasswordAuth({ username, password }: Credentials) {
|
async function handlePasswordAuth({
|
||||||
|
username: user,
|
||||||
|
password: passwd,
|
||||||
|
}: Credentials) {
|
||||||
try {
|
try {
|
||||||
setPasswordAuthenticating(true);
|
setPasswordAuthenticating(true);
|
||||||
const portalConfigResponse = await portalService.fetchConfig({
|
const portalConfigResponse = await portalService.fetchConfig(
|
||||||
portal: portalAddress,
|
portalAddress,
|
||||||
username,
|
{ user, passwd }
|
||||||
password,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const { gateways, preferredGateway, userAuthCookie } =
|
const { gateways, preferredGateway, userAuthCookie } =
|
||||||
portalConfigResponse;
|
portalConfigResponse;
|
||||||
@ -139,13 +156,13 @@ export default function App() {
|
|||||||
|
|
||||||
const token = await gatewayService.login({
|
const token = await gatewayService.login({
|
||||||
gateway: preferredGateway,
|
gateway: preferredGateway,
|
||||||
username,
|
user,
|
||||||
password,
|
passwd,
|
||||||
userAuthCookie,
|
userAuthCookie,
|
||||||
});
|
});
|
||||||
|
|
||||||
await vpnService.connect(preferredGateway.address!, token);
|
await vpnService.connect(preferredGateway.address!, token);
|
||||||
setProcessing(false);
|
// setProcessing(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setNotification({
|
setNotification({
|
||||||
@ -162,7 +179,8 @@ export default function App() {
|
|||||||
function cancelPasswordAuth() {
|
function cancelPasswordAuth() {
|
||||||
setPasswordAuthenticating(false);
|
setPasswordAuthenticating(false);
|
||||||
setPasswordAuthOpen(false);
|
setPasswordAuthOpen(false);
|
||||||
setProcessing(false);
|
// setProcessing(false);
|
||||||
|
setStatus("disconnected");
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box padding={2} paddingTop={3}>
|
<Box padding={2} paddingTop={3}>
|
||||||
@ -193,10 +211,11 @@ export default function App() {
|
|||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{status === "connecting" && (
|
{["processing", "authenticating", "connecting"].includes(status) && (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
disabled={status === "authenticating"}
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
sx={{ textTransform: "none" }}
|
sx={{ textTransform: "none" }}
|
||||||
>
|
>
|
||||||
|
@ -11,6 +11,7 @@ import { BeatLoader } from "react-spinners";
|
|||||||
|
|
||||||
export type Status =
|
export type Status =
|
||||||
| "processing"
|
| "processing"
|
||||||
|
| "authenticating"
|
||||||
| "disconnected"
|
| "disconnected"
|
||||||
| "connecting"
|
| "connecting"
|
||||||
| "connected"
|
| "connected"
|
||||||
@ -18,6 +19,7 @@ export type Status =
|
|||||||
|
|
||||||
export const statusTextMap: Record<Status, string> = {
|
export const statusTextMap: Record<Status, string> = {
|
||||||
processing: "Processing...",
|
processing: "Processing...",
|
||||||
|
authenticating: "Authenticating...",
|
||||||
connected: "Connected",
|
connected: "Connected",
|
||||||
disconnected: "Not Connected",
|
disconnected: "Not Connected",
|
||||||
connecting: "Connecting...",
|
connecting: "Connecting...",
|
||||||
@ -32,13 +34,14 @@ export default function ConnectionStatus(
|
|||||||
const { palette } = theme;
|
const { palette } = theme;
|
||||||
const colorsMap: Record<Status, string> = {
|
const colorsMap: Record<Status, string> = {
|
||||||
processing: palette.info.main,
|
processing: palette.info.main,
|
||||||
|
authenticating: palette.info.main,
|
||||||
connected: palette.success.main,
|
connected: palette.success.main,
|
||||||
disconnected: palette.action.disabled,
|
disconnected: palette.action.disabled,
|
||||||
connecting: palette.info.main,
|
connecting: palette.info.main,
|
||||||
disconnecting: palette.info.main,
|
disconnecting: palette.info.main,
|
||||||
};
|
};
|
||||||
|
|
||||||
const pending = ["processing", "connecting", "disconnecting"].includes(status);
|
const pending = ["processing", "authenticating", "connecting", "disconnecting"].includes(status);
|
||||||
const connected = status === "connected";
|
const connected = status === "connected";
|
||||||
const disconnected = status === "disconnected";
|
const disconnected = status === "disconnected";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Event, emit, listen } from "@tauri-apps/api/event";
|
import { emit, listen } from "@tauri-apps/api/event";
|
||||||
import invokeCommand from "../utils/invokeCommand";
|
import invokeCommand from "../utils/invokeCommand";
|
||||||
|
|
||||||
type AuthData = {
|
type AuthData = {
|
||||||
@ -8,51 +8,35 @@ type AuthData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
private authSuccessCallback: ((data: AuthData) => void) | undefined;
|
|
||||||
private authErrorCallback: (() => void) | undefined;
|
private authErrorCallback: (() => void) | undefined;
|
||||||
private authCancelCallback: (() => void) | undefined;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
await listen("auth-success", (event: Event<AuthData>) => {
|
await listen("auth-error", () => {
|
||||||
this.authSuccessCallback?.(event.payload);
|
|
||||||
});
|
|
||||||
await listen("auth-error", (event) => {
|
|
||||||
this.authErrorCallback?.();
|
this.authErrorCallback?.();
|
||||||
});
|
});
|
||||||
await listen("auth-cancel", (event) => {
|
|
||||||
this.authCancelCallback?.();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAuthSuccess(callback: (data: AuthData) => void) {
|
|
||||||
this.authSuccessCallback = callback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAuthError(callback: () => void) {
|
onAuthError(callback: () => void) {
|
||||||
this.authErrorCallback = callback;
|
this.authErrorCallback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAuthCancel(callback: () => void) {
|
|
||||||
this.authCancelCallback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
// binding: "POST" | "REDIRECT"
|
// binding: "POST" | "REDIRECT"
|
||||||
async samlLogin(binding: string, request: string) {
|
async samlLogin(binding: string, request: string) {
|
||||||
return invokeCommand<AuthData>("saml_login", { binding, request });
|
return invokeCommand<AuthData>("saml_login", { binding, request });
|
||||||
}
|
}
|
||||||
|
|
||||||
emitAuthRequest({
|
async emitAuthRequest({
|
||||||
samlBinding,
|
samlBinding,
|
||||||
samlRequest,
|
samlRequest,
|
||||||
}: {
|
}: {
|
||||||
samlBinding: string;
|
samlBinding: string;
|
||||||
samlRequest: string;
|
samlRequest: string;
|
||||||
}) {
|
}) {
|
||||||
emit("auth-request", { samlBinding, samlRequest });
|
await emit("auth-request", { samlBinding, samlRequest });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,14 +5,14 @@ import { Gateway } from "./types";
|
|||||||
|
|
||||||
type LoginParams = {
|
type LoginParams = {
|
||||||
gateway: Gateway;
|
gateway: Gateway;
|
||||||
username: string;
|
user: string;
|
||||||
password: string;
|
passwd: string;
|
||||||
userAuthCookie: Maybe<string>;
|
userAuthCookie: Maybe<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
class GatewayService {
|
class GatewayService {
|
||||||
async login(params: LoginParams) {
|
async login(params: LoginParams) {
|
||||||
const { gateway, username, password, userAuthCookie } = params;
|
const { gateway, user, passwd, userAuthCookie } = params;
|
||||||
if (!gateway.address) {
|
if (!gateway.address) {
|
||||||
throw new Error("Gateway address is required");
|
throw new Error("Gateway address is required");
|
||||||
}
|
}
|
||||||
@ -37,8 +37,8 @@ class GatewayService {
|
|||||||
clientos: "Linux",
|
clientos: "Linux",
|
||||||
"os-version": "Linux",
|
"os-version": "Linux",
|
||||||
server: gateway.address,
|
server: gateway.address,
|
||||||
user: username,
|
user,
|
||||||
passwd: password,
|
passwd,
|
||||||
"portal-userauthcookie": userAuthCookie ?? "",
|
"portal-userauthcookie": userAuthCookie ?? "",
|
||||||
"portal-prelogonuserauthcookie": "",
|
"portal-prelogonuserauthcookie": "",
|
||||||
"prelogin-cookie": "",
|
"prelogin-cookie": "",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Body, ResponseType, fetch } from "@tauri-apps/api/http";
|
import { Body, ResponseType, fetch } from "@tauri-apps/api/http";
|
||||||
import { Maybe, MaybeProperties } from "../types";
|
import { Maybe, MaybeProperties } from "../types";
|
||||||
import { parseXml } from "../utils/parseXml";
|
import { parseXml } from "../utils/parseXml";
|
||||||
import authService from "./authService";
|
|
||||||
import { Gateway } from "./types";
|
import { Gateway } from "./types";
|
||||||
|
|
||||||
type SamlPreloginResponse = {
|
type SamlPreloginResponse = {
|
||||||
@ -30,6 +29,19 @@ type ConfigResponse = {
|
|||||||
gateways: Gateway[];
|
gateways: Gateway[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// user: username,
|
||||||
|
// passwd: password,
|
||||||
|
// "prelogin-cookie": "",
|
||||||
|
// "portal-userauthcookie": "",
|
||||||
|
// "portal-prelogonuserauthcookie": "",
|
||||||
|
type PortalConfigParams = {
|
||||||
|
user: string;
|
||||||
|
passwd?: string | null;
|
||||||
|
"prelogin-cookie"?: string | null;
|
||||||
|
"portal-userauthcookie"?: string | null;
|
||||||
|
"portal-prelogonuserauthcookie"?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
class PortalService {
|
class PortalService {
|
||||||
async prelogin(portal: string) {
|
async prelogin(portal: string) {
|
||||||
const preloginUrl = `https://${portal}/global-protect/prelogin.esp`;
|
const preloginUrl = `https://${portal}/global-protect/prelogin.esp`;
|
||||||
@ -84,40 +96,42 @@ class PortalService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchConfig({
|
async fetchConfig(portal: string, params: PortalConfigParams) {
|
||||||
portal,
|
const {
|
||||||
username,
|
user,
|
||||||
password,
|
passwd,
|
||||||
}: {
|
"prelogin-cookie": preloginCookie,
|
||||||
portal: string;
|
"portal-userauthcookie": portalUserAuthCookie,
|
||||||
username: string;
|
"portal-prelogonuserauthcookie": portalPrelogonUserAuthCookie,
|
||||||
password: string;
|
} = params;
|
||||||
}) {
|
|
||||||
const configUrl = `https://${portal}/global-protect/getconfig.esp`;
|
const configUrl = `https://${portal}/global-protect/getconfig.esp`;
|
||||||
|
const body = Body.form({
|
||||||
|
prot: "https:",
|
||||||
|
inputStr: "",
|
||||||
|
jnlpReady: "jnlpReady",
|
||||||
|
computer: "Linux", // TODO
|
||||||
|
clientos: "Linux",
|
||||||
|
ok: "Login",
|
||||||
|
direct: "yes",
|
||||||
|
clientVer: "4100",
|
||||||
|
"os-version": "Linux",
|
||||||
|
"ipv6-support": "yes",
|
||||||
|
server: portal,
|
||||||
|
user,
|
||||||
|
passwd: passwd || "",
|
||||||
|
"prelogin-cookie": preloginCookie || "",
|
||||||
|
"portal-userauthcookie": portalUserAuthCookie || "",
|
||||||
|
"portal-prelogonuserauthcookie": portalPrelogonUserAuthCookie || "",
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch<string>(configUrl, {
|
const response = await fetch<string>(configUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": "PAN GlobalProtect",
|
"User-Agent": "PAN GlobalProtect",
|
||||||
},
|
},
|
||||||
responseType: ResponseType.Text,
|
responseType: ResponseType.Text,
|
||||||
body: Body.form({
|
body,
|
||||||
prot: "https:",
|
|
||||||
inputStr: "",
|
|
||||||
jnlpReady: "jnlpReady",
|
|
||||||
computer: "Linux", // TODO
|
|
||||||
clientos: "Linux",
|
|
||||||
ok: "Login",
|
|
||||||
direct: "yes",
|
|
||||||
clientVer: "4100",
|
|
||||||
"os-version": "Linux",
|
|
||||||
"ipv6-support": "yes",
|
|
||||||
server: portal,
|
|
||||||
user: username,
|
|
||||||
passwd: password,
|
|
||||||
"portal-userauthcookie": "",
|
|
||||||
"portal-prelogonuserauthcookie": "",
|
|
||||||
"prelogin-cookie": "",
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
Loading…
Reference in New Issue
Block a user