refactor: support edit the gp.conf

This commit is contained in:
Kevin Yue 2023-07-22 19:05:48 +08:00
parent 601f422863
commit a10539e9c3
18 changed files with 247 additions and 16 deletions

View File

@ -14,14 +14,17 @@
"humantime", "humantime",
"Immer", "Immer",
"jnlp", "jnlp",
"notistack",
"oneshot", "oneshot",
"openconnect", "openconnect",
"pkexec",
"prelogin", "prelogin",
"prelogon", "prelogon",
"prelogonuserauthcookie", "prelogonuserauthcookie",
"repr", "repr",
"rustc", "rustc",
"tauri", "tauri",
"tempdir",
"thiserror", "thiserror",
"unlisten", "unlisten",
"userauthcookie", "userauthcookie",

12
Cargo.lock generated
View File

@ -2300,6 +2300,17 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -3398,6 +3409,7 @@ dependencies = [
"objc", "objc",
"once_cell", "once_cell",
"open", "open",
"os_info",
"percent-encoding", "percent-encoding",
"rand 0.8.5", "rand 0.8.5",
"raw-window-handle", "raw-window-handle",

View File

@ -1,3 +1,7 @@
<p align="center">
<img src="https://github.com/yuezk/GlobalProtect-openconnect/assets/3297602/9242df9c-217d-42ab-8c21-8f9f69cd4eb5">
</p>
## Development ## Development
### Build the service ### Build the service

19
com.yuezk.gp.policy Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>The GlobalProtect-openconnect Project</vendor>
<vendor_url>https://github.com/yuezk/GlobalProtect-openconnect</vendor_url>
<icon_name>gpgui</icon_name>
<action id="com.yuezk.gp.update-gpconf">
<description>Update the /etc/gpservice/gp.conf</description>
<message>Authentication is required to update the GlobalProtect service configuration</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/tee</annotate>
<annotate key="org.freedesktop.policykit.exec.argv1">/etc/gpservice/gp.conf</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
</action>
</policyconfig>

View File

@ -19,6 +19,7 @@
"jotai": "^2.2.1", "jotai": "^2.2.1",
"jotai-immer": "^0.2.0", "jotai-immer": "^0.2.0",
"jotai-optics": "^0.3.0", "jotai-optics": "^0.3.0",
"notistack": "^3.0.1",
"optics-ts": "^2.4.0", "optics-ts": "^2.4.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

26
gpgui/pnpm-lock.yaml generated
View File

@ -35,6 +35,9 @@ dependencies:
jotai-optics: jotai-optics:
specifier: ^0.3.0 specifier: ^0.3.0
version: 0.3.0(jotai@2.2.1)(optics-ts@2.4.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: optics-ts:
specifier: ^2.4.0 specifier: ^2.4.0
version: 2.4.0 version: 2.4.0
@ -1160,6 +1163,14 @@ packages:
/function-bind@1.1.1: /function-bind@1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 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: /has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -1257,6 +1268,21 @@ packages:
hasBin: true hasBin: true
dev: 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: /object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}

View File

@ -16,7 +16,7 @@ tauri-build = { version = "1.3", features = [] }
[dependencies] [dependencies]
gpcommon = { path = "../../gpcommon" } 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 = [ tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = [
"colored", "colored",
] } ] }

View File

