mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-04-29 14:16:26 -04:00
Implement gpgui-helper
This commit is contained in:
parent
426989350e
commit
c31c7c46d5
40
Cargo.lock
generated
40
Cargo.lock
generated
@ -126,28 +126,6 @@ version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
|
||||
dependencies = [
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.77"
|
||||
@ -1475,6 +1453,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sha256",
|
||||
"specta",
|
||||
"specta-macros",
|
||||
"tauri",
|
||||
@ -1533,13 +1512,12 @@ name = "gpgui-helper"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
"base64 0.21.5",
|
||||
"clap",
|
||||
"compile-time",
|
||||
"downloader",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"gpapi",
|
||||
"log",
|
||||
"reqwest",
|
||||
"tauri",
|
||||
@ -1561,6 +1539,7 @@ dependencies = [
|
||||
"gpapi",
|
||||
"log",
|
||||
"openconnect",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@ -3503,6 +3482,19 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha256"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"hex",
|
||||
"sha2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
|
@ -42,6 +42,7 @@ dotenvy_macro = "0.15"
|
||||
compile-time = "0.2"
|
||||
serde_urlencoded = "0.7"
|
||||
md5="0.7"
|
||||
sha256="1"
|
||||
|
||||
# Tauri dependencies
|
||||
tauri = { version = "1.5" }
|
||||
|
@ -9,6 +9,7 @@ license.workspace = true
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
gpapi = { path = "../../../crates/gpapi", features = ["tauri"] }
|
||||
tauri = { workspace = true, features = ["window-start-dragging"] }
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
@ -16,9 +17,10 @@ log.workspace = true
|
||||
clap.workspace = true
|
||||
compile-time.workspace = true
|
||||
env_logger.workspace = true
|
||||
base64.workspace = true
|
||||
async-stream = "0.3"
|
||||
futures-util.workspace = true
|
||||
downloader = "0.2"
|
||||
tempfile.workspace = true
|
||||
reqwest = { workspace = true, features = ["stream"] }
|
||||
|
||||
[features]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
@ -1,34 +1,52 @@
|
||||
use std::{io::Write, time::Duration};
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use gpapi::utils::window::WindowExt;
|
||||
use log::info;
|
||||
use tauri::{window::MenuHandle, Manager, Window};
|
||||
use tempfile::NamedTempFile;
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::updater::{GuiUpdater, Installer, ProgressNotifier};
|
||||
|
||||
pub struct App {
|
||||
api_key: Vec<u8>,
|
||||
gui_version: String,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(api_key: Vec<u8>) -> Self {
|
||||
Self { api_key }
|
||||
pub fn new(api_key: Vec<u8>, gui_version: &str) -> Self {
|
||||
Self {
|
||||
api_key,
|
||||
gui_version: gui_version.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&self) -> anyhow::Result<()> {
|
||||
let gui_version = self.gui_version.clone();
|
||||
let api_key = self.api_key.clone();
|
||||
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let win = app.get_window("main").unwrap();
|
||||
hide_menu(win.menu_handle());
|
||||
.setup(move |app| {
|
||||
let win = app.get_window("main").expect("no main window");
|
||||
win.hide_menu();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let win_clone = win.clone();
|
||||
download_gui(win.clone()).await;
|
||||
let notifier = ProgressNotifier::new(win.clone());
|
||||
let installer = Installer::new(api_key);
|
||||
let updater = Arc::new(GuiUpdater::new(gui_version, notifier, installer));
|
||||
|
||||
win.listen("app://download", move |_event| {
|
||||
tokio::spawn(download_gui(win_clone.clone()));
|
||||
});
|
||||
let win_clone = win.clone();
|
||||
app.listen_global("app://update-done", move |_event| {
|
||||
info!("Update done");
|
||||
let _ = win_clone.close();
|
||||
});
|
||||
|
||||
// Listen for the update event
|
||||
win.listen("app://update", move |_event| {
|
||||
let updater = Arc::clone(&updater);
|
||||
tokio::spawn(async move { updater.update().await });
|
||||
});
|
||||
|
||||
// Update the GUI on startup
|
||||
win.trigger("app://update", None);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())?;
|
||||
@ -36,80 +54,3 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Fix the bug that the menu bar is visible on Ubuntu 18.04
|
||||
fn hide_menu(menu_handle: MenuHandle) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let menu_visible = menu_handle.is_visible().unwrap_or(false);
|
||||
|
||||
if !menu_visible {
|
||||
break;
|
||||
}
|
||||
|
||||
if menu_visible {
|
||||
let _ = menu_handle.hide();
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn download_gui(win: Window) {
|
||||
let url = "https://github.com/yuezk/GlobalProtect-openconnect/releases/download/v2.0.0/globalprotect-openconnect_2.0.0_x86_64.bin.tar.gz";
|
||||
// let url = "https://free.nchc.org.tw/opensuse/distribution/leap/15.5/iso/openSUSE-Leap-15.5-DVD-x86_64-Build491.1-Media.iso";
|
||||
|
||||
let win_clone = win.clone();
|
||||
|
||||
match download(url, move |p| {
|
||||
let _ = win.emit_all("app://download-progress", p);
|
||||
})
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
info!("download error: {}", err);
|
||||
let _ = win_clone.emit_all("app://download-error", ());
|
||||
}
|
||||
Ok(file) => {
|
||||
let path = file.into_temp_path();
|
||||
info!("download completed: {:?}", path);
|
||||
// Close window after 300ms
|
||||
tokio::time::sleep(Duration::from_millis(300 * 1000)).await;
|
||||
info!("file: {:?}", path);
|
||||
let _ = win_clone.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn download<T>(url: &str, on_progress: T) -> anyhow::Result<NamedTempFile>
|
||||
where
|
||||
T: Fn(Option<f64>) + Send + 'static,
|
||||
{
|
||||
let res = reqwest::get(url).await?.error_for_status()?;
|
||||
let content_length = res.content_length().unwrap_or(0);
|
||||
|
||||
info!("content_length: {}", content_length);
|
||||
|
||||
let mut current_length = 0;
|
||||
let mut stream = res.bytes_stream();
|
||||
|
||||
let mut file = NamedTempFile::new()?;
|
||||
|
||||
while let Some(item) = stream.next().await {
|
||||
let chunk = item?;
|
||||
let chunk_size = chunk.len() as u64;
|
||||
|
||||
file.write_all(&chunk)?;
|
||||
|
||||
current_length += chunk_size;
|
||||
let progress = current_length as f64 / content_length as f64 * 100.0;
|
||||
|
||||
if content_length > 0 {
|
||||
on_progress(Some(progress));
|
||||
} else {
|
||||
on_progress(None);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use base64::prelude::*;
|
||||
use clap::Parser;
|
||||
use gpapi::utils::base64;
|
||||
use log::{info, LevelFilter};
|
||||
|
||||
use crate::app::App;
|
||||
@ -9,27 +9,33 @@ const GP_API_KEY: &[u8; 32] = &[0; 32];
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version = VERSION)]
|
||||
struct Cli {}
|
||||
struct Cli {
|
||||
#[arg(long, help = "Read the API key from stdin")]
|
||||
api_key_on_stdin: bool,
|
||||
|
||||
#[arg(long, default_value = env!("CARGO_PKG_VERSION"), help = "The version of the GUI")]
|
||||
gui_version: String,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
fn run(&self) -> anyhow::Result<()> {
|
||||
#[cfg(debug_assertions)]
|
||||
let api_key = GP_API_KEY.to_vec();
|
||||
#[cfg(not(debug_assertions))]
|
||||
let api_key = self.read_api_key()?;
|
||||
|
||||
let app = App::new(api_key);
|
||||
let app = App::new(api_key, &self.gui_version);
|
||||
|
||||
app.run()
|
||||
}
|
||||
|
||||
fn read_api_key(&self) -> anyhow::Result<Vec<u8>> {
|
||||
let mut api_key = String::new();
|
||||
std::io::stdin().read_line(&mut api_key)?;
|
||||
if self.api_key_on_stdin {
|
||||
let mut api_key = String::new();
|
||||
std::io::stdin().read_line(&mut api_key)?;
|
||||
|
||||
let api_key = BASE64_STANDARD.decode(api_key.trim())?;
|
||||
let api_key = base64::decode_to_vec(api_key.trim())?;
|
||||
|
||||
Ok(api_key)
|
||||
Ok(api_key)
|
||||
} else {
|
||||
Ok(GP_API_KEY.to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
87
apps/gpgui-helper/src-tauri/src/downloader.rs
Normal file
87
apps/gpgui-helper/src-tauri/src/downloader.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::bail;
|
||||
use futures_util::StreamExt;
|
||||
use log::info;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
type OnProgress = Box<dyn Fn(Option<f64>) + Send + Sync + 'static>;
|
||||
|
||||
pub struct FileDownloader<'a> {
|
||||
url: &'a str,
|
||||
on_progress: RwLock<Option<OnProgress>>,
|
||||
}
|
||||
|
||||
impl<'a> FileDownloader<'a> {
|
||||
pub fn new(url: &'a str) -> Self {
|
||||
Self {
|
||||
url,
|
||||
on_progress: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_progress<T>(&self, on_progress: T)
|
||||
where
|
||||
T: Fn(Option<f64>) + Send + Sync + 'static,
|
||||
{
|
||||
if let Ok(mut guard) = self.on_progress.try_write() {
|
||||
*guard = Some(Box::new(on_progress));
|
||||
} else {
|
||||
info!("Failed to acquire on_progress lock");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download(&self) -> anyhow::Result<NamedTempFile> {
|
||||
let res = reqwest::get(self.url).await?.error_for_status()?;
|
||||
let content_length = res.content_length().unwrap_or(0);
|
||||
|
||||
info!("Content length: {}", content_length);
|
||||
|
||||
let mut current_length = 0;
|
||||
let mut stream = res.bytes_stream();
|
||||
|
||||
let mut file = NamedTempFile::new()?;
|
||||
|
||||
while let Some(item) = stream.next().await {
|
||||
let chunk = item?;
|
||||
let chunk_size = chunk.len() as u64;
|
||||
|
||||
file.write_all(&chunk)?;
|
||||
|
||||
current_length += chunk_size;
|
||||
let progress = current_length as f64 / content_length as f64 * 100.0;
|
||||
|
||||
if let Some(on_progress) = &*self.on_progress.read().await {
|
||||
let progress = if content_length > 0 { Some(progress) } else { None };
|
||||
|
||||
on_progress(progress);
|
||||
}
|
||||
}
|
||||
|
||||
if content_length > 0 && current_length != content_length {
|
||||
bail!("Download incomplete");
|
||||
}
|
||||
|
||||
info!("Downloaded to: {:?}", file.path());
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChecksumFetcher<'a> {
|
||||
url: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> ChecksumFetcher<'a> {
|
||||
pub fn new(url: &'a str) -> Self {
|
||||
Self { url }
|
||||
}
|
||||
|
||||
pub async fn fetch(&self) -> anyhow::Result<String> {
|
||||
let res = reqwest::get(self.url).await?.error_for_status()?;
|
||||
let checksum = res.text().await?.trim().to_string();
|
||||
|
||||
Ok(checksum)
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
pub(crate) mod app;
|
||||
pub(crate) mod downloader;
|
||||
pub(crate) mod updater;
|
||||
|
||||
pub mod cli;
|
||||
|
129
apps/gpgui-helper/src-tauri/src/updater.rs
Normal file
129
apps/gpgui-helper/src-tauri/src/updater.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpapi::{
|
||||
service::request::UpdateGuiRequest,
|
||||
utils::{checksum::verify_checksum, crypto::Crypto, endpoint::http_endpoint},
|
||||
};
|
||||
use log::{info, warn};
|
||||
use tauri::{Manager, Window};
|
||||
|
||||
use crate::downloader::{ChecksumFetcher, FileDownloader};
|
||||
|
||||
pub struct ProgressNotifier {
|
||||
win: Window,
|
||||
}
|
||||
|
||||
impl ProgressNotifier {
|
||||
pub fn new(win: Window) -> Self {
|
||||
Self { win }
|
||||
}
|
||||
|
||||
fn notify(&self, progress: Option<f64>) {
|
||||
let _ = self.win.emit_all("app://update-progress", progress);
|
||||
}
|
||||
|
||||
fn notify_error(&self) {
|
||||
let _ = self.win.emit_all("app://update-error", ());
|
||||
}
|
||||
|
||||
fn notify_done(&self) {
|
||||
let _ = self.win.emit_and_trigger("app://update-done", ());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Installer {
|
||||
crypto: Crypto,
|
||||
}
|
||||
|
||||
impl Installer {
|
||||
pub fn new(api_key: Vec<u8>) -> Self {
|
||||
Self {
|
||||
crypto: Crypto::new(api_key),
|
||||
}
|
||||
}
|
||||
|
||||
async fn install(&self, path: &str, checksum: &str) -> anyhow::Result<()> {
|
||||
let service_endpoint = http_endpoint().await?;
|
||||
|
||||
let request = UpdateGuiRequest {
|
||||
path: path.to_string(),
|
||||
checksum: checksum.to_string(),
|
||||
};
|
||||
let payload = self.crypto.encrypt(&request)?;
|
||||
|
||||
reqwest::Client::default()
|
||||
.post(format!("{}/update-gui", service_endpoint))
|
||||
.body(payload)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GuiUpdater {
|
||||
version: String,
|
||||
notifier: Arc<ProgressNotifier>,
|
||||
installer: Installer,
|
||||
}
|
||||
|
||||
impl GuiUpdater {
|
||||
pub fn new(version: String, notifier: ProgressNotifier, installer: Installer) -> Self {
|
||||
Self {
|
||||
version,
|
||||
notifier: Arc::new(notifier),
|
||||
installer,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(&self) {
|
||||
info!("Update GUI, version: {}", self.version);
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
let arch = "amd64";
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
let arch = "arm64";
|
||||
|
||||
let file_url = format!("https://github.com/yuezk/GlobalProtect-openconnect/releases/download/v{}/gpgui-linux-{}", self.version, arch);
|
||||
let checksum_url = format!("{}.sha256", file_url);
|
||||
|
||||
info!("Downloading file: {}", file_url);
|
||||
|
||||
let dl = FileDownloader::new(&file_url);
|
||||
let cf = ChecksumFetcher::new(&checksum_url);
|
||||
let notifier = Arc::clone(&self.notifier);
|
||||
|
||||
dl.on_progress(move |progress| notifier.notify(progress));
|
||||
|
||||
let res = tokio::try_join!(dl.download(), cf.fetch());
|
||||
|
||||
let (file, checksum) = match res {
|
||||
Ok((file, checksum)) => (file, checksum),
|
||||
Err(err) => {
|
||||
warn!("Download error: {}", err);
|
||||
self.notifier.notify_error();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let path = file.into_temp_path();
|
||||
let file_path = path.to_string_lossy();
|
||||
|
||||
if let Err(err) = verify_checksum(&file_path, &checksum) {
|
||||
warn!("Checksum error: {}", err);
|
||||
self.notifier.notify_error();
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Checksum success");
|
||||
|
||||
if let Err(err) = self.installer.install(&file_path, &checksum).await {
|
||||
warn!("Install error: {}", err);
|
||||
self.notifier.notify_error();
|
||||
} else {
|
||||
info!("Install success");
|
||||
self.notifier.notify_done();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,8 +2,8 @@
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "echo 'Skipping Vite build command...'",
|
||||
"devPath": "http://localhost:1420",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"devPath": "http://localhost:1421",
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": false
|
||||
},
|
||||
@ -19,7 +19,7 @@
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"active": false,
|
||||
"targets": "deb",
|
||||
"identifier": "com.yuezk.gpgui-helper",
|
||||
"icon": [
|
||||
@ -40,6 +40,10 @@
|
||||
"resizable": true,
|
||||
"width": 500,
|
||||
"height": 100,
|
||||
"minWidth": 500,
|
||||
"minHeight": 100,
|
||||
"maxWidth": 500,
|
||||
"maxHeight": 100,
|
||||
"label": "main",
|
||||
"decorations": false
|
||||
}
|
||||
|
@ -1,39 +1,43 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CssBaseline,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Box, Button, CssBaseline, LinearProgress, Typography } from "@mui/material";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import logo from "../../assets/icon.svg";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import logo from "../../assets/icon.svg";
|
||||
import { useEffect, useState } from 'react';
|
||||
function useUpdateProgress() {
|
||||
const [progress, setProgress] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = appWindow.listen("app://update-progress", (event) => {
|
||||
setProgress(event.payload as number);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((unlisten) => unlisten());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = appWindow.listen("app://download-error", () => {
|
||||
const unlisten = appWindow.listen("app://update-error", () => {
|
||||
setError(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((unlisten) => unlisten());
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = appWindow.listen("app://download", () => {
|
||||
setError(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((unlisten) => unlisten());
|
||||
}
|
||||
}, []);
|
||||
const handleRetry = () => {
|
||||
setError(false);
|
||||
appWindow.emit("app://update");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -54,7 +58,7 @@ export default function App() {
|
||||
data-tauri-drag-region
|
||||
/>
|
||||
<Box flex={1} ml={2}>
|
||||
{error ? <DownloadFailed /> : <DownloadIndicator />}
|
||||
{error ? <DownloadFailed onRetry={handleRetry} /> : <DownloadIndicator />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
@ -63,17 +67,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
function DownloadIndicator() {
|
||||
const [progress, setProgress] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = appWindow.listen("app://download-progress", (event) => {
|
||||
setProgress(event.payload as number);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((unlisten) => unlisten());
|
||||
}
|
||||
}, []);
|
||||
const progress = useUpdateProgress();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -87,7 +81,7 @@ function DownloadIndicator() {
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadFailed() {
|
||||
function DownloadFailed({ onRetry }: { onRetry: () => void }) {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h1" fontSize="1rem" data-tauri-drag-region>
|
||||
@ -98,7 +92,7 @@ function DownloadFailed() {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => appWindow.emit("app://download")}
|
||||
onClick={onRetry}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
}}
|
||||
@ -121,10 +115,11 @@ function LinearProgressWithLabel(props: { value: number | null }) {
|
||||
value={value ?? 0}
|
||||
sx={{
|
||||
py: 1.2,
|
||||
'.MuiLinearProgress-bar': {
|
||||
transition: "none"
|
||||
}
|
||||
}} />
|
||||
".MuiLinearProgress-bar": {
|
||||
transition: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{value !== null && (
|
||||
<Box sx={{ minWidth: 35, textAlign: "right", ml: 1 }}>
|
||||
|
@ -13,7 +13,7 @@ export default defineConfig(async () => {
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
port: 1421,
|
||||
strictPort: true,
|
||||
},
|
||||
// 3. to make use of `TAURI_DEBUG` and other env variables
|
||||
|
@ -13,6 +13,7 @@ tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
futures.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
|
@ -112,7 +112,7 @@ fn init_logger() -> Arc<Redaction> {
|
||||
let timestamp = buf.timestamp();
|
||||
writeln!(
|
||||
buf,
|
||||
"[{} {} {}] {}",
|
||||
"[{} {} {}] {}",
|
||||
timestamp,
|
||||
record.level(),
|
||||
record.module_path().unwrap_or_default(),
|
||||
@ -127,10 +127,8 @@ fn init_logger() -> Arc<Redaction> {
|
||||
|
||||
async fn launch_gui(envs: Option<HashMap<String, String>>, api_key: Vec<u8>, mut minimized: bool) {
|
||||
loop {
|
||||
let api_key_clone = api_key.clone();
|
||||
let gui_launcher = GuiLauncher::new()
|
||||
let gui_launcher = GuiLauncher::new(env!("CARGO_PKG_VERSION"), &api_key)
|
||||
.envs(envs.clone())
|
||||
.api_key(api_key_clone)
|
||||
.minimized(minimized);
|
||||
|
||||
match gui_launcher.launch().await {
|
||||
|
@ -1,16 +1,23 @@
|
||||
use std::{borrow::Cow, ops::ControlFlow, sync::Arc};
|
||||
use std::{borrow::Cow, fs::Permissions, ops::ControlFlow, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::bail;
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::{
|
||||
ws::{self, CloseFrame, Message, WebSocket},
|
||||
State, WebSocketUpgrade,
|
||||
},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use gpapi::service::event::WsEvent;
|
||||
use gpapi::{
|
||||
service::{event::WsEvent, request::UpdateGuiRequest},
|
||||
utils::checksum::verify_checksum,
|
||||
GP_GUI_BINARY,
|
||||
};
|
||||
use log::{info, warn};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::ws_server::WsServerContext;
|
||||
|
||||
@ -26,12 +33,47 @@ pub(crate) async fn auth_data(State(ctx): State<Arc<WsServerContext>>, body: Str
|
||||
ctx.send_event(WsEvent::AuthData(body)).await;
|
||||
}
|
||||
|
||||
pub(crate) struct UpdateGuiPayload {
|
||||
pub(crate) file: String,
|
||||
pub(crate) checksum: String,
|
||||
pub async fn update_gui(State(ctx): State<Arc<WsServerContext>>, body: Bytes) -> Result<(), StatusCode> {
|
||||
let payload = match ctx.decrypt::<UpdateGuiRequest>(body.to_vec()) {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => {
|
||||
warn!("Failed to decrypt update payload: {}", err);
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
};
|
||||
|
||||
info!("Update GUI: {:?}", payload);
|
||||
let UpdateGuiRequest { path, checksum } = payload;
|
||||
|
||||
info!("Verifying checksum");
|
||||
verify_checksum(&path, &checksum).map_err(|err| {
|
||||
warn!("Failed to verify checksum: {}", err);
|
||||
StatusCode::BAD_REQUEST
|
||||
})?;
|
||||
|
||||
info!("Installing GUI");
|
||||
install_gui(&path).await.map_err(|err| {
|
||||
warn!("Failed to install GUI: {}", err);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_gui(State(ctx): State<Arc<WsServerContext>>, body: Bytes) -> impl IntoResponse {}
|
||||
async fn install_gui(src: &str) -> anyhow::Result<()> {
|
||||
let path = PathBuf::from(GP_GUI_BINARY);
|
||||
let Some(dir) = path.parent() else {
|
||||
bail!("Failed to get parent directory of GUI binary");
|
||||
};
|
||||
|
||||
fs::create_dir_all(dir).await?;
|
||||
|
||||
// Copy the file to the final location and make it executable
|
||||
fs::copy(src, GP_GUI_BINARY).await?;
|
||||
fs::set_permissions(GP_GUI_BINARY, Permissions::from_mode(0o755)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn ws_handler(ws: WebSocketUpgrade, State(ctx): State<Arc<WsServerContext>>) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, ctx))
|
||||
|
@ -6,6 +6,7 @@ use gpapi::{
|
||||
utils::{crypto::Crypto, lock_file::LockFile, redact::Redaction},
|
||||
};
|
||||
use log::{info, warn};
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::{
|
||||
net::TcpListener,
|
||||
sync::{mpsc, watch, RwLock},
|
||||
@ -38,6 +39,10 @@ impl WsServerContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt<T: DeserializeOwned>(&self, encrypted: Vec<u8>) -> anyhow::Result<T> {
|
||||
self.crypto.decrypt(encrypted)
|
||||
}
|
||||
|
||||
pub async fn send_event(&self, event: WsEvent) {
|
||||
let connections = self.connections.read().await;
|
||||
|
||||
|
@ -27,6 +27,7 @@ dotenvy_macro.workspace = true
|
||||
uzers.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
md5.workspace = true
|
||||
sha256.workspace = true
|
||||
|
||||
tauri = { workspace = true, optional = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
|
@ -23,6 +23,8 @@ pub const GP_SERVICE_BINARY: &str = "/usr/bin/gpservice";
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub const GP_GUI_BINARY: &str = "/usr/bin/gpgui";
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub const GP_GUI_HELPER_BINARY: &str = "/usr/bin/gpgui-helper";
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub(crate) const GP_AUTH_BINARY: &str = "/usr/bin/gpauth";
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@ -32,4 +34,6 @@ pub const GP_SERVICE_BINARY: &str = dotenvy_macro::dotenv!("GP_SERVICE_BINARY");
|
||||
#[cfg(debug_assertions)]
|
||||
pub const GP_GUI_BINARY: &str = dotenvy_macro::dotenv!("GP_GUI_BINARY");
|
||||
#[cfg(debug_assertions)]
|
||||
pub const GP_GUI_HELPER_BINARY: &str = dotenvy_macro::dotenv!("GP_GUI_HELPER_BINARY");
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) const GP_AUTH_BINARY: &str = dotenvy_macro::dotenv!("GP_AUTH_BINARY");
|
||||
|
@ -12,11 +12,7 @@ pub trait CommandExt {
|
||||
impl CommandExt for Command {
|
||||
fn new_pkexec<S: AsRef<OsStr>>(program: S) -> Command {
|
||||
let mut cmd = Command::new("pkexec");
|
||||
cmd
|
||||
.arg("--disable-internal-agent")
|
||||
.arg("--user")
|
||||
.arg("root")
|
||||
.arg(program);
|
||||
cmd.arg("--user").arg("root").arg(program);
|
||||
|
||||
cmd
|
||||
}
|
||||
|
68
crates/gpapi/src/process/gui_helper_launcher.rs
Normal file
68
crates/gpapi/src/process/gui_helper_launcher.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use std::{collections::HashMap, path::PathBuf, process::Stdio};
|
||||
|
||||
use anyhow::bail;
|
||||
use log::info;
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
|
||||
use crate::{process::command_traits::CommandExt, utils, GP_GUI_HELPER_BINARY};
|
||||
|
||||
pub struct GuiHelperLauncher<'a> {
|
||||
program: PathBuf,
|
||||
envs: Option<&'a HashMap<String, String>>,
|
||||
api_key: &'a [u8],
|
||||
gui_version: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> GuiHelperLauncher<'a> {
|
||||
pub fn new(api_key: &'a [u8]) -> Self {
|
||||
Self {
|
||||
program: GP_GUI_HELPER_BINARY.into(),
|
||||
envs: None,
|
||||
api_key,
|
||||
gui_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn envs(mut self, envs: Option<&'a HashMap<String, String>>) -> Self {
|
||||
self.envs = envs;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gui_version(mut self, version: Option<&'a str>) -> Self {
|
||||
self.gui_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn launch(&self) -> anyhow::Result<()> {
|
||||
let mut cmd = Command::new(&self.program);
|
||||
|
||||
if let Some(envs) = self.envs {
|
||||
cmd.env_clear();
|
||||
cmd.envs(envs);
|
||||
}
|
||||
|
||||
cmd.arg("--api-key-on-stdin");
|
||||
|
||||
if let Some(gui_version) = self.gui_version {
|
||||
cmd.arg("--gui-version").arg(gui_version);
|
||||
}
|
||||
|
||||
info!("Launching gpgui-helper");
|
||||
let mut non_root_cmd = cmd.into_non_root()?;
|
||||
let mut child = non_root_cmd.kill_on_drop(true).stdin(Stdio::piped()).spawn()?;
|
||||
let Some(mut stdin) = child.stdin.take() else {
|
||||
bail!("Failed to open stdin");
|
||||
};
|
||||
|
||||
let api_key = utils::base64::encode(self.api_key);
|
||||
tokio::spawn(async move {
|
||||
stdin.write_all(api_key.as_bytes()).await.unwrap();
|
||||
drop(stdin);
|
||||
});
|
||||
|
||||
let exit_status = child.wait().await?;
|
||||
info!("gpgui-helper exited with: {}", exit_status);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -4,30 +4,28 @@ use std::{
|
||||
process::{ExitStatus, Stdio},
|
||||
};
|
||||
|
||||
use anyhow::bail;
|
||||
use log::info;
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
|
||||
use crate::{utils::base64, GP_GUI_BINARY};
|
||||
use crate::{process::gui_helper_launcher::GuiHelperLauncher, utils::base64, GP_GUI_BINARY};
|
||||
|
||||
use super::command_traits::CommandExt;
|
||||
|
||||
pub struct GuiLauncher {
|
||||
pub struct GuiLauncher<'a> {
|
||||
version: &'a str,
|
||||
program: PathBuf,
|
||||
api_key: Option<Vec<u8>>,
|
||||
api_key: &'a [u8],
|
||||
minimized: bool,
|
||||
envs: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl Default for GuiLauncher {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl GuiLauncher {
|
||||
pub fn new() -> Self {
|
||||
impl<'a> GuiLauncher<'a> {
|
||||
pub fn new(version: &'a str, api_key: &'a [u8]) -> Self {
|
||||
Self {
|
||||
version,
|
||||
program: GP_GUI_BINARY.into(),
|
||||
api_key: None,
|
||||
api_key,
|
||||
minimized: false,
|
||||
envs: None,
|
||||
}
|
||||
@ -38,17 +36,23 @@ impl GuiLauncher {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn api_key(mut self, api_key: Vec<u8>) -> Self {
|
||||
self.api_key = Some(api_key);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn minimized(mut self, minimized: bool) -> Self {
|
||||
self.minimized = minimized;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn launch(&self) -> anyhow::Result<ExitStatus> {
|
||||
// Check if the program's version
|
||||
if let Err(err) = self.check_version().await {
|
||||
info!("Check version failed: {}", err);
|
||||
// Download the program and replace the current one
|
||||
self.download_program().await?;
|
||||
}
|
||||
|
||||
self.launch_program().await
|
||||
}
|
||||
|
||||
async fn launch_program(&self) -> anyhow::Result<ExitStatus> {
|
||||
let mut cmd = Command::new(&self.program);
|
||||
|
||||
if let Some(envs) = &self.envs {
|
||||
@ -56,33 +60,60 @@ impl GuiLauncher {
|
||||
cmd.envs(envs);
|
||||
}
|
||||
|
||||
if self.api_key.is_some() {
|
||||
cmd.arg("--api-key-on-stdin");
|
||||
}
|
||||
cmd.arg("--api-key-on-stdin");
|
||||
|
||||
if self.minimized {
|
||||
cmd.arg("--minimized");
|
||||
}
|
||||
|
||||
info!("Launching gpgui");
|
||||
let mut non_root_cmd = cmd.into_non_root()?;
|
||||
|
||||
let mut child = non_root_cmd.kill_on_drop(true).stdin(Stdio::piped()).spawn()?;
|
||||
let Some(mut stdin) = child.stdin.take() else {
|
||||
bail!("Failed to open stdin");
|
||||
};
|
||||
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to open stdin"))?;
|
||||
|
||||
if let Some(api_key) = &self.api_key {
|
||||
let api_key = base64::encode(api_key);
|
||||
tokio::spawn(async move {
|
||||
stdin.write_all(api_key.as_bytes()).await.unwrap();
|
||||
drop(stdin);
|
||||
});
|
||||
}
|
||||
let api_key = base64::encode(self.api_key);
|
||||
tokio::spawn(async move {
|
||||
stdin.write_all(api_key.as_bytes()).await.unwrap();
|
||||
drop(stdin);
|
||||
});
|
||||
|
||||
let exit_status = child.wait().await?;
|
||||
|
||||
Ok(exit_status)
|
||||
}
|
||||
|
||||
async fn check_version(&self) -> anyhow::Result<()> {
|
||||
let cmd = Command::new(&self.program).arg("--version").output().await?;
|
||||
let output = String::from_utf8_lossy(&cmd.stdout);
|
||||
|
||||
// Version string: "gpgui 2.0.0 (2024-02-05)"
|
||||
let Some(version) = output.split_whitespace().nth(1) else {
|
||||
bail!("Failed to parse version: {}", output);
|
||||
};
|
||||
|
||||
if version != self.version {
|
||||
bail!("Version mismatch: expected {}, got {}", self.version, version);
|
||||
}
|
||||
|
||||
info!("Version check passed: {}", version);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_program(&self) -> anyhow::Result<()> {
|
||||
let gui_helper = GuiHelperLauncher::new(self.api_key);
|
||||
|
||||
gui_helper
|
||||
.envs(self.envs.as_ref())
|
||||
.gui_version(Some(self.version))
|
||||
.launch()
|
||||
.await?;
|
||||
|
||||
// Check the version again
|
||||
self.check_version().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
pub(crate) mod command_traits;
|
||||
pub(crate) mod gui_helper_launcher;
|
||||
|
||||
pub mod auth_launcher;
|
||||
#[cfg(feature = "browser-auth")]
|
||||
|
@ -135,3 +135,9 @@ pub enum WsRequest {
|
||||
Connect(Box<ConnectRequest>),
|
||||
Disconnect(DisconnectRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct UpdateGuiRequest {
|
||||
pub path: String,
|
||||
pub checksum: String,
|
||||
}
|
||||
|
14
crates/gpapi/src/utils/checksum.rs
Normal file
14
crates/gpapi/src/utils/checksum.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::bail;
|
||||
|
||||
pub fn verify_checksum(path: &str, expected: &str) -> anyhow::Result<()> {
|
||||
let file = Path::new(&path);
|
||||
let checksum = sha256::try_digest(&file)?;
|
||||
|
||||
if checksum != expected {
|
||||
bail!("Checksum mismatch, expected: {}, actual: {}", expected, checksum);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -3,6 +3,7 @@ use reqwest::Url;
|
||||
pub(crate) mod xml;
|
||||
|
||||
pub mod base64;
|
||||
pub mod checksum;
|
||||
pub mod crypto;
|
||||
pub mod endpoint;
|
||||
pub mod env_file;
|
||||
|
@ -2,17 +2,22 @@ use std::{process::ExitStatus, time::Duration};
|
||||
|
||||
use anyhow::bail;
|
||||
use log::{info, warn};
|
||||
use tauri::{window::MenuHandle, Window};
|
||||
use tauri::Window;
|
||||
use tokio::process::Command;
|
||||
|
||||
pub trait WindowExt {
|
||||
fn raise(&self) -> anyhow::Result<()>;
|
||||
fn hide_menu(&self);
|
||||
}
|
||||
|
||||
impl WindowExt for Window {
|
||||
fn raise(&self) -> anyhow::Result<()> {
|
||||
raise_window(self)
|
||||
}
|
||||
|
||||
fn hide_menu(&self) {
|
||||
hide_menu(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raise_window(win: &Window) -> anyhow::Result<()> {
|
||||
@ -34,7 +39,8 @@ pub fn raise_window(win: &Window) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
// Calling window.show() on Windows will cause the menu to be shown.
|
||||
hide_menu(win.menu_handle());
|
||||
// We need to hide it again.
|
||||
hide_menu(win);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -71,7 +77,9 @@ async fn wmctrl_try_raise_window(title: &str) -> anyhow::Result<ExitStatus> {
|
||||
Ok(exit_status)
|
||||
}
|
||||
|
||||
fn hide_menu(menu_handle: MenuHandle) {
|
||||
fn hide_menu(win: &Window) {
|
||||
let menu_handle = win.menu_handle();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let menu_visible = menu_handle.is_visible().unwrap_or(false);
|
||||
|
Loading…
x
Reference in New Issue
Block a user