refactor: rewrite
24
gpgui/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
27
gpgui/docs/gateway-login-response.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<jnlp>
|
||||
<application-desc>
|
||||
<argument>(null)</argument>
|
||||
<argument>44d9988f3a3b5a247d359b1d39229add</argument>
|
||||
<argument>1cb389d44ec35e98665211761b65a049ef7ba77e</argument>
|
||||
<argument>GP-Gateway-N</argument>
|
||||
<argument>user</argument>
|
||||
<argument>AD_Authentication</argument>
|
||||
<argument>vsys1</argument>
|
||||
<argument>vpn.example.com</argument>
|
||||
<argument>(null)</argument>
|
||||
<argument></argument>
|
||||
<argument></argument>
|
||||
<argument></argument>
|
||||
<argument>tunnel</argument>
|
||||
<argument>-1</argument>
|
||||
<argument>4100</argument>
|
||||
<argument></argument>
|
||||
<argument>xxxxxxxxxxxxxxxx</argument>
|
||||
<argument>xxxxxxxxxxxxxxxx</argument>
|
||||
<argument></argument>
|
||||
<argument>4</argument>
|
||||
<argument>unknown</argument>
|
||||
<argument></argument>
|
||||
</application-desc>
|
||||
</jnlp>
|
212
gpgui/docs/portal-config-response.xml
Normal file
@@ -0,0 +1,212 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<policy>
|
||||
<portal-name>vpn.example.com</portal-name>
|
||||
<portal-config-version>4100</portal-config-version>
|
||||
<version>6.0.1-19 </version>
|
||||
<client-role>global-protect-full</client-role>
|
||||
<agent-user-override-key>****</agent-user-override-key>
|
||||
<root-ca>
|
||||
<entry name="DigiCert Global Root CA">
|
||||
<cert>
|
||||
-----BEGIN CERTIFICATE-----
|
||||
-----END CERTIFICATE-----
|
||||
</cert>
|
||||
<install-in-cert-store>yes</install-in-cert-store>
|
||||
</entry>
|
||||
<entry name="Thawte RSA CA 2018">
|
||||
<cert>
|
||||
-----BEGIN CERTIFICATE-----
|
||||
-----END CERTIFICATE-----
|
||||
</cert>
|
||||
<install-in-cert-store>yes</install-in-cert-store>
|
||||
</entry>
|
||||
<entry name="Temp_VPN_Root_Certificate">
|
||||
<cert>
|
||||
-----BEGIN CERTIFICATE-----
|
||||
-----END CERTIFICATE-----
|
||||
</cert>
|
||||
<install-in-cert-store>no</install-in-cert-store>
|
||||
</entry>
|
||||
</root-ca>
|
||||
<connect-method>on-demand</connect-method>
|
||||
<pre-logon-then-on-demand>yes</pre-logon-then-on-demand>
|
||||
<refresh-config>yes</refresh-config>
|
||||
<refresh-config-interval>24</refresh-config-interval>
|
||||
<authentication-modifier>
|
||||
<none />
|
||||
</authentication-modifier>
|
||||
<authentication-override>
|
||||
<accept-cookie>yes</accept-cookie>
|
||||
<generate-cookie>yes</generate-cookie>
|
||||
<cookie-lifetime>
|
||||
<lifetime-in-days>365</lifetime-in-days>
|
||||
</cookie-lifetime>
|
||||
<cookie-encrypt-decrypt-cert>vpn.example.com</cookie-encrypt-decrypt-cert>
|
||||
</authentication-override>
|
||||
<use-sso>yes</use-sso>
|
||||
<ip-address></ip-address>
|
||||
<host></host>
|
||||
<gateways>
|
||||
<cutoff-time>5</cutoff-time>
|
||||
<external>
|
||||
<list>
|
||||
<entry name="xxx.xxx.xxx.xxx">
|
||||
<priority-rule>
|
||||
<entry name="Any">
|
||||
<priority>1</priority>
|
||||
</entry>
|
||||
</priority-rule>
|
||||
<priority>1</priority>
|
||||
<description>vpn_gateway</description>
|
||||
</entry>
|
||||
</list>
|
||||
</external>
|
||||
</gateways>
|
||||
<gateways-v6>
|
||||
<cutoff-time>5</cutoff-time>
|
||||
<external>
|
||||
<list>
|
||||
<entry name="vpn_gateway">
|
||||
<ipv4>xxx.xxx.xxx.xxx</ipv4>
|
||||
<priority-rule>
|
||||
<entry name="Any">
|
||||
<priority>1</priority>
|
||||
</entry>
|
||||
</priority-rule>
|
||||
<priority>1</priority>
|
||||
</entry>
|
||||
</list>
|
||||
</external>
|
||||
</gateways-v6>
|
||||
<agent-ui>
|
||||
<can-save-password>yes</can-save-password>
|
||||
<passcode></passcode>
|
||||
<uninstall-passwd></uninstall-passwd>
|
||||
<agent-user-override-timeout>0</agent-user-override-timeout>
|
||||
<max-agent-user-overrides>0</max-agent-user-overrides>
|
||||
<help-page></help-page>
|
||||
<help-page-2></help-page-2>
|
||||
<welcome-page>
|
||||
<display>no</display>
|
||||
<page></page>
|
||||
</welcome-page>
|
||||
<agent-user-override>allowed</agent-user-override>
|
||||
<enable-advanced-view>yes</enable-advanced-view>
|
||||
<enable-do-not-display-this-welcome-page-again>yes</enable-do-not-display-this-welcome-page-again>
|
||||
<can-change-portal>yes</can-change-portal>
|
||||
<show-agent-icon>yes</show-agent-icon>
|
||||
<password-expiry-message></password-expiry-message>
|
||||
<init-panel>no</init-panel>
|
||||
<user-input-on-top>no</user-input-on-top>
|
||||
</agent-ui>
|
||||
<hip-collection>
|
||||
<hip-report-interval>3600</hip-report-interval>
|
||||
<max-wait-time>20</max-wait-time>
|
||||
<collect-hip-data>yes</collect-hip-data>
|
||||
<default>
|
||||
<category>
|
||||
<member>antivirus</member>
|
||||
<member>anti-spyware</member>
|
||||
<member>host-info</member>
|
||||
<member>data-loss-prevention</member>
|
||||
<member>patch-management</member>
|
||||
<member>firewall</member>
|
||||
<member>anti-malware</member>
|
||||
<member>disk-backup</member>
|
||||
<member>disk-encryption</member>
|
||||
</category>
|
||||
</default>
|
||||
</hip-collection>
|
||||
<agent-config>
|
||||
<save-user-credentials>1</save-user-credentials>
|
||||
<portal-2fa>no</portal-2fa>
|
||||
<internal-gateway-2fa>no</internal-gateway-2fa>
|
||||
<auto-discovery-external-gateway-2fa>no</auto-discovery-external-gateway-2fa>
|
||||
<manual-only-gateway-2fa>no</manual-only-gateway-2fa>
|
||||
<disconnect-reasons></disconnect-reasons>
|
||||
<uninstall>allowed</uninstall>
|
||||
<client-upgrade>prompt</client-upgrade>
|
||||
<enable-signout>yes</enable-signout>
|
||||
<use-sso-pin>no</use-sso-pin>
|
||||
<use-sso-macos>no</use-sso-macos>
|
||||
<logout-remove-sso>yes</logout-remove-sso>
|
||||
<krb-auth-fail-fallback>yes</krb-auth-fail-fallback>
|
||||
<default-browser>no</default-browser>
|
||||
<retry-tunnel>30</retry-tunnel>
|
||||
<retry-timeout>5</retry-timeout>
|
||||
<traffic-enforcement>no</traffic-enforcement>
|
||||
<enforce-globalprotect>no</enforce-globalprotect>
|
||||
<enforcer-exception-list />
|
||||
<enforcer-exception-list-domain />
|
||||
<captive-portal-exception-timeout>0</captive-portal-exception-timeout>
|
||||
<captive-portal-login-url></captive-portal-login-url>
|
||||
<traffic-blocking-notification-delay>15</traffic-blocking-notification-delay>
|
||||
<display-traffic-blocking-notification-msg>yes</display-traffic-blocking-notification-msg>
|
||||
<traffic-blocking-notification-msg><div style="font-family:'Helvetica
|
||||
Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size:
|
||||
30px;">Notice</h1><p style="margin: 0;font-size: 15px;
|
||||
line-height: 1.2em;">To access the network, you must first connect to
|
||||
GlobalProtect.</p></div></traffic-blocking-notification-msg>
|
||||
<allow-traffic-blocking-notification-dismissal>yes</allow-traffic-blocking-notification-dismissal>
|
||||
<display-captive-portal-detection-msg>no</display-captive-portal-detection-msg>
|
||||
<captive-portal-detection-msg><div style="font-family:'Helvetica
|
||||
Neue';"><h1 style="color:red;text-align:center; margin: 0; font-size:
|
||||
30px;">Captive Portal Detected</h1><p style="margin: 0; font-size:
|
||||
15px; line-height: 1.2em;">GlobalProtect has temporarily permitted network
|
||||
access for you to connect to the Internet. Follow instructions from your internet
|
||||
provider.</p><p style="margin: 0; font-size: 15px; line-height:
|
||||
1.2em;">If you let the connection time out, open GlobalProtect and click Connect
|
||||
to try again.</p></div></captive-portal-detection-msg>
|
||||
<captive-portal-notification-delay>5</captive-portal-notification-delay>
|
||||
<certificate-store-lookup>user-and-machine</certificate-store-lookup>
|
||||
<scep-certificate-renewal-period>7</scep-certificate-renewal-period>
|
||||
<ext-key-usage-oid-for-client-cert></ext-key-usage-oid-for-client-cert>
|
||||
<retain-connection-smartcard-removal>yes</retain-connection-smartcard-removal>
|
||||
<user-accept-terms-before-creating-tunnel>no</user-accept-terms-before-creating-tunnel>
|
||||
<rediscover-network>yes</rediscover-network>
|
||||
<resubmit-host-info>yes</resubmit-host-info>
|
||||
<can-continue-if-portal-cert-invalid>yes</can-continue-if-portal-cert-invalid>
|
||||
<user-switch-tunnel-rename-timeout>0</user-switch-tunnel-rename-timeout>
|
||||
<pre-logon-tunnel-rename-timeout>0</pre-logon-tunnel-rename-timeout>
|
||||
<preserve-tunnel-upon-user-logoff-timeout>0</preserve-tunnel-upon-user-logoff-timeout>
|
||||
<ipsec-failover-ssl>0</ipsec-failover-ssl>
|
||||
<display-tunnel-fallback-notification>yes</display-tunnel-fallback-notification>
|
||||
<ssl-only-selection>0</ssl-only-selection>
|
||||
<tunnel-mtu>1400</tunnel-mtu>
|
||||
<max-internal-gateway-connection-attempts>0</max-internal-gateway-connection-attempts>
|
||||
<adv-internal-host-detection>no</adv-internal-host-detection>
|
||||
<portal-timeout>30</portal-timeout>
|
||||
<connect-timeout>60</connect-timeout>
|
||||
<receive-timeout>30</receive-timeout>
|
||||
<split-tunnel-option>network-traffic</split-tunnel-option>
|
||||
<enforce-dns>yes</enforce-dns>
|
||||
<append-local-search-domain>no</append-local-search-domain>
|
||||
<flush-dns>no</flush-dns>
|
||||
<auto-proxy-pac></auto-proxy-pac>
|
||||
<proxy-multiple-autodetect>no</proxy-multiple-autodetect>
|
||||
<use-proxy>yes</use-proxy>
|
||||
<wsc-autodetect>yes</wsc-autodetect>
|
||||
<mfa-enabled>no</mfa-enabled>
|
||||
<mfa-listening-port>4501</mfa-listening-port>
|
||||
<mfa-trusted-host-list />
|
||||
<mfa-notification-msg>You have attempted to access a protected resource that requires
|
||||
additional authentication. Proceed to authenticate at</mfa-notification-msg>
|
||||
<mfa-prompt-suppress-time>0</mfa-prompt-suppress-time>
|
||||
<ipv6-preferred>yes</ipv6-preferred>
|
||||
<change-password-message></change-password-message>
|
||||
<log-gateway>no</log-gateway>
|
||||
<cdl-log>no</cdl-log>
|
||||
<dem-notification>yes</dem-notification>
|
||||
<diagnostic-servers />
|
||||
<dem-agent>not-install</dem-agent>
|
||||
<quarantine-add-message>Access to the network from this device has been restricted as per
|
||||
your organization's security policy. Please contact your IT Administrator.</quarantine-add-message>
|
||||
<quarantine-remove-message>Access to the network from this device has been restored as per
|
||||
your organization's security policy.</quarantine-remove-message>
|
||||
|
||||
</agent-config>
|
||||
<user-email>user@example.com</user-email>
|
||||
<portal-userauthcookie>xxxxxx</portal-userauthcookie>
|
||||
<portal-prelogonuserauthcookie>xxxxxx</portal-prelogonuserauthcookie>
|
||||
<config-digest>2d8e997765a2f59cbf80284b2f2fbd38</config-digest>
|
||||
</policy>
|
13
gpgui/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
30
gpgui/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "gpgui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/lab": "5.0.0-alpha.125",
|
||||
"@mui/material": "^5.11.11",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-spinners": "^0.13.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.2.3",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.1.0"
|
||||
}
|
||||
}
|
1391
gpgui/pnpm-lock.yaml
generated
Normal file
1
gpgui/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
3
gpgui/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
29
gpgui/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
default-run = "app"
|
||||
edition = "2021"
|
||||
rust-version = "1.59"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.2.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2.4", features = ["http-all"] }
|
||||
common = { path = "../../common" }
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
default = [ "custom-protocol" ]
|
||||
# this feature is used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
3
gpgui/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
BIN
gpgui/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
gpgui/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
gpgui/src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
gpgui/src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
gpgui/src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
gpgui/src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
gpgui/src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
gpgui/src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
gpgui/src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
gpgui/src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
gpgui/src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
gpgui/src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
gpgui/src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
gpgui/src-tauri/icons/icon.icns
Normal file
BIN
gpgui/src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
gpgui/src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 49 KiB |
65
gpgui/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use common::{Client, ServerApiError, VpnStatus};
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use tauri::{Manager, State};
|
||||
|
||||
#[tauri::command]
|
||||
async fn vpn_status<'a>(client: State<'a, Arc<Client>>) -> Result<VpnStatus, ServerApiError> {
|
||||
client.status().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn vpn_connect<'a>(
|
||||
server: String,
|
||||
cookie: String,
|
||||
client: State<'a, Arc<Client>>,
|
||||
) -> Result<(), ServerApiError> {
|
||||
client.connect(server, cookie).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn vpn_disconnect<'a>(client: State<'a, Arc<Client>>) -> Result<(), ServerApiError> {
|
||||
client.disconnect().await
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct StatusPayload {
|
||||
status: VpnStatus,
|
||||
}
|
||||
|
||||
fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Arc::new(Client::default());
|
||||
let client_clone = client.clone();
|
||||
let app_handle = app.handle();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = client_clone.subscribe_status(move |status| {
|
||||
let payload = StatusPayload { status };
|
||||
if let Err(err) = app_handle.emit_all("vpn-status-received", payload) {
|
||||
println!("Error emmiting event: {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
let _ = client_clone.run().await;
|
||||
});
|
||||
|
||||
app.manage(client);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.setup(setup)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
vpn_status,
|
||||
vpn_connect,
|
||||
vpn_disconnect
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
70
gpgui/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"build": {
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devPath": "http://localhost:5173",
|
||||
"distDir": "../dist"
|
||||
},
|
||||
"package": {
|
||||
"productName": "gpgui",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"http": {
|
||||
"all": true,
|
||||
"request": true,
|
||||
"scope": ["https://**"]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "com.tauri.dev",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"height": 360,
|
||||
"resizable": false,
|
||||
"title": "GlobalProtect",
|
||||
"width": 260
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
10
gpgui/src/App.css
Normal file
@@ -0,0 +1,10 @@
|
||||
html {
|
||||
height: 100%;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
background: #f6f6f6 !important;
|
||||
}
|
202
gpgui/src/App.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import { Box, TextField } from "@mui/material";
|
||||
import Button from "@mui/material/Button";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
|
||||
import "./App.css";
|
||||
import ConnectionStatus, { Status } from "./components/ConnectionStatus";
|
||||
import Notification, { NotificationConfig } from "./components/Notification";
|
||||
import PasswordAuth, {
|
||||
Credentials,
|
||||
PasswordAuthData,
|
||||
} from "./components/PasswordAuth";
|
||||
import gatewayService from "./services/gatewayService";
|
||||
import portalService from "./services/portalService";
|
||||
import vpnService from "./services/vpnService";
|
||||
|
||||
export default function App() {
|
||||
const [portalAddress, setPortalAddress] = useState("220.191.185.154");
|
||||
const [status, setStatus] = useState<Status>("disconnected");
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [passwordAuthOpen, setPasswordAuthOpen] = useState(false);
|
||||
const [passwordAuthenticating, setPasswordAuthenticating] = useState(false); ``
|
||||
const [passwordAuth, setPasswordAuth] = useState<PasswordAuthData>();
|
||||
const [notification, setNotification] = useState<NotificationConfig>({
|
||||
open: false,
|
||||
message: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return vpnService.onStatusChanged((latestStatus) => {
|
||||
console.log("status changed", latestStatus);
|
||||
setStatus(latestStatus);
|
||||
if (latestStatus === "connected") {
|
||||
clearOverlays();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
function closeNotification() {
|
||||
setNotification((notification) => ({
|
||||
...notification,
|
||||
open: false,
|
||||
}));
|
||||
}
|
||||
|
||||
function clearOverlays() {
|
||||
closeNotification()
|
||||
setPasswordAuthenticating(false)
|
||||
setPasswordAuthOpen(false)
|
||||
}
|
||||
|
||||
function handlePortalChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
const { value } = e.target;
|
||||
setPortalAddress(value.trim());
|
||||
}
|
||||
|
||||
async function handleConnect() {
|
||||
setProcessing(true);
|
||||
// setStatus("connecting");
|
||||
|
||||
try {
|
||||
const response = await portalService.prelogin(portalAddress);
|
||||
|
||||
if (portalService.isSamlAuth(response)) {
|
||||
// TODO SAML login
|
||||
} else if (portalService.isPasswordAuth(response)) {
|
||||
setPasswordAuthOpen(true);
|
||||
setPasswordAuth({
|
||||
authMessage: response.authMessage,
|
||||
labelPassword: response.labelPassword,
|
||||
labelUsername: response.labelUsername,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unsupported portal login method");
|
||||
}
|
||||
} catch (e) {
|
||||
setProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
// TODO cancel the request first
|
||||
setProcessing(false)
|
||||
}
|
||||
|
||||
async function handleDisconnect() {
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
await vpnService.disconnect();
|
||||
} catch (err: any) {
|
||||
setNotification({
|
||||
open: true,
|
||||
type: "error",
|
||||
title: "Failed to disconnect",
|
||||
message: err.message,
|
||||
});
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePasswordAuth({ username, password }: Credentials) {
|
||||
try {
|
||||
setPasswordAuthenticating(true);
|
||||
const portalConfigResponse = await portalService.fetchConfig({
|
||||
portal: portalAddress,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
const { gateways, preferredGateway, userAuthCookie } =
|
||||
portalConfigResponse;
|
||||
|
||||
if (gateways.length === 0) {
|
||||
// TODO handle no gateways, treat the portal as a gateway
|
||||
throw new Error("No gateways found");
|
||||
}
|
||||
|
||||
const token = await gatewayService.login({
|
||||
gateway: preferredGateway,
|
||||
username,
|
||||
password,
|
||||
userAuthCookie,
|
||||
});
|
||||
|
||||
await vpnService.connect(preferredGateway.address!, token);
|
||||
setProcessing(false);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setNotification({
|
||||
open: true,
|
||||
type: "error",
|
||||
title: "Login failed",
|
||||
message: err.message,
|
||||
});
|
||||
} finally {
|
||||
setPasswordAuthenticating(false);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelPasswordAuth() {
|
||||
setPasswordAuthenticating(false);
|
||||
setPasswordAuthOpen(false);
|
||||
setProcessing(false);
|
||||
}
|
||||
return (
|
||||
<Box padding={2} paddingTop={3}>
|
||||
<ConnectionStatus sx={{ mb: 2 }} status={processing ? "processing" : status} />
|
||||
|
||||
<TextField
|
||||
autoFocus
|
||||
label="Portal address"
|
||||
placeholder="Hostname or IP address"
|
||||
fullWidth
|
||||
size="small"
|
||||
value={portalAddress}
|
||||
onChange={handlePortalChange}
|
||||
InputProps={{ readOnly: status !== "disconnected" }}
|
||||
/>
|
||||
<Box sx={{ mt: 1.5 }}>
|
||||
{status === "disconnected" && (
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleConnect}
|
||||
sx={{ textTransform: "none" }}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
{status === "connecting" && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={handleCancel}
|
||||
sx={{ textTransform: "none" }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{status === "connected" && (
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={handleDisconnect}
|
||||
sx={{ textTransform: "none" }}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<PasswordAuth
|
||||
open={passwordAuthOpen}
|
||||
authData={passwordAuth}
|
||||
authenticating={passwordAuthenticating}
|
||||
onCancel={cancelPasswordAuth}
|
||||
onLogin={handlePasswordAuth}
|
||||
/>
|
||||
<Notification {...notification} onClose={closeNotification} />
|
||||
</Box>
|
||||
);
|
||||
}
|
1
gpgui/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
104
gpgui/src/components/ConnectionStatus.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import GppBadIcon from "@mui/icons-material/GppBad";
|
||||
import VerifiedIcon from "@mui/icons-material/VerifiedUser";
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { BeatLoader } from "react-spinners";
|
||||
|
||||
export type Status =
|
||||
| "processing"
|
||||
| "disconnected"
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "disconnecting";
|
||||
|
||||
export const statusTextMap: Record<Status, string> = {
|
||||
processing: "Processing...",
|
||||
connected: "Connected",
|
||||
disconnected: "Not Connected",
|
||||
connecting: "Connecting...",
|
||||
disconnecting: "Disconnecting...",
|
||||
};
|
||||
|
||||
export default function ConnectionStatus(
|
||||
props: BoxProps<"div", { status?: Status }>
|
||||
) {
|
||||
const theme = useTheme();
|
||||
const { status = "disconnected" } = props;
|
||||
const { palette } = theme;
|
||||
const colorsMap: Record<Status, string> = {
|
||||
processing: 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 connected = status === "connected";
|
||||
const disconnected = status === "disconnected";
|
||||
|
||||
return (
|
||||
<Box {...props}>
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
position: "relative",
|
||||
width: 150,
|
||||
height: 150,
|
||||
mx: "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
size={150}
|
||||
thickness={1}
|
||||
value={pending ? undefined : 100}
|
||||
variant={pending ? "indeterminate" : "determinate"}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
color: colorsMap[status],
|
||||
"& circle": {
|
||||
fill: colorsMap[status],
|
||||
fillOpacity: pending ? 0.1 : 0.25,
|
||||
transition: "all 0.3s ease",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{pending && <BeatLoader color={colorsMap[status]} />}
|
||||
|
||||
{connected && (
|
||||
<VerifiedIcon
|
||||
sx={{
|
||||
position: "relative",
|
||||
fontSize: 80,
|
||||
color: colorsMap[status],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{disconnected && (
|
||||
<GppBadIcon
|
||||
color="disabled"
|
||||
sx={{
|
||||
fontSize: 80,
|
||||
color: colorsMap[status],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Typography textAlign="center" mt={1.5} variant="subtitle1" paragraph>
|
||||
{statusTextMap[status]}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
68
gpgui/src/components/Notification.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertColor,
|
||||
AlertTitle,
|
||||
Slide,
|
||||
SlideProps,
|
||||
Snackbar,
|
||||
SnackbarCloseReason,
|
||||
} from "@mui/material";
|
||||
|
||||
type TransitionProps = Omit<SlideProps, "direction">;
|
||||
|
||||
function TransitionDown(props: TransitionProps) {
|
||||
return <Slide {...props} direction="down" />;
|
||||
}
|
||||
|
||||
export type NotificationType = AlertColor;
|
||||
export type NotificationConfig = {
|
||||
open: boolean;
|
||||
message: string;
|
||||
title?: string;
|
||||
type?: NotificationType;
|
||||
};
|
||||
|
||||
type NotificationProps = {
|
||||
onClose: () => void;
|
||||
} & NotificationConfig;
|
||||
|
||||
export default function Notification(props: NotificationProps) {
|
||||
const { open, message, title, type = "info", onClose } = props;
|
||||
|
||||
function handleClose(
|
||||
_: React.SyntheticEvent | Event,
|
||||
reason?: SnackbarCloseReason
|
||||
) {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
autoHideDuration={5000}
|
||||
TransitionComponent={TransitionDown}
|
||||
onClose={handleClose}
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
severity={type}
|
||||
icon={false}
|
||||
sx={{
|
||||
width: "100%",
|
||||
borderRadius: 0,
|
||||
}}
|
||||
>
|
||||
{title && <AlertTitle>{title}</AlertTitle>}
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
120
gpgui/src/components/PasswordAuth.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { Box, Button, Drawer, TextField, Typography } from "@mui/material";
|
||||
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||
import { Maybe } from "../types";
|
||||
|
||||
export type PasswordAuthData = {
|
||||
labelUsername: string;
|
||||
labelPassword: string;
|
||||
authMessage: Maybe<string>;
|
||||
};
|
||||
|
||||
export type Credentials = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
type LoginCallback = (params: Credentials) => void;
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
authData: PasswordAuthData | undefined;
|
||||
authenticating: boolean;
|
||||
onCancel: () => void;
|
||||
onLogin: LoginCallback;
|
||||
};
|
||||
|
||||
type AuthFormProps = {
|
||||
authenticating: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: LoginCallback;
|
||||
} & PasswordAuthData;
|
||||
|
||||
function AuthForm(props: AuthFormProps) {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.querySelector("input")?.focus();
|
||||
}, []);
|
||||
|
||||
const {
|
||||
authenticating,
|
||||
authMessage,
|
||||
labelUsername,
|
||||
labelPassword,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
} = props;
|
||||
|
||||
function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
if (username.trim() === "" || password === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({ username, password });
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Box display="flex" flexDirection="column" gap={1.5} padding={2}>
|
||||
<Typography>{authMessage}</Typography>
|
||||
<TextField
|
||||
ref={inputRef}
|
||||
label={labelUsername}
|
||||
size="small"
|
||||
autoFocus
|
||||
value={username}
|
||||
InputProps={{ readOnly: authenticating }}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label={labelPassword}
|
||||
size="small"
|
||||
type="password"
|
||||
value={password}
|
||||
InputProps={{ readOnly: authenticating }}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<Box display="flex" gap={1.5}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{ flex: 1, textTransform: "none" }}
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
loading={authenticating}
|
||||
variant="contained"
|
||||
sx={{ flex: 1, textTransform: "none" }}
|
||||
type="submit"
|
||||
disabled={authenticating}
|
||||
>
|
||||
Login
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PasswordAuth(props: Props) {
|
||||
const { open, authData, authenticating, onCancel, onLogin } = props;
|
||||
|
||||
return (
|
||||
<Drawer anchor="bottom" variant="temporary" open={open}>
|
||||
{authData && (
|
||||
<AuthForm
|
||||
{...authData}
|
||||
authenticating={authenticating}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onLogin}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
11
gpgui/src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { CssBaseline } from '@mui/material'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
70
gpgui/src/services/gatewayService.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Body, ResponseType, fetch } from "@tauri-apps/api/http";
|
||||
import { Maybe } from "../types";
|
||||
import { parseXml } from "../utils/parseXml";
|
||||
import { Gateway } from "./types";
|
||||
|
||||
type LoginParams = {
|
||||
gateway: Gateway;
|
||||
username: string;
|
||||
password: string;
|
||||
userAuthCookie: Maybe<string>;
|
||||
};
|
||||
|
||||
class GatewayService {
|
||||
async login(params: LoginParams) {
|
||||
const { gateway, username, password, userAuthCookie } = params;
|
||||
if (!gateway.address) {
|
||||
throw new Error("Gateway address is required");
|
||||
}
|
||||
|
||||
const loginUrl = `https://${gateway.address}/ssl-vpn/login.esp`;
|
||||
|
||||
const response = await fetch<string>(loginUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"User-Agent": "PAN GlobalProtect",
|
||||
},
|
||||
responseType: ResponseType.Text,
|
||||
body: Body.form({
|
||||
prot: "https:",
|
||||
inputStr: "",
|
||||
jnlpReady: "jnlpReady",
|
||||
computer: "Linux", // TODO
|
||||
ok: "Login",
|
||||
direct: "yes",
|
||||
"ipv6-support": "yes",
|
||||
clientVer: "4100",
|
||||
clientos: "Linux",
|
||||
"os-version": "Linux",
|
||||
server: gateway.address,
|
||||
user: username,
|
||||
passwd: password,
|
||||
"portal-userauthcookie": userAuthCookie ?? "",
|
||||
"portal-prelogonuserauthcookie": "",
|
||||
"prelogin-cookie": "",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Login failed");
|
||||
}
|
||||
|
||||
return this.parseLoginResponse(response.data);
|
||||
}
|
||||
|
||||
private parseLoginResponse(response: string) {
|
||||
const result = parseXml(response);
|
||||
const query = new URLSearchParams();
|
||||
|
||||
query.append("authcookie", result.text("argument:nth-child(2)"));
|
||||
query.append("portal", result.text("argument:nth-child(4)"));
|
||||
query.append("user", result.text("argument:nth-child(5)"));
|
||||
query.append("domain", result.text("argument:nth-child(8)"));
|
||||
query.append("preferred-ip", result.text("argument:nth-child(16)"));
|
||||
query.append("computer", "Linux");
|
||||
|
||||
return query.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export default new GatewayService();
|
158
gpgui/src/services/portalService.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Body, ResponseType, fetch } from "@tauri-apps/api/http";
|
||||
import { Maybe, MaybeProperties } from "../types";
|
||||
import { parseXml } from "../utils/parseXml";
|
||||
import { Gateway } from "./types";
|
||||
|
||||
type SamlPreloginResponse = {
|
||||
samlAuthMethod: string;
|
||||
samlAuthRequest: string;
|
||||
};
|
||||
|
||||
type PasswordPreloginResponse = {
|
||||
labelUsername: string;
|
||||
labelPassword: string;
|
||||
authMessage: Maybe<string>;
|
||||
};
|
||||
|
||||
type Region = {
|
||||
region: string;
|
||||
};
|
||||
|
||||
type PreloginResponse = MaybeProperties<
|
||||
SamlPreloginResponse & PasswordPreloginResponse & Region
|
||||
>;
|
||||
|
||||
type ConfigResponse = {
|
||||
userAuthCookie: Maybe<string>;
|
||||
prelogonUserAuthCookie: Maybe<string>;
|
||||
preferredGateway: Gateway;
|
||||
gateways: Gateway[];
|
||||
};
|
||||
|
||||
class PortalService {
|
||||
async prelogin(portal: string) {
|
||||
const preloginUrl = `https://${portal}/global-protect/prelogin.esp`;
|
||||
|
||||
const response = await fetch<string>(preloginUrl, {
|
||||
method: "GET",
|
||||
responseType: ResponseType.Text,
|
||||
query: {
|
||||
tmp: "tmp",
|
||||
"kerberos-support": "yes",
|
||||
"ipv6-support": "yes",
|
||||
clientVer: "4100",
|
||||
clientos: "Linux",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to connect to portal: ${response.status}`);
|
||||
}
|
||||
return this.parsePreloginResponse(response.data);
|
||||
}
|
||||
|
||||
private parsePreloginResponse(response: string): PreloginResponse {
|
||||
const doc = parseXml(response);
|
||||
|
||||
return {
|
||||
samlAuthMethod: doc.text("saml-auth-method"),
|
||||
samlAuthRequest: doc.text("saml-auth-request"),
|
||||
labelUsername: doc.text("username-label"),
|
||||
labelPassword: doc.text("password-label"),
|
||||
authMessage: doc.text("authentication-message"),
|
||||
region: doc.text("region"),
|
||||
};
|
||||
}
|
||||
|
||||
isSamlAuth(response: PreloginResponse): response is SamlPreloginResponse {
|
||||
if (response.samlAuthMethod && response.samlAuthRequest) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isPasswordAuth(
|
||||
response: PreloginResponse
|
||||
): response is PasswordPreloginResponse {
|
||||
if (response.labelUsername && response.labelPassword) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async fetchConfig({
|
||||
portal,
|
||||
username,
|
||||
password,
|
||||
}: {
|
||||
portal: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}) {
|
||||
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({
|
||||
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) {
|
||||
console.error(response);
|
||||
throw new Error(`Failed to fetch portal config: ${response.status}`);
|
||||
}
|
||||
|
||||
return this.parsePortalConfigResponse(response.data);
|
||||
}
|
||||
|
||||
private parsePortalConfigResponse(response: string): ConfigResponse {
|
||||
const result = parseXml(response);
|
||||
const gateways = result.all("gateways list > entry").map((entry) => {
|
||||
const address = entry.attr("name");
|
||||
const name = entry.text("description");
|
||||
const priority = entry.text(":scope > priority");
|
||||
|
||||
return {
|
||||
name,
|
||||
address,
|
||||
priority: priority ? parseInt(priority, 10) : undefined,
|
||||
priorityRules: entry.all("priority-rule > entry").map((entry) => {
|
||||
const name = entry.attr("name");
|
||||
const priority = entry.text("priority");
|
||||
return {
|
||||
name,
|
||||
priority: priority ? parseInt(priority, 10) : undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
userAuthCookie: result.text("portal-userauthcookie"),
|
||||
prelogonUserAuthCookie: result.text("portal-prelogonuserauthcookie"),
|
||||
preferredGateway: gateways[0],
|
||||
gateways,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new PortalService();
|
13
gpgui/src/services/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Maybe } from '../types';
|
||||
|
||||
type PriorityRule = {
|
||||
name: Maybe<string>;
|
||||
priority: Maybe<number>;
|
||||
};
|
||||
|
||||
export type Gateway = {
|
||||
name: Maybe<string>;
|
||||
address: Maybe<string>;
|
||||
priorityRules: PriorityRule[];
|
||||
priority: Maybe<number>;
|
||||
};
|
72
gpgui/src/services/vpnService.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
type Status = 'disconnected' | 'connecting' | 'connected' | 'disconnecting'
|
||||
type StatusCallback = (status: Status) => void
|
||||
type StatusEvent = {
|
||||
payload: {
|
||||
status: Status
|
||||
}
|
||||
}
|
||||
|
||||
class VpnService {
|
||||
private _status: Status = 'disconnected';
|
||||
private statusCallbacks: StatusCallback[] = [];
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
const unlisten = await listen('vpn-status-received', (event: StatusEvent) => {
|
||||
console.log('vpn-status-received', event.payload)
|
||||
this.setStatus(event.payload.status);
|
||||
})
|
||||
|
||||
const status = await this.status();
|
||||
this.setStatus(status);
|
||||
}
|
||||
|
||||
private setStatus(status: Status) {
|
||||
if (this._status != status) {
|
||||
this._status = status;
|
||||
this.fireStatusCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
private async status(): Promise<Status> {
|
||||
return this.invokeCommand<Status>("vpn_status");
|
||||
}
|
||||
|
||||
async connect(server: string, cookie: string) {
|
||||
return this.invokeCommand("vpn_connect", { server, cookie });
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
return this.invokeCommand("vpn_disconnect");
|
||||
}
|
||||
|
||||
onStatusChanged(callback: StatusCallback) {
|
||||
this.statusCallbacks.push(callback);
|
||||
callback(this._status);
|
||||
return () => this.removeStatusCallback(callback);
|
||||
}
|
||||
|
||||
private fireStatusCallbacks() {
|
||||
this.statusCallbacks.forEach(cb => cb(this._status));
|
||||
}
|
||||
|
||||
private removeStatusCallback(callback: StatusCallback) {
|
||||
this.statusCallbacks = this.statusCallbacks.filter(cb => cb !== callback);
|
||||
}
|
||||
|
||||
private async invokeCommand<T>(command: string, args?: any) {
|
||||
try {
|
||||
return await invoke<T>(command, args);
|
||||
} catch (err: any) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new VpnService();
|
5
gpgui/src/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type Maybe<T> = T | null | undefined;
|
||||
|
||||
export type MaybeProperties<T> = {
|
||||
[P in keyof T]?: Maybe<T[P]>;
|
||||
};
|
31
gpgui/src/utils/parseXml.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
type ParseResult = {
|
||||
text: (selector: string) => string;
|
||||
all: (selector: string) => ParseResult[];
|
||||
attr: (selector: string, attr?: string) => string;
|
||||
};
|
||||
|
||||
export function parseXml(xml: string): ParseResult {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
|
||||
return buildParseResult(doc.documentElement);
|
||||
}
|
||||
|
||||
function buildParseResult(el: Element): ParseResult {
|
||||
return {
|
||||
text(selector) {
|
||||
return el.querySelector(selector)?.textContent ?? '';
|
||||
},
|
||||
all(selector) {
|
||||
return [...el.querySelectorAll(selector)].map((node) => {
|
||||
return buildParseResult(node);
|
||||
});
|
||||
},
|
||||
attr(attr, selector) {
|
||||
if (selector) {
|
||||
return el.querySelector(selector)?.getAttribute(attr) ?? '';
|
||||
}
|
||||
return el.getAttribute(attr) ?? '';
|
||||
},
|
||||
};
|
||||
}
|
1
gpgui/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
21
gpgui/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
9
gpgui/tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
7
gpgui/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|