From 1954f997837bc7a65b518f4d7a5acd429d4b8e2a Mon Sep 17 00:00:00 2001 From: Observer KRypt0n_ Date: Sun, 16 Apr 2023 14:12:25 +0200 Subject: [PATCH] feat(ui): added sandboxing feature --- Cargo.lock | 38 +++++- src/ui/preferences/environment.rs | 36 +++-- src/ui/preferences/sandbox.rs | 216 ++++++++++++++++++++++++++---- 3 files changed, 242 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a47e8e1..b36000e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,7 +44,8 @@ version = "1.6.2" dependencies = [ "anyhow", "bzip2", - "cached", + "cached 0.43.0", + "dns-lookup", "flate2", "fs_extra", "kinda-virtual-fs", @@ -67,7 +68,7 @@ version = "3.4.1" dependencies = [ "anime-launcher-sdk", "anyhow", - "cached", + "cached 0.42.0", "fluent-templates", "glib-build-tools", "gtk4", @@ -89,7 +90,7 @@ version = "0.5.17" dependencies = [ "anime-game-core", "anyhow", - "cached", + "cached 0.43.0", "discord-rich-presence", "enum-ordinalize", "lazy_static", @@ -344,6 +345,25 @@ dependencies = [ "tokio", ] +[[package]] +name = "cached" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc2fafddf188d13788e7099295a59b99e99b2148ab2195cae454e754cc099925" +dependencies = [ + "async-trait", + "async_once", + "cached_proc_macro", + "cached_proc_macro_types", + "futures", + "hashbrown 0.13.2", + "instant", + "lazy_static", + "once_cell", + "thiserror", + "tokio", +] + [[package]] name = "cached_proc_macro" version = "0.16.0" @@ -629,6 +649,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dns-lookup" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872" +dependencies = [ + "cfg-if", + "libc", + "socket2", + "winapi", +] + [[package]] name = "doc-comment" version = "0.3.3" diff --git a/src/ui/preferences/environment.rs b/src/ui/preferences/environment.rs index ddc8442..81ce751 100644 --- a/src/ui/preferences/environment.rs +++ b/src/ui/preferences/environment.rs @@ -62,8 +62,8 @@ impl AsyncFactoryComponent for Variable { pub struct EnvironmentApp { variables: AsyncFactoryVecDeque, - name: gtk::Entry, - value: gtk::Entry + name_entry: adw::EntryRow, + value_entry: adw::EntryRow } #[derive(Debug, Clone)] @@ -110,20 +110,14 @@ impl SimpleAsyncComponent for EnvironmentApp { add = &adw::PreferencesGroup { set_title: &tr("new-variable"), - gtk::Box { - set_orientation: gtk::Orientation::Horizontal, - set_spacing: 8, + #[local_ref] + name_entry -> adw::EntryRow { + set_title: &tr("name") + }, - #[local_ref] - name -> gtk::Entry { - set_placeholder_text: Some(&tr("name")) - }, - - #[local_ref] - value -> gtk::Entry { - set_placeholder_text: Some(&tr("value")), - set_hexpand: true - } + #[local_ref] + value_entry -> adw::EntryRow { + set_title: &tr("value") }, gtk::Button { @@ -152,8 +146,8 @@ impl SimpleAsyncComponent for EnvironmentApp { let mut model = Self { variables: AsyncFactoryVecDeque::new(adw::PreferencesGroup::new(), sender.input_sender()), - name: gtk::Entry::new(), - value: gtk::Entry::new() + name_entry: adw::EntryRow::new(), + value_entry: adw::EntryRow::new() }; for (name, value) in &CONFIG.game.environment { @@ -162,8 +156,8 @@ impl SimpleAsyncComponent for EnvironmentApp { let variables = model.variables.widget(); - let name = &model.name; - let value = &model.value; + let name_entry = &model.name_entry; + let value_entry = &model.value_entry; let widgets = view_output!(); @@ -174,8 +168,8 @@ impl SimpleAsyncComponent for EnvironmentApp { match msg { EnvironmentMsg::Add => { if let Ok(mut config) = Config::get() { - let name = self.name.text().trim().to_string(); - let value = self.value.text().trim().to_string(); + let name = self.name_entry.text().trim().to_string(); + let value = self.value_entry.text().trim().to_string(); if !name.is_empty() && !value.is_empty() { config.game.environment.insert(name.clone(), value.clone()); diff --git a/src/ui/preferences/sandbox.rs b/src/ui/preferences/sandbox.rs index 79d4315..d71a397 100644 --- a/src/ui/preferences/sandbox.rs +++ b/src/ui/preferences/sandbox.rs @@ -10,12 +10,12 @@ use crate::i18n::tr; use crate::*; #[derive(Debug)] -struct Directory { +struct PrivateDirectory { path: String } #[relm4::factory(async)] -impl AsyncFactoryComponent for Directory { +impl AsyncFactoryComponent for PrivateDirectory { type Init = String; type Input = SandboxAppMsg; type Output = SandboxAppMsg; @@ -33,7 +33,7 @@ impl AsyncFactoryComponent for Directory { set_valign: gtk::Align::Center, connect_clicked[sender, index] => move |_| { - sender.input(SandboxAppMsg::Remove(index.clone())); + sender.input(SandboxAppMsg::RemovePrivate(index.clone())); } } } @@ -58,16 +58,76 @@ impl AsyncFactoryComponent for Directory { } } -pub struct SandboxApp { - directories: AsyncFactoryVecDeque, +#[derive(Debug)] +struct SharedDirectory { + mount_from: String, + mount_to: String +} - sandboxing_path: adw::EntryRow +#[relm4::factory(async)] +impl AsyncFactoryComponent for SharedDirectory { + type Init = (String, String); + type Input = SandboxAppMsg; + type Output = SandboxAppMsg; + type CommandOutput = (); + type ParentInput = SandboxAppMsg; + type ParentWidget = adw::PreferencesGroup; + + view! { + root = adw::ActionRow { + set_title: &self.mount_to, + set_subtitle: &self.mount_from, + + add_suffix = >k::Button { + set_icon_name: "user-trash-symbolic", + add_css_class: "flat", + set_valign: gtk::Align::Center, + + connect_clicked[sender, index] => move |_| { + sender.input(SandboxAppMsg::RemoveShared(index.clone())); + } + } + } + } + + fn output_to_parent_input(output: Self::Output) -> Option { + Some(output) + } + + async fn init_model( + init: Self::Init, + _index: &DynamicIndex, + _sender: AsyncFactorySender, + ) -> Self { + Self { + mount_from: init.0, + mount_to: init.1 + } + } + + async fn update(&mut self, msg: Self::Input, sender: AsyncFactorySender) { + sender.output(msg); + } +} + +pub struct SandboxApp { + private_paths: AsyncFactoryVecDeque, + shared_paths: AsyncFactoryVecDeque, + + private_path_entry: adw::EntryRow, + + shared_path_from_entry: adw::EntryRow, + shared_path_to_entry: adw::EntryRow, + read_only_switch: gtk::Switch } #[derive(Debug, Clone)] pub enum SandboxAppMsg { - Add, - Remove(DynamicIndex) + AddPrivate, + RemovePrivate(DynamicIndex), + + AddShared, + RemoveShared(DynamicIndex) } #[relm4::component(async, pub)] @@ -127,15 +187,19 @@ impl SimpleAsyncComponent for SandboxApp { } } } + }, + + adw::EntryRow { + set_title: "Hostname" } }, add = &adw::PreferencesGroup { - set_title: "Sandboxed directories", - set_description: Some("These folders will be replaced by in-memory filesystem (tmpfs), and their original content will not be available to sandboxed game"), + set_title: "Private directories", + set_description: Some("These folders will be replaced by an empty virtual filesystem (tmpfs), and their original content will not be available to sandboxed game"), #[local_ref] - sandboxing_path -> adw::EntryRow { + private_path_entry -> adw::EntryRow { set_title: "Path" }, @@ -146,12 +210,50 @@ impl SimpleAsyncComponent for SandboxApp { set_margin_top: 8, set_halign: gtk::Align::Start, - connect_clicked => SandboxAppMsg::Add + connect_clicked => SandboxAppMsg::AddPrivate } }, #[local_ref] - add = directories -> adw::PreferencesGroup {} + add = private_paths -> adw::PreferencesGroup {}, + + add = &adw::PreferencesGroup { + set_title: "Shared directories", + set_description: Some("These directories will be symlinked to directories in your host system"), + + #[local_ref] + shared_path_from_entry -> adw::EntryRow { + set_title: "Original path" + }, + + #[local_ref] + shared_path_to_entry -> adw::EntryRow { + set_title: "New path" + }, + + adw::ActionRow { + set_title: "Read-only", + set_subtitle: "Forbid game to write any data to this directory", + + #[local_ref] + add_suffix = read_only_switch -> gtk::Switch { + set_valign: gtk::Align::Center + } + }, + + gtk::Button { + set_label: &tr("add"), + add_css_class: "pill", + + set_margin_top: 8, + set_halign: gtk::Align::Start, + + connect_clicked => SandboxAppMsg::AddShared + } + }, + + #[local_ref] + add = shared_paths -> adw::PreferencesGroup {} } } @@ -163,17 +265,42 @@ impl SimpleAsyncComponent for SandboxApp { tracing::info!("Initializing environment settings"); let mut model = Self { - directories: AsyncFactoryVecDeque::new(adw::PreferencesGroup::new(), sender.input_sender()), + private_paths: AsyncFactoryVecDeque::new(adw::PreferencesGroup::new(), sender.input_sender()), + shared_paths: AsyncFactoryVecDeque::new(adw::PreferencesGroup::new(), sender.input_sender()), - sandboxing_path: adw::EntryRow::new() + private_path_entry: adw::EntryRow::new(), + + shared_path_from_entry: adw::EntryRow::new(), + shared_path_to_entry: adw::EntryRow::new(), + read_only_switch: gtk::Switch::new() }; for path in &CONFIG.sandbox.private { - model.directories.guard().push_back(path.trim().to_string()); + model.private_paths.guard().push_back(path.trim().to_string()); } - let directories = model.directories.widget(); - let sandboxing_path = &model.sandboxing_path; + for (from, to) in &CONFIG.sandbox.mounts.read_only { + model.shared_paths.guard().push_back(( + from.trim().to_string(), + format!("[read-only] {}", to.trim()) + )); + } + + for (from, to) in &CONFIG.sandbox.mounts.bind { + model.shared_paths.guard().push_back(( + from.trim().to_string(), + to.trim().to_string() + )); + } + + let private_paths = model.private_paths.widget(); + let shared_paths = model.shared_paths.widget(); + + let private_path_entry = &model.private_path_entry; + + let shared_path_from_entry = &model.shared_path_from_entry; + let shared_path_to_entry = &model.shared_path_to_entry; + let read_only_switch = &model.read_only_switch; let widgets = view_output!(); @@ -182,29 +309,70 @@ impl SimpleAsyncComponent for SandboxApp { async fn update(&mut self, msg: Self::Input, _sender: AsyncComponentSender) { match msg { - SandboxAppMsg::Add => { + SandboxAppMsg::AddPrivate => { if let Ok(mut config) = Config::get() { - let path = self.sandboxing_path.text().trim().to_string(); + let path = self.private_path_entry.text().trim().to_string(); if !path.is_empty() { config.sandbox.private.push(path.clone()); Config::update(config); - self.directories.guard().push_back(path); + self.private_paths.guard().push_back(path); } } } - SandboxAppMsg::Remove(index) => { + SandboxAppMsg::RemovePrivate(index) => { if let Ok(mut config) = Config::get() { - if let Some(var) = self.directories.guard().get(index.current_index()) { + if let Some(var) = self.private_paths.guard().get(index.current_index()) { config.sandbox.private.retain(|item| item != &var.path); Config::update(config); } - self.directories.guard().remove(index.current_index()); + self.private_paths.guard().remove(index.current_index()); + } + }, + + SandboxAppMsg::AddShared => { + if let Ok(mut config) = Config::get() { + let from = self.shared_path_from_entry.text().trim().to_string(); + let to = self.shared_path_to_entry.text().trim().to_string(); + + let read_only = self.read_only_switch.state(); + + if !from.is_empty() && !to.is_empty() { + if read_only { + config.sandbox.mounts.read_only.insert(from.clone(), to.clone()); + } else { + config.sandbox.mounts.bind.insert(from.clone(), to.clone()); + } + + Config::update(config); + + self.shared_paths.guard().push_back(( + from, + if read_only { + format!("[read-only] {}", to) + } else { + to + } + )); + } + } + } + + SandboxAppMsg::RemoveShared(index) => { + if let Ok(mut config) = Config::get() { + if let Some(var) = self.shared_paths.guard().get(index.current_index()) { + config.sandbox.mounts.read_only.remove(&var.mount_from); + config.sandbox.mounts.bind.remove(&var.mount_from); + + Config::update(config); + } + + self.shared_paths.guard().remove(index.current_index()); } } }