diff --git a/.vscode/settings.json b/.vscode/settings.json index ef8ad26..69e62d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,14 +14,17 @@ "humantime", "Immer", "jnlp", + "notistack", "oneshot", "openconnect", + "pkexec", "prelogin", "prelogon", "prelogonuserauthcookie", "repr", "rustc", "tauri", + "tempdir", "thiserror", "unlisten", "userauthcookie", diff --git a/Cargo.lock b/Cargo.lock index f402a12..c797961 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2300,6 +2300,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "serde", + "winapi", +] + [[package]] name = "overload" version = "0.1.1" @@ -3398,6 +3409,7 @@ dependencies = [ "objc", "once_cell", "open", + "os_info", "percent-encoding", "rand 0.8.5", "raw-window-handle", diff --git a/README.md b/README.md index 8d49c80..3012017 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ +

+ ## Development ### Build the service diff --git a/com.yuezk.gp.policy b/com.yuezk.gp.policy new file mode 100644 index 0000000..7e34309 --- /dev/null +++ b/com.yuezk.gp.policy @@ -0,0 +1,19 @@ + + + + The GlobalProtect-openconnect Project + https://github.com/yuezk/GlobalProtect-openconnect + gpgui + + Update the /etc/gpservice/gp.conf + Authentication is required to update the GlobalProtect service configuration + + auth_admin + auth_admin + auth_admin + + /usr/bin/tee + /etc/gpservice/gp.conf + true + + diff --git a/gpgui/package.json b/gpgui/package.json index 4d4af5c..b0741b2 100644 --- a/gpgui/package.json +++ b/gpgui/package.json @@ -19,6 +19,7 @@ "jotai": "^2.2.1", "jotai-immer": "^0.2.0", "jotai-optics": "^0.3.0", + "notistack": "^3.0.1", "optics-ts": "^2.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/gpgui/pnpm-lock.yaml b/gpgui/pnpm-lock.yaml index f7c4dbf..1ee253d 100644 --- a/gpgui/pnpm-lock.yaml +++ b/gpgui/pnpm-lock.yaml @@ -35,6 +35,9 @@ dependencies: jotai-optics: specifier: ^0.3.0 version: 0.3.0(jotai@2.2.1)(optics-ts@2.4.0) + notistack: + specifier: ^3.0.1 + version: 3.0.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0) optics-ts: specifier: ^2.4.0 version: 2.4.0 @@ -1160,6 +1163,14 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /goober@2.1.13(csstype@3.1.2): + resolution: {integrity: sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==} + peerDependencies: + csstype: ^3.0.10 + dependencies: + csstype: 3.1.2 + dev: false + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1257,6 +1268,21 @@ packages: hasBin: true dev: true + /notistack@3.0.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==} + engines: {node: '>=12.0.0', npm: '>=6.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + clsx: 1.2.1 + goober: 2.1.13(csstype@3.1.2) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - csstype + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} diff --git a/gpgui/src-tauri/Cargo.toml b/gpgui/src-tauri/Cargo.toml index c4102ab..5057aa4 100644 --- a/gpgui/src-tauri/Cargo.toml +++ b/gpgui/src-tauri/Cargo.toml @@ -16,7 +16,7 @@ tauri-build = { version = "1.3", features = [] } [dependencies] gpcommon = { path = "../../gpcommon" } -tauri = { version = "1.3", features = ["http-all", "process-exit", "shell-open", "window-all", "window-data-url"] } +tauri = { version = "1.3", features = ["fs-write-file", "http-all", "os-all", "process-exit", "shell-open", "window-all", "window-data-url"] } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = [ "colored", ] } diff --git a/gpgui/src-tauri/src/commands.rs b/gpgui/src-tauri/src/commands.rs index e715020..28530b2 100644 --- a/gpgui/src-tauri/src/commands.rs +++ b/gpgui/src-tauri/src/commands.rs @@ -6,9 +6,9 @@ use crate::{ }; use gpcommon::{Client, ServerApiError, VpnStatus}; use serde_json::Value; -use std::sync::Arc; +use std::{process::Stdio, sync::Arc}; use tauri::{AppHandle, State}; -use tokio::fs; +use tokio::{fs, io::AsyncWriteExt, process::Command}; #[tauri::command] pub(crate) async fn service_online<'a>(client: State<'a, Arc>) -> Result { @@ -62,8 +62,8 @@ pub(crate) fn os_version() -> String { } #[tauri::command] -pub(crate) fn openssl_config() -> String { - get_openssl_conf() +pub(crate) async fn openssl_config() -> Result { + Ok(get_openssl_conf()) } #[tauri::command] @@ -75,6 +75,40 @@ pub(crate) async fn update_openssl_config(app_handle: AppHandle) -> tauri::Resul Ok(()) } +#[tauri::command] +pub(crate) async fn openconnect_config() -> tauri::Result { + let file = "/etc/gpservice/gp.conf"; + let content = fs::read_to_string(file).await?; + Ok(content) +} + +#[tauri::command] +pub(crate) async fn update_openconnect_config(content: String) -> tauri::Result { + let file = "/etc/gpservice/gp.conf"; + let mut child = Command::new("pkexec") + .arg("tee") + .arg(file) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .spawn()?; + + let mut stdin = child.stdin.take().unwrap(); + + tokio::spawn(async move { + stdin.write_all(content.as_bytes()).await.unwrap(); + drop(stdin); + }); + + let exit_status = child.wait().await?; + + exit_status.code().ok_or_else(|| { + tauri::Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "Process exited without a code", + )) + }) +} + #[tauri::command] pub(crate) async fn store_get<'a>( hint: KeyHint<'_>, diff --git a/gpgui/src-tauri/src/main.rs b/gpgui/src-tauri/src/main.rs index 0b23c2f..6f0f2f5 100644 --- a/gpgui/src-tauri/src/main.rs +++ b/gpgui/src-tauri/src/main.rs @@ -31,6 +31,8 @@ fn main() { commands::os_version, commands::openssl_config, commands::update_openssl_config, + commands::openconnect_config, + commands::update_openconnect_config, commands::store_get, commands::store_set, commands::store_save, diff --git a/gpgui/src-tauri/tauri.conf.json b/gpgui/src-tauri/tauri.conf.json index fe19e5a..80fc81f 100644 --- a/gpgui/src-tauri/tauri.conf.json +++ b/gpgui/src-tauri/tauri.conf.json @@ -25,6 +25,13 @@ }, "process": { "exit": true + }, + "fs": { + "scope": ["$TEMP/gp.conf"], + "writeFile": true + }, + "os": { + "all": true } }, "bundle": { diff --git a/gpgui/src/atoms/settings.ts b/gpgui/src/atoms/settings.ts index 7ea0449..44c6e83 100644 --- a/gpgui/src/atoms/settings.ts +++ b/gpgui/src/atoms/settings.ts @@ -79,6 +79,12 @@ export const opensslConfigAtom = atomWithDefault(async () => { return settingsService.getOpenSSLConfig(); }); +export const openconnectConfigAtom = atomWithDefault>( + () => { + return settingsService.getOpenconnectConfig(); + } +); + export const saveSettingsAtom = atom(null, async (get, set) => { const clientOS = get(clientOSAtom); const osVersion = get(osVersionAtom); @@ -95,4 +101,11 @@ export const saveSettingsAtom = atom(null, async (get, set) => { if (customOpenSSL) { await settingsService.updateOpenSSLConfig(); } + + const initialOpenconnectConfig = await settingsService.getOpenconnectConfig(); + const openconnectConfig = await get(openconnectConfigAtom); + + if (initialOpenconnectConfig !== openconnectConfig) { + await settingsService.updateOpenconnectConfig(openconnectConfig); + } }); diff --git a/gpgui/src/components/AppShell/index.tsx b/gpgui/src/components/AppShell/index.tsx index 0873263..4749efb 100644 --- a/gpgui/src/components/AppShell/index.tsx +++ b/gpgui/src/components/AppShell/index.tsx @@ -1,4 +1,5 @@ import { Box, CssBaseline, ThemeProvider } from "@mui/material"; +import { SnackbarProvider } from "notistack"; import React, { Suspense } from "react"; import { createRoot } from "react-dom/client"; import "./styles.css"; @@ -27,8 +28,10 @@ function AppShell({ children }: { children: React.ReactNode }) { return ( - - }>{children} + + + }>{children} + ); diff --git a/gpgui/src/components/AppShell/useGlobalTheme.ts b/gpgui/src/components/AppShell/useGlobalTheme.ts index 4627be7..2521f5a 100644 --- a/gpgui/src/components/AppShell/useGlobalTheme.ts +++ b/gpgui/src/components/AppShell/useGlobalTheme.ts @@ -7,7 +7,7 @@ export default function useGlobalTheme() { () => createTheme({ palette: { - mode: prefersDarkMode ? "dark" : "light", + mode: prefersDarkMode ? "light" : "light", }, components: { MuiButton: { diff --git a/gpgui/src/components/MainMenu/index.tsx b/gpgui/src/components/MainMenu/index.tsx index e55b576..db87928 100644 --- a/gpgui/src/components/MainMenu/index.tsx +++ b/gpgui/src/components/MainMenu/index.tsx @@ -4,7 +4,7 @@ import { LockReset, Menu as MenuIcon, Settings, - VpnLock, + SyncAlt, } from "@mui/icons-material"; import { Box, Divider, IconButton, Menu, MenuItem } from "@mui/material"; import { alpha, styled } from "@mui/material/styles"; @@ -73,7 +73,7 @@ export default function MainMenu() { onClick={handleClose} > - + Switch Gateway openSettings()}> diff --git a/gpgui/src/components/settings/OpenConnect.tsx b/gpgui/src/components/settings/OpenConnect.tsx new file mode 100644 index 0000000..102eeff --- /dev/null +++ b/gpgui/src/components/settings/OpenConnect.tsx @@ -0,0 +1,59 @@ +import { TabPanel } from "@mui/lab"; +import { Alert, Box, Link, TextField, Typography } from "@mui/material"; +import { useAtom } from "jotai"; +import { openconnectConfigAtom } from "../../atoms/settings"; + +export default function OpenConnect() { + const [openconnectConfig, setOpenconnectConfig] = useAtom( + openconnectConfigAtom + ); + + return ( + + + You can edit the OpenConnect parameters here. More information can be + found{" "} + + here + + . + + + + + File location: /etc/gpservice/gp.conf + + setOpenconnectConfig(event.target.value)} + sx={{ + flex: 1, + display: "flex", + "& .MuiInputBase-root": { + flex: "1 1 auto", + display: "flex", + flexDirection: "column", + height: 0, + + "& textarea": { + whiteSpace: "pre", + fontFamily: "monospace", + overflow: "auto !important", + fontSize: 14, + lineHeight: 1.2, + }, + }, + }} + /> + + + ); +} diff --git a/gpgui/src/components/settings/index.tsx b/gpgui/src/components/settings/index.tsx index b3ffeb9..f0db823 100644 --- a/gpgui/src/components/settings/index.tsx +++ b/gpgui/src/components/settings/index.tsx @@ -1,10 +1,12 @@ -import { Devices, Https } from "@mui/icons-material"; +import { Devices, Https, VpnLock } from "@mui/icons-material"; import { TabContext, TabList } from "@mui/lab"; import { Box, Button, DialogActions, Tab } from "@mui/material"; import { useSetAtom } from "jotai"; +import { useSnackbar } from "notistack"; import { useState } from "react"; import { saveSettingsAtom } from "../../atoms/settings"; import settingsService, { TabValue } from "../../services/settingsService"; +import OpenConnect from "./OpenConnect"; import OpenSSL from "./OpenSSL"; import Simulation from "./Simulation"; @@ -15,6 +17,7 @@ const activeTab = new URLSearchParams(window.location.search).get( export default function SettingsPanel() { const [value, setValue] = useState(activeTab); const saveSettings = useSetAtom(saveSettingsAtom); + const { enqueueSnackbar } = useSnackbar(); const handleChange = (event: React.SyntheticEvent, newValue: string) => { setValue(newValue as TabValue); @@ -25,8 +28,14 @@ export default function SettingsPanel() { }; const save = async () => { - await saveSettings(); - await closeWindow(); + try { + await saveSettings(); + enqueueSnackbar("Settings saved", { variant: "success" }); + await closeWindow(); + } catch (err) { + console.warn("Failed to save settings", err); + enqueueSnackbar("Failed to save settings", { variant: "error" }); + } }; return ( @@ -43,16 +52,33 @@ export default function SettingsPanel() { value="simulation" icon={} iconPosition="start" + sx={{ justifyContent: "start" }} + /> + } + iconPosition="start" + sx={{ justifyContent: "start" }} /> } iconPosition="start" + sx={{ justifyContent: "start" }} /> - + + diff --git a/gpgui/src/services/settingsService.ts b/gpgui/src/services/settingsService.ts index 063fb88..35ff8e5 100644 --- a/gpgui/src/services/settingsService.ts +++ b/gpgui/src/services/settingsService.ts @@ -1,8 +1,11 @@ +import { tempdir } from "@tauri-apps/api/os"; import { UserAttentionType, WebviewWindow } from "@tauri-apps/api/window"; import invokeCommand from "../utils/invokeCommand"; import { appStorage } from "./storageService"; +import { fs } from "@tauri-apps/api"; +import { Command } from "@tauri-apps/api/shell"; -export type TabValue = "simulation" | "openssl"; +export type TabValue = "simulation" | "openssl" | "openconnect"; const SETTINGS_WINDOW_LABEL = "settings"; async function openSettings(options?: { tab?: TabValue }) { @@ -17,7 +20,7 @@ async function openSettings(options?: { tab?: TabValue }) { new WebviewWindow(SETTINGS_WINDOW_LABEL, { url: `pages/settings/index.html?tab=${tab}`, title: "GlobalProtect Settings", - width: 650, + width: 680, height: 480, center: true, resizable: false, @@ -128,6 +131,23 @@ async function updateOpenSSLConfig() { return invokeCommand("update_openssl_config"); } +async function getOpenconnectConfig(): Promise { + try { + const content = await invokeCommand("openconnect_config"); + return content; + } catch (e) { + console.error(e); + return "# Failed to read /etc/gpservice/gp.conf"; + } +} + +async function updateOpenconnectConfig(content: string) { + const exitCode = await invokeCommand("update_openconnect_config", { content }); + if (exitCode) { + throw new Error(`Failed to update openconnect config: ${exitCode}`); + } +} + export default { openSettings, closeSettings, @@ -137,4 +157,6 @@ export default { determineOsVersion, getOpenSSLConfig, updateOpenSSLConfig, + getOpenconnectConfig, + updateOpenconnectConfig, }; diff --git a/screenshot-light.png b/screenshot-light.png new file mode 100644 index 0000000..7e48778 Binary files /dev/null and b/screenshot-light.png differ