mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	refactor: get portal config
This commit is contained in:
		| @@ -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) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user