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",
"Immer",
"jnlp",
"notistack",
"oneshot",
"openconnect",
"pkexec",
"prelogin",
"prelogon",
"prelogonuserauthcookie",
"repr",
"rustc",
"tauri",
"tempdir",
"thiserror",
"unlisten",
"userauthcookie",

12
Cargo.lock generated
View File

@ -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",

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
### 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-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",

26
gpgui/pnpm-lock.yaml generated
View File

@ -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'}

View File

@ -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",
] }

View File

@ -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<Client>>) -> Result<bool, ()> {
@ -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<String, ()> {
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<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]
pub(crate) async fn store_get<'a>(
hint: KeyHint<'_>,

View File

@ -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,

View File

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

View File

@ -79,6 +79,12 @@ export const opensslConfigAtom = atomWithDefault(async () => {
return settingsService.getOpenSSLConfig();
});
export const openconnectConfigAtom = atomWithDefault<string | Promise<string>>(
() => {
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);
}
});

View File

@ -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 (
<React.StrictMode>
<ThemeProvider theme={theme}>
<CssBaseline />
<Suspense fallback={<Loading />}>{children}</Suspense>
<SnackbarProvider>
<CssBaseline />
<Suspense fallback={<Loading />}>{children}</Suspense>
</SnackbarProvider>
</ThemeProvider>
</React.StrictMode>
);

View File

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

View File

@ -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}
>
<MenuItem onClick={openGatewaySwitcher}>
<VpnLock />
<SyncAlt />
Switch Gateway
</MenuItem>
<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 { 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<TabValue>(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={<Devices />}
iconPosition="start"
sx={{ justifyContent: "start" }}
/>
<Tab
label="OpenConnect"
value="openconnect"
icon={<VpnLock />}
iconPosition="start"
sx={{ justifyContent: "start" }}
/>
<Tab
label="OpenSSL"
value="openssl"
icon={<Https />}
iconPosition="start"
sx={{ justifyContent: "start" }}
/>
</TabList>
<Box sx={{ flex: 1 }}>
<Box
sx={{
flex: 1,
display: "flex",
flexDirection: "column",
"& .MuiTabPanel-root[hidden]": { display: "none" },
}}
>
<Simulation />
<OpenConnect />
<OpenSSL />
</Box>
</TabContext>

View File

@ -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<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 {
openSettings,
closeSettings,
@ -137,4 +157,6 @@ export default {
determineOsVersion,
getOpenSSLConfig,
updateOpenSSLConfig,
getOpenconnectConfig,
updateOpenconnectConfig,
};

BIN
screenshot-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB