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<()> {
window.with_webview(move |wv| {
let wv = wv.inner();
let event_tx = event_tx.clone();
let event_tx_clone = event_tx.clone();
if clear_cookies {
clear_webview_cookies(&wv);
@ -146,25 +146,22 @@ fn setup_webview(
// Empty URI indicates that an error occurred
if uri.is_empty() {
warn!("Empty URI loaded");
if let Err(err) = event_tx.blocking_send(AuthEvent::Error(AuthError::TokenInvalid))
{
warn!("Error sending event: {}", err);
}
send_auth_error(&event_tx_clone, AuthError::TokenInvalid);
return;
}
// TODO, redact URI
debug!("Loaded URI: {}", uri);
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 {
warn!("No main_resource");
}
});
wv.connect_load_failed(|_wv, event, err_msg, err| {
warn!("Load failed: {:?}, {}, {:?}", event, err_msg, err);
wv.connect_load_failed(move |_wv, event, _uri, err| {
warn!("Load failed: {:?}, {:?}", event, err);
send_auth_error(&event_tx, AuthError::TokenInvalid);
false
});
})
@ -175,9 +172,7 @@ fn setup_window(window: &Window, event_tx: mpsc::Sender<AuthEvent>) -> EventHand
window.on_window_event(move |event| {
if let CloseRequested { api, .. } = event {
api.prevent_close();
if let Err(err) = event_tx_clone.blocking_send(AuthEvent::Cancel) {
warn!("Error sending event: {}", err)
}
send_auth_event(&event_tx_clone, AuthEvent::Cancel);
}
});
@ -335,9 +330,7 @@ fn parse_auth_data(main_res: &WebResource, event_tx: mpsc::Sender<AuthEvent>) {
}
Err(err) => {
debug!("Error reading auth data from HTML: {:?}", err);
if let Err(err) = event_tx.blocking_send(AuthEvent::Error(err)) {
warn!("Error sending event: {}", err)
}
send_auth_error(&event_tx, 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) {
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)
}
}

View File

@ -37,16 +37,16 @@ export default function App() {
}, []);
useEffect(() => {
authService.onAuthSuccess((data) => {});
authService.onAuthError(async () => {
const preloginResponse = await portalService.prelogin(portalAddress);
if (portalService.isSamlAuth(preloginResponse)) {
// Retry SAML login when auth error occurs
authService.emitAuthRequest({
samlBinding: preloginResponse.samlAuthMethod!,
samlRequest: preloginResponse.samlAuthRequest!,
await authService.emitAuthRequest({
samlBinding: preloginResponse.samlAuthMethod,
samlRequest: preloginResponse.samlAuthRequest,
});
}
});
authService.onAuthCancel(() => {});
}, [portalAddress]);
function closeNotification() {
@ -70,18 +70,33 @@ export default function App() {
async function handleConnect(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setProcessing(true);
// setProcessing(true);
setStatus("processing");
try {
const response = await portalService.prelogin(portalAddress);
if (portalService.isSamlAuth(response)) {
const { samlAuthMethod, samlAuthRequest } = response;
setStatus("authenticating");
const authData = await authService.samlLogin(
samlAuthMethod,
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)) {
setPasswordAuthOpen(true);
setPasswordAuth({
@ -94,17 +109,17 @@ export default function App() {
}
} catch (e) {
console.error(e);
setProcessing(false);
setStatus("disconnected");
}
}
function handleCancel() {
// TODO cancel the request first
setProcessing(false);
setStatus("disconnected");
}
async function handleDisconnect() {
setProcessing(true);
setStatus("processing");
try {
await vpnService.disconnect();
@ -116,18 +131,20 @@ export default function App() {
message: err.message,
});
} finally {
setProcessing(false);
setStatus("disconnected");
}
}
async function handlePasswordAuth({ username, password }: Credentials) {
async function handlePasswordAuth({
username: user,
password: passwd,
}: Credentials) {
try {
setPasswordAuthenticating(true);
const portalConfigResponse = await portalService.fetchConfig({
portal: portalAddress,
username,
password,
});
const portalConfigResponse = await portalService.fetchConfig(
portalAddress,
{ user, passwd }
);
const { gateways, preferredGateway, userAuthCookie } =
portalConfigResponse;
@ -139,13 +156,13 @@ export default function App() {
const token = await gatewayService.login({
gateway: preferredGateway,
username,
password,
user,
passwd,
userAuthCookie,
});
await vpnService.connect(preferredGateway.address!, token);
setProcessing(false);
// setProcessing(false);
} catch (err: any) {
console.error(err);
setNotification({
@ -162,7 +179,8 @@ export default function App() {
function cancelPasswordAuth() {
setPasswordAuthenticating(false);
setPasswordAuthOpen(false);
setProcessing(false);
// setProcessing(false);
setStatus("disconnected");
}
return (
<Box padding={2} paddingTop={3}>
@ -193,10 +211,11 @@ export default function App() {
Connect
</Button>
)}
{status === "connecting" && (
{["processing", "authenticating", "connecting"].includes(status) && (
<Button
variant="outlined"
fullWidth
disabled={status === "authenticating"}
onClick={handleCancel}
sx={{ textTransform: "none" }}
>

View File

@ -11,6 +11,7 @@ import { BeatLoader } from "react-spinners";
export type Status =
| "processing"
| "authenticating"
| "disconnected"
| "connecting"
| "connected"
@ -18,6 +19,7 @@ export type Status =
export const statusTextMap: Record<Status, string> = {
processing: "Processing...",
authenticating: "Authenticating...",
connected: "Connected",
disconnected: "Not Connected",
connecting: "Connecting...",
@ -32,13 +34,14 @@ export default function ConnectionStatus(
const { palette } = theme;
const colorsMap: Record<Status, string> = {
processing: palette.info.main,
authenticating: palette.info.main,
connected: palette.success.main,
disconnected: palette.action.disabled,
connecting: 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 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";
type AuthData = {
@ -8,51 +8,35 @@ type AuthData = {
};
class AuthService {
private authSuccessCallback: ((data: AuthData) => void) | undefined;
private authErrorCallback: (() => void) | undefined;
private authCancelCallback: (() => void) | undefined;
constructor() {
this.init();
}
private async init() {
await listen("auth-success", (event: Event<AuthData>) => {
this.authSuccessCallback?.(event.payload);
});
await listen("auth-error", (event) => {
await listen("auth-error", () => {
this.authErrorCallback?.();
});
await listen("auth-cancel", (event) => {
this.authCancelCallback?.();
});
}
onAuthSuccess(callback: (data: AuthData) => void) {
this.authSuccessCallback = callback;
}
onAuthError(callback: () => void) {
this.authErrorCallback = callback;
}
onAuthCancel(callback: () => void) {
this.authCancelCallback = callback;
}
// binding: "POST" | "REDIRECT"
async samlLogin(binding: string, request: string) {
return invokeCommand<AuthData>("saml_login", { binding, request });
}
emitAuthRequest({
async emitAuthRequest({
samlBinding,
samlRequest,
}: {
samlBinding: 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 = {
gateway: Gateway;
username: string;
password: string;
user: string;
passwd: string;
userAuthCookie: Maybe<string>;
};
class GatewayService {
async login(params: LoginParams) {
const { gateway, username, password, userAuthCookie } = params;
const { gateway, user, passwd, userAuthCookie } = params;
if (!gateway.address) {
throw new Error("Gateway address is required");
}
@ -37,8 +37,8 @@ class GatewayService {
clientos: "Linux",
"os-version": "Linux",
server: gateway.address,
user: username,
passwd: password,
user,
passwd,
"portal-userauthcookie": userAuthCookie ?? "",
"portal-prelogonuserauthcookie": "",
"prelogin-cookie": "",

View File

@ -1,7 +1,6 @@
import { Body, ResponseType, fetch } from "@tauri-apps/api/http";
import { Maybe, MaybeProperties } from "../types";
import { parseXml } from "../utils/parseXml";
import authService from "./authService";
import { Gateway } from "./types";
type SamlPreloginResponse = {
@ -30,6 +29,19 @@ type ConfigResponse = {
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 {
async prelogin(portal: string) {
const preloginUrl = `https://${portal}/global-protect/prelogin.esp`;
@ -84,23 +96,17 @@ class PortalService {
return false;
}
async fetchConfig({
portal,
username,
password,
}: {
portal: string;
username: string;
password: string;
}) {
async fetchConfig(portal: string, params: PortalConfigParams) {
const {
user,
passwd,
"prelogin-cookie": preloginCookie,
"portal-userauthcookie": portalUserAuthCookie,
"portal-prelogonuserauthcookie": portalPrelogonUserAuthCookie,
} = params;
const configUrl = `https://${portal}/global-protect/getconfig.esp`;
const response = await fetch<string>(configUrl, {
method: "POST",
headers: {
"User-Agent": "PAN GlobalProtect",
},
responseType: ResponseType.Text,
body: Body.form({
const body = Body.form({
prot: "https:",
inputStr: "",
jnlpReady: "jnlpReady",
@ -112,12 +118,20 @@ class PortalService {
"os-version": "Linux",
"ipv6-support": "yes",
server: portal,
user: username,
passwd: password,
"portal-userauthcookie": "",
"portal-prelogonuserauthcookie": "",
"prelogin-cookie": "",
}),
user,
passwd: passwd || "",
"prelogin-cookie": preloginCookie || "",
"portal-userauthcookie": portalUserAuthCookie || "",
"portal-prelogonuserauthcookie": portalPrelogonUserAuthCookie || "",
});
const response = await fetch<string>(configUrl, {
method: "POST",
headers: {
"User-Agent": "PAN GlobalProtect",
},
responseType: ResponseType.Text,
body,
});
if (!response.ok) {