refactor: get portal config

This commit is contained in:
Kevin Yue 2023-05-29 09:26:03 +08:00
parent c74ce52c2d
commit d975f981cc
6 changed files with 115 additions and 94 deletions

View File

@ -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)
} }
} }

View File

@ -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);
if (portalService.isSamlAuth(preloginResponse)) {
// Retry SAML login when auth error occurs // Retry SAML login when auth error occurs
authService.emitAuthRequest({ await authService.emitAuthRequest({
samlBinding: preloginResponse.samlAuthMethod!, samlBinding: preloginResponse.samlAuthMethod,
samlRequest: preloginResponse.samlAuthRequest!, 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" }}
> >

View File

@ -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";

View File

@ -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 });
} }
} }

View File

@ -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": "",

View File

@ -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,23 +96,17 @@ 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 response = await fetch<string>(configUrl, { const body = Body.form({
method: "POST",
headers: {
"User-Agent": "PAN GlobalProtect",
},
responseType: ResponseType.Text,
body: Body.form({
prot: "https:", prot: "https:",
inputStr: "", inputStr: "",
jnlpReady: "jnlpReady", jnlpReady: "jnlpReady",
@ -112,12 +118,20 @@ class PortalService {
"os-version": "Linux", "os-version": "Linux",
"ipv6-support": "yes", "ipv6-support": "yes",
server: portal, server: portal,
user: username, user,
passwd: password, passwd: passwd || "",
"portal-userauthcookie": "", "prelogin-cookie": preloginCookie || "",
"portal-prelogonuserauthcookie": "", "portal-userauthcookie": portalUserAuthCookie || "",
"prelogin-cookie": "", "portal-prelogonuserauthcookie": portalPrelogonUserAuthCookie || "",
}), });
const response = await fetch<string>(configUrl, {
method: "POST",
headers: {
"User-Agent": "PAN GlobalProtect",
},
responseType: ResponseType.Text,
body,
}); });
if (!response.ok) { if (!response.ok) {