diff --git a/Cargo.lock b/Cargo.lock index 52b2981..0a6109e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,7 @@ dependencies = [ "tor-rtcompat", "walkdir", "winres", + "zeroize", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index 753eadb..5456993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ tokio = { version = "1.21.2", features = ["rt", "time", "macros", "process"] } toml = { version = "0.5.9", features = ["preserve_order"] } tor-rtcompat = "0.7.0" walkdir = "2.3.2" +zeroize = "1.5.7" # Unix dependencies [target.'cfg(unix)'.dependencies] diff --git a/images/ferris/cute.png b/images/ferris/cute.png new file mode 100644 index 0000000..3a2a8df Binary files /dev/null and b/images/ferris/cute.png differ diff --git a/images/ferris/gesture.png b/images/ferris/gesture.png new file mode 100644 index 0000000..c3df3ad Binary files /dev/null and b/images/ferris/gesture.png differ diff --git a/images/ferris/sudo.png b/images/ferris/sudo.png new file mode 100644 index 0000000..9523494 Binary files /dev/null and b/images/ferris/sudo.png differ diff --git a/src/constants.rs b/src/constants.rs index 0b36072..96ac1cb 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -67,6 +67,7 @@ pub const SPACE: f32 = 10.0; pub const RED: egui::Color32 = egui::Color32::from_rgb(230, 50, 50); pub const GREEN: egui::Color32 = egui::Color32::from_rgb(100, 230, 100); pub const YELLOW: egui::Color32 = egui::Color32::from_rgb(230, 230, 100); +pub const BRIGHT_YELLOW: egui::Color32 = egui::Color32::from_rgb(250, 250, 100); pub const GRAY: egui::Color32 = egui::Color32::GRAY; pub const LIGHT_GRAY: egui::Color32 = egui::Color32::LIGHT_GRAY; pub const BLACK: egui::Color32 = egui::Color32::BLACK; @@ -78,6 +79,16 @@ pub const ZERO_SECONDS: std::time::Duration = std::time::Duration::from_secs(0); pub const MILLI_900: std::time::Duration = std::time::Duration::from_millis(900); pub const TOKIO_SECOND: tokio::time::Duration = std::time::Duration::from_secs(1); +// The explaination given to the user on why XMRig needs sudo. +pub const XMRIG_ADMIN_REASON: &str = +r#"The large hashrate difference between XMRig and other miners like Monero and P2Pool's built-in miners is mostly due to XMRig configuring CPU MSRs and setting up hugepages. Other miners like Monero or P2Pool's built-in miner do not do this. It can be done manually but it isn't recommended since XMRig does this for you automatically, but only if it has the proper admin priviledges."#; +// Password buttons +pub const PASSWORD_TEXT: &str = "Enter sudo/admin password here..."; +pub const PASSWORD_LEAVE: &str = "Return to the previous screen"; +pub const PASSWORD_ENTER: &str = "Attempt with the current password"; +pub const PASSWORD_HIDE: &str = "Toggle hiding/showing the password"; + + // OS specific #[cfg(target_os = "windows")] pub const OS: &'static str = " Windows"; diff --git a/src/ferris.rs b/src/ferris.rs index ab9d5b4..e639f3f 100644 --- a/src/ferris.rs +++ b/src/ferris.rs @@ -21,6 +21,7 @@ pub const FERRIS_HAPPY: &[u8] = include_bytes!("../images/ferris/happy.png"); pub const FERRIS_OOPS: &[u8] = include_bytes!("../images/ferris/oops.png"); pub const FERRIS_ERROR: &[u8] = include_bytes!("../images/ferris/error.png"); pub const FERRIS_PANIC: &[u8] = include_bytes!("../images/ferris/panic.png"); // This isnt technically ferris but its ok since its spooky +pub const FERRIS_SUDO: &[u8] = include_bytes!("../images/ferris/sudo.png"); diff --git a/src/main.rs b/src/main.rs index 4ee74c1..b2d44d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,11 +24,12 @@ use egui::{ TextStyle::*, color::Color32, FontFamily::Proportional, - TextStyle, + TextStyle,Spinner, Layout,Align, FontId,Label,RichText,Stroke,Vec2,Button,SelectableLabel, - Key,Modifiers, + Key,Modifiers,TextEdit, CentralPanel,TopBottomPanel, + Hyperlink, }; use egui_extras::RetainedImage; use eframe::{egui,NativeOptions}; @@ -61,6 +62,12 @@ mod update; mod helper; use {ferris::*,constants::*,node::*,disk::*,status::*,update::*,gupax::*,helper::*}; +// Sudo (unix only) +#[cfg(target_family = "unix")] +mod sudo; +#[cfg(target_family = "unix")] +use sudo::*; + //---------------------------------------------------------------------------------------------------- Struct + Impl // The state of the outer main [App]. // See the [State] struct in [state.rs] for the @@ -113,6 +120,9 @@ pub struct App { xmrig_img: Arc>, // A one-time snapshot of what data XMRig started with // Buffer State p2pool_console: String, // The buffer between the p2pool console and the [Helper] + // Sudo State + #[cfg(target_family = "unix")] + sudo: Arc>, // State from [--flags] no_startup: bool, // Static stuff @@ -174,6 +184,8 @@ impl App { p2pool_img, xmrig_img, p2pool_console: String::with_capacity(10), + #[cfg(target_family = "unix")] + sudo: Arc::new(Mutex::new(SudoState::new())), resizing: false, alpha: 0, no_startup: false, @@ -366,6 +378,7 @@ pub enum ErrorButtons { ResetNode, Okay, Quit, + Sudo, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -374,6 +387,7 @@ pub enum ErrorFerris { Oops, Error, Panic, + Sudo, } pub struct ErrorState { @@ -412,6 +426,23 @@ impl ErrorState { buttons, }; } + + // Just sets the current state to new, resetting it. + pub fn reset(&mut self) { + *self = Self::new(); + } + + // Instead of creating a whole new screen and system, this (ab)uses ErrorState + // to ask for the [sudo] when starting XMRig. Yes, yes I know, it's called "ErrorState" + // but rewriting the UI code and button stuff might be worse. + pub fn ask_sudo(&mut self) { + *self = Self { + error: true, + msg: String::new(), + ferris: ErrorFerris::Sudo, + buttons: ErrorButtons::Sudo, + } + } } //---------------------------------------------------------------------------------------------------- [Images] struct @@ -421,6 +452,7 @@ struct Images { oops: RetainedImage, error: RetainedImage, panic: RetainedImage, + sudo: RetainedImage, } impl Images { @@ -431,6 +463,7 @@ impl Images { oops: RetainedImage::from_image_bytes("oops.png", FERRIS_OOPS).unwrap(), error: RetainedImage::from_image_bytes("error.png", FERRIS_ERROR).unwrap(), panic: RetainedImage::from_image_bytes("panic.png", FERRIS_PANIC).unwrap(), + sudo: RetainedImage::from_image_bytes("panic.png", FERRIS_SUDO).unwrap(), } } } @@ -804,6 +837,7 @@ impl eframe::App for App { Oops => &self.img.oops, Error => &self.img.error, Panic => &self.img.panic, + ErrorFerris::Sudo => &self.img.sudo, }; ferris.show_max_size(ui, Vec2::new(width, height)); @@ -825,6 +859,14 @@ impl eframe::App for App { ui.add_sized([width, height], Label::new(format!("--- Gupax has encountered an error! ---\n{}", &self.error_state.msg))); ui.add_sized([width, height], Label::new("Reset the manual node list?")) }, + ErrorButtons::Sudo => { + let text = format!("Why does XMRig need admin priviledge?\n{}", XMRIG_ADMIN_REASON); + let height = height/4.0; + ui.add_sized([width, height], Label::new(format!("--- Gupax needs sudo/admin priviledge for XMRig! ---\n{}", &self.error_state.msg))); + ui.style_mut().override_text_style = Some(Name("MonospaceSmall".into())); + ui.add_sized([width/2.0, height], Label::new(text)); + ui.add_sized([width, height], Hyperlink::from_label_and_url("Click here for more info.", "https://xmrig.com/docs/miner/randomx-optimization-guide")) + }, _ => { match self.error_state.ferris { Panic => ui.add_sized([width, height], Label::new("--- Gupax has encountered an un-recoverable error! ---")), @@ -841,7 +883,7 @@ impl eframe::App for App { match self.error_state.buttons { YesNo => { - if ui.add_sized([width, height/2.0], Button::new("Yes")).clicked() { self.error_state = ErrorState::new(); } + if ui.add_sized([width, height/2.0], Button::new("Yes")).clicked() { self.error_state.reset() } // If [Esc] was pressed, assume [No] if esc || ui.add_sized([width, height/2.0], Button::new("No")).clicked() { exit(0); } }, @@ -871,7 +913,7 @@ impl eframe::App for App { Err(e) => self.error_state.set(format!("State reset fail: {}", e), ErrorFerris::Panic, ErrorButtons::Quit), }; } - if esc || ui.add_sized([width, height/2.0], Button::new("No")).clicked() { self.error_state = ErrorState::new() } + if esc || ui.add_sized([width, height/2.0], Button::new("No")).clicked() { self.error_state.reset() } }, ResetNode => { if ui.add_sized([width, height/2.0], Button::new("Yes")).clicked() { @@ -889,9 +931,38 @@ impl eframe::App for App { Err(e) => self.error_state.set(format!("Node reset fail: {}", e), ErrorFerris::Panic, ErrorButtons::Quit), }; } - if esc || ui.add_sized([width, height/2.0], Button::new("No")).clicked() { self.error_state = ErrorState::new() } + if esc || ui.add_sized([width, height/2.0], Button::new("No")).clicked() { self.error_state.reset() } }, - Okay => if esc || ui.add_sized([width, height], Button::new("Okay")).clicked() { self.error_state = ErrorState::new(); }, + ErrorButtons::Sudo => { + let sudo_width = (width/10.0); + let height = ui.available_height()/4.0; + let mut sudo = self.sudo.lock().unwrap(); + let hide = sudo.hide.clone(); + ui.style_mut().override_text_style = Some(Monospace); + if sudo.testing { + ui.add_sized([width, height], Spinner::new().size(height)); + ui.set_enabled(false); + } else { + ui.add_sized([width, height], Label::new(&sudo.msg)); + } + ui.add_space(height); + let height = ui.available_height()/5.0; + // Password input box with a hider. + ui.horizontal(|ui| { + let response = ui.add_sized([sudo_width*8.0, height], TextEdit::hint_text(TextEdit::singleline(&mut sudo.pass).password(hide), PASSWORD_TEXT)); + let box_width = (ui.available_width()/2.0)-5.0; + if (response.lost_focus() && ui.input().key_pressed(Key::Enter)) || + ui.add_sized([box_width, height], Button::new("Enter")).on_hover_text(PASSWORD_ENTER).clicked() { + if !sudo.testing { + SudoState::test_sudo(Arc::clone(&self.sudo)); + } + } + let color = if hide { BLACK } else { BRIGHT_YELLOW }; + if ui.add_sized([box_width, height], Button::new(RichText::new("👁").color(color))).on_hover_text(PASSWORD_HIDE).clicked() { sudo.hide = !sudo.hide; } + }); + if esc || ui.add_sized([width, height*4.0], Button::new("Leave")).clicked() { self.error_state.reset(); }; + }, + Okay => if esc || ui.add_sized([width, height], Button::new("Okay")).clicked() { self.error_state.reset(); }, Quit => if ui.add_sized([width, height], Button::new("Quit")).clicked() { exit(1); }, } })}); @@ -1079,10 +1150,10 @@ impl eframe::App for App { let width = (ui.available_width()/3.0)-5.0; if self.xmrig.lock().unwrap().is_alive() { if ui.add_sized([width, height], Button::new("⟲")).on_hover_text("Restart XMRig").clicked() { - self.xmrig.lock().unwrap().state = ProcessState::Middle; + self.error_state.ask_sudo(); } if ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop XMRig").clicked() { - self.xmrig.lock().unwrap().state = ProcessState::Dead; + self.error_state.ask_sudo(); } ui.add_enabled_ui(false, |ui| { ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start XMRig"); @@ -1093,7 +1164,7 @@ impl eframe::App for App { ui.add_sized([width, height], Button::new("⏹")).on_hover_text("Stop XMRig"); }); if ui.add_sized([width, height], Button::new("⏺")).on_hover_text("Start XMRig").clicked() { -// Helper::spawn_xmrig(&self.helper, &self.state.xmrig, self.state.gupax.absolute_xmrig_path.clone()); + self.error_state.ask_sudo(); } } }); diff --git a/src/sudo.rs b/src/sudo.rs new file mode 100644 index 0000000..2d50941 --- /dev/null +++ b/src/sudo.rs @@ -0,0 +1,75 @@ +// Gupax - GUI Uniting P2Pool And XMRig +// +// Copyright (c) 2022 hinto-janaiyo +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Handling of [sudo] for XMRig. +// [zeroize] is used to wipe the memory after use. +// Only gets imported in [main.rs] for Unix. + +use zeroize::Zeroize; +use std::sync::{Arc,Mutex}; +use std::thread; +use log::*; + +#[derive(Debug,Clone)] +pub struct SudoState { + pub testing: bool, // Are we attempting a sudo test right now? + pub success: bool, // Was the sudo test a success? + pub hide: bool, // Are we hiding the password? + pub msg: String, // The message shown to the user if unsuccessful + pub pass: String, // The actual password wrapped in a [SecretVec] +} + +impl SudoState { + pub fn new() -> Self { + Self { + testing: false, + success: false, + hide: true, + msg: "".to_string(), + pass: String::with_capacity(256), + } + } + + // Swaps the pass with another 256-capacity String, + // zeroizes the old and drops it. + pub fn wipe(state: &Arc>) { + info!("Sudo | Wiping password with zeros and dropping from memory..."); + let mut new = String::with_capacity(256); + let mut state = state.lock().unwrap(); + // new is now == old, and vice-versa. + std::mem::swap(&mut new, &mut state.pass); + // we're wiping & dropping the old pass here. + new.zeroize(); + std::mem::drop(new); + info!("Sudo ... Password Wipe OK"); + } + + pub fn test_sudo(state: Arc>) { + std::thread::spawn(move || { + state.lock().unwrap().testing = true; + info!("in test_sudo()"); + std::thread::sleep(std::time::Duration::from_secs(3)); + state.lock().unwrap().testing = false; + if state.lock().unwrap().pass == "secret" { + state.lock().unwrap().msg = "Correct!".to_string(); + } else { + state.lock().unwrap().msg = "Incorrect password!".to_string(); + } + Self::wipe(&state); + }); + } +}