@ -6,9 +6,9 @@ use crate::{
}; };
use gpcommon::{Client, ServerApiError, VpnStatus}; use gpcommon::{Client, ServerApiError, VpnStatus};
use serde_json::Value; use serde_json::Value;
use std::sync::Arc; use std::{process::Stdio, sync::Arc};
use tauri::{AppHandle, State}; use tauri::{AppHandle, State};
use tokio::fs; use tokio::{fs, io::AsyncWriteExt, process::Command};
#[tauri::command] #[tauri::command]
pub(crate) async fn service_online<'a>(client: State<'a, Arc<Client>>) -> Result<bool, ()> { pub(crate) async fn service_online<'a>(client: State<'a, Arc<Client>>) -> Result<bool, ()> {
@ -62,8 +62,8 @@ pub(crate) fn os_version() -> String {
} }
#[tauri::command] #[tauri::command]
pub(crate) fn openssl_config() -> String { pub(crate) async fn openssl_config() -> Result<String, ()> {
get_openssl_conf() Ok(get_openssl_conf())
} }
#[tauri::command] #[tauri::command]
@ -75,6 +75,40 @@ pub(crate) async fn update_openssl_config(app_handle: AppHandle) -> tauri::Resul
Ok(()) Ok(())
} }
#[tauri::command]
pub(crate) async fn openconnect_config() -> tauri::Result<String> {
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<i32> {
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] #[tauri::command]
pub(crate) async fn store_get<'a>( pub(crate) async fn store_get<'a>(
hint: KeyHint<'_>, hint: KeyHint<'_>,

View File

@ -31,6 +31,8 @@ fn main() {
commands::os_version, commands::os_version,
commands::openssl_config, commands::openssl_config,
commands::update_openssl_config, commands::update_openssl_config,
commands::openconnect_config,
commands::update_openconnect_config,
commands::store_get, commands::store_get,
commands::store_set, commands::store_set,
commands::store_save, commands::store_save,

View File

@ -25,6 +25,13 @@
}, },
"process": { "process": {
"exit": true "exit": true
},
"fs": {
"scope": ["$TEMP/gp.conf"],
"writeFile": true
},
"os": {
"all": true
} }
}, },
"bundle": { "bundle": {

View File

@ -79,6 +79,12 @@ export const opensslConfigAtom = atomWithDefault(async () => {
return settingsService.getOpenSSLConfig(); return settingsService.getOpenSSLConfig();
}); });
export const openconnectConfigAtom = atomWithDefault<string | Promise<string>>(
() => {
return settingsService.getOpenconnectConfig();
}
);
export const saveSettingsAtom = atom(null, async (get, set) => { export const saveSettingsAtom = atom(null, async (get, set) => {
const clientOS = get(clientOSAtom); const clientOS = get(clientOSAtom);
const osVersion = get(osVersionAtom); const osVersion = get(osVersionAtom);
@ -95,4 +101,11 @@ export const saveSettingsAtom = atom(null, async (get, set) => {
if (customOpenSSL) { if (customOpenSSL) {
await settingsService.updateOpenSSLConfig(); await settingsService.updateOpenSSLConfig();
} }
const initialOpenconnectConfig = await settingsService.getOpenconnectConfig();
const openconnectConfig = await get(openconnectConfigAtom);
if (initialOpenconnectConfig !== openconnectConfig) {
await settingsService.updateOpenconnectConfig(openconnectConfig);
}
}); });

View File

@ -1,4 +1,5 @@
import { Box, CssBaseline, ThemeProvider } from "@mui/material"; import { Box, CssBaseline, ThemeProvider } from "@mui/material";
import { SnackbarProvider } from "notistack";
import React, { Suspense } from "react"; import React, { Suspense } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import "./styles.css"; import "./styles.css";
@ -27,8 +28,10 @@ function AppShell({ children }: { children: React.ReactNode }) {
return ( return (
<React.StrictMode> <React.StrictMode>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <SnackbarProvider>
<Suspense fallback={<Loading />}>{children}</Suspense> <CssBaseline />
<Suspense fallback={<Loading />}>{children}</Suspense>
</SnackbarProvider>
</ThemeProvider> </ThemeProvider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -7,7 +7,7 @@ export default function useGlobalTheme() {
() => () =>
createTheme({ createTheme({
palette: { palette: {
mode: prefersDarkMode ? "dark" : "light", mode: prefersDarkMode ? "light" : "light",
}, },
components: { components: {
MuiButton: { MuiButton: {

View File

@ -4,7 +4,7 @@ import {
LockReset, LockReset,
Menu as MenuIcon, Menu as MenuIcon,
Settings, Settings,
VpnLock, SyncAlt,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { Box, Divider, IconButton, Menu, MenuItem } from "@mui/material"; import { Box, Divider, IconButton, Menu, MenuItem } from "@mui/material";
import { alpha, styled } from "@mui/material/styles"; import { alpha, styled } from "@mui/material/styles";
@ -73,7 +73,7 @@ export default function MainMenu() {
onClick={handleClose} onClick={handleClose}
> >
<MenuItem onClick={openGatewaySwitcher}> <MenuItem onClick={openGatewaySwitcher}>
<VpnLock /> <SyncAlt />
Switch Gateway Switch Gateway
</MenuItem> </MenuItem>
<MenuItem onClick={() => openSettings()}> <MenuItem onClick={() => openSettings()}>

View File

@ -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 (
<TabPanel
value="openconnect"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
>
<Alert severity="info">
You can edit the OpenConnect parameters here. More information can be
found{" "}
<Link
target="_blank"
href="https://github.com/yuezk/GlobalProtect-openconnect/wiki/Configuration"
>
here
</Link>
.
</Alert>
<Box mt={2} sx={{ flex: 1, display: "flex", flexDirection: "column" }}>
<Typography variant="subtitle1">
File location: /etc/gpservice/gp.conf
</Typography>
<TextField
fullWidth
multiline
value={openconnectConfig}
onChange={(event) => 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,
},
},
}}
/>
</Box>
</TabPanel>
);
}

View File

@ -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 { TabContext, TabList } from "@mui/lab";
import { Box, Button, DialogActions, Tab } from "@mui/material"; import { Box, Button, DialogActions, Tab } from "@mui/material";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { useSnackbar } from "notistack";
import { useState } from "react"; import { useState } from "react";
import { saveSettingsAtom } from "../../atoms/settings"; import { saveSettingsAtom } from "../../atoms/settings";
import settingsService, { TabValue } from "../../services/settingsService"; import settingsService, { TabValue } from "../../services/settingsService";
import OpenConnect from "./OpenConnect";
import OpenSSL from "./OpenSSL"; import OpenSSL from "./OpenSSL";
import Simulation from "./Simulation"; import Simulation from "./Simulation";
@ -15,6 +17,7 @@ const activeTab = new URLSearchParams(window.location.search).get(
export default function SettingsPanel() { export default function SettingsPanel() {
const [value, setValue] = useState<TabValue>(activeTab); const [value, setValue] = useState<TabValue>(activeTab);
const saveSettings = useSetAtom(saveSettingsAtom); const saveSettings = useSetAtom(saveSettingsAtom);
const { enqueueSnackbar } = useSnackbar();
const handleChange = (event: React.SyntheticEvent, newValue: string) => { const handleChange = (event: React.SyntheticEvent, newValue: string) => {
setValue(newValue as TabValue); setValue(newValue as TabValue);
@ -25,8 +28,14 @@ export default function SettingsPanel() {
}; };
const save = async () => { const save = async () => {
await saveSettings(); try {
await closeWindow(); 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 ( return (
@ -43,16 +52,33 @@ export default function SettingsPanel() {
value="simulation" value="simulation"
icon={<Devices />} icon={<Devices />}
iconPosition="start" iconPosition="start"
sx={{ justifyContent: "start" }}
/>
<Tab
label="OpenConnect"
value="openconnect"
icon={<VpnLock />}
iconPosition="start"
sx={{ justifyContent: "start" }}
/> />
<Tab <Tab
label="OpenSSL" label="OpenSSL"
value="openssl" value="openssl"
icon={<Https />} icon={<Https />}
iconPosition="start" iconPosition="start"
sx={{ justifyContent: "start" }}
/> />
</TabList> </TabList>
<Box sx={{ flex: 1 }}> <Box
sx={{
flex: 1,
display: "flex",
flexDirection: "column",
"& .MuiTabPanel-root[hidden]": { display: "none" },
}}
>
<Simulation /> <Simulation />
<OpenConnect />
<OpenSSL /> <OpenSSL />
</Box> </Box>
</TabContext> </TabContext>

View File

@ -1,8 +1,11 @@
import { tempdir } from "@tauri-apps/api/os";
import { UserAttentionType, WebviewWindow } from "@tauri-apps/api/window"; import { UserAttentionType, WebviewWindow } from "@tauri-apps/api/window";
import invokeCommand from "../utils/invokeCommand"; import invokeCommand from "../utils/invokeCommand";
import { appStorage } from "./storageService"; 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"; const SETTINGS_WINDOW_LABEL = "settings";
async function openSettings(options?: { tab?: TabValue }) { async function openSettings(options?: { tab?: TabValue }) {
@ -17,7 +20,7 @@ async function openSettings(options?: { tab?: TabValue }) {
new WebviewWindow(SETTINGS_WINDOW_LABEL, { new WebviewWindow(SETTINGS_WINDOW_LABEL, {
url: `pages/settings/index.html?tab=${tab}`, url: `pages/settings/index.html?tab=${tab}`,
title: "GlobalProtect Settings", title: "GlobalProtect Settings",
width: 650, width: 680,
height: 480, height: 480,
center: true, center: true,
resizable: false, resizable: false,
@ -128,6 +131,23 @@ async function updateOpenSSLConfig() {
return invokeCommand("update_openssl_config"); return invokeCommand("update_openssl_config");
} }
async function getOpenconnectConfig(): Promise<string> {
try {
const content = await invokeCommand<string>("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 { export default {
openSettings, openSettings,
closeSettings, closeSettings,
@ -137,4 +157,6 @@ export default {
determineOsVersion, determineOsVersion,
getOpenSSLConfig, getOpenSSLConfig,
updateOpenSSLConfig, updateOpenSSLConfig,
getOpenconnectConfig,
updateOpenconnectConfig,
}; };

BIN
screenshot-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB