diff --git a/Cargo.lock b/Cargo.lock index 557a147..07c5d75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2119,9 +2119,10 @@ dependencies = [ "dashmap", "dunce", "easytier", - "elevated-command", "gethostname 1.0.2", + "libc", "once_cell", + "security-framework-sys", "serde", "serde_json", "tauri", @@ -2137,6 +2138,8 @@ dependencies = [ "thunk-rs", "tokio", "uuid", + "winapi", + "windows 0.52.0", ] [[package]] @@ -2207,20 +2210,6 @@ dependencies = [ "serde", ] -[[package]] -name = "elevated-command" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c410eccdcc5b759704fdb6a792afe6b01ab8a062e2c003ff2567e2697a94aa" -dependencies = [ - "anyhow", - "base64 0.21.7", - "libc", - "log", - "winapi", - "windows 0.52.0", -] - [[package]] name = "embed-resource" version = "3.0.5" diff --git a/easytier-gui/src-tauri/Cargo.toml b/easytier-gui/src-tauri/Cargo.toml index e5635a3..08e6246 100644 --- a/easytier-gui/src-tauri/Cargo.toml +++ b/easytier-gui/src-tauri/Cargo.toml @@ -40,7 +40,6 @@ chrono = { version = "0.4.37", features = ["serde"] } once_cell = "1.18.0" dashmap = "6.0" -elevated-command = "1.1.2" gethostname = "1.0.2" dunce = "1.0.4" @@ -54,6 +53,15 @@ tauri-plugin-os = "2.3.0" tauri-plugin-autostart = "2.5.0" uuid = "1.17.0" +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.52", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } +winapi = { version = "0.3.9", features = ["securitybaseapi", "processthreadsapi"] } + +[target.'cfg(target_family = "unix")'.dependencies] +libc = "0.2" + +[target.'cfg(target_os = "macos")'.dependencies] +security-framework-sys = "2.9.0" [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/easytier-gui/src-tauri/src/elevate/linux.rs b/easytier-gui/src-tauri/src/elevate/linux.rs new file mode 100644 index 0000000..374a976 --- /dev/null +++ b/easytier-gui/src-tauri/src/elevate/linux.rs @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Luis Liu. All rights reserved. + * Licensed under the MIT License. See License in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use super::Command; +use anyhow::{anyhow, Result}; +use std::env; +use std::ffi::OsStr; +use std::path::PathBuf; +use std::process::{Command as StdCommand, Output}; +use std::str::FromStr; + +/// The implementation of state check and elevated executing varies on each platform +impl Command { + /// Check the state the current program running + /// + /// Return `true` if the program is running as root, otherwise false + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// + /// fn main() { + /// let is_elevated = Command::is_elevated(); + /// + /// } + /// ``` + pub fn is_elevated() -> bool { + let uid = unsafe { libc::getuid() }; + if uid == 0 { + true + } else { + false + } + } + + /// Prompting the user with a graphical OS dialog for the root password, + /// excuting the command with escalated privileges, and return the output + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// use std::process::Command as StdCommand; + /// + /// fn main() { + /// let mut cmd = StdCommand::new("path to the application"); + /// let elevated_cmd = Command::new(cmd); + /// let output = elevated_cmd.output().unwrap(); + /// } + /// ``` + pub fn output(&self) -> Result { + let pkexec = PathBuf::from_str("/bin/pkexec")?; + let mut command = StdCommand::new(pkexec); + let display = env::var("DISPLAY"); + let xauthority = env::var("XAUTHORITY"); + let home = env::var("HOME"); + + command.arg("--disable-internal-agent"); + if display.is_ok() || xauthority.is_ok() || home.is_ok() { + command.arg("env"); + if let Ok(display) = display { + command.arg(format!("DISPLAY={}", display)); + } + if let Ok(xauthority) = xauthority { + command.arg(format!("XAUTHORITY={}", xauthority)); + } + if let Ok(home) = home { + command.arg(format!("HOME={}", home)); + } + } else { + if self.cmd.get_envs().any(|(_, v)| v.is_some()) { + command.arg("env"); + } + } + for (k, v) in self.cmd.get_envs() { + if let Some(value) = v { + command.arg(format!( + "{}={}", + k.to_str().ok_or(anyhow!("invalid key"))?, + value.to_str().ok_or(anyhow!("invalid value"))? + )); + } + } + + command.arg(self.cmd.get_program()); + let args: Vec<&OsStr> = self.cmd.get_args().collect(); + if !args.is_empty() { + command.args(args); + } + + let output = command.output()?; + Ok(output) + } +} diff --git a/easytier-gui/src-tauri/src/elevate/macos.rs b/easytier-gui/src-tauri/src/elevate/macos.rs new file mode 100644 index 0000000..18b721d --- /dev/null +++ b/easytier-gui/src-tauri/src/elevate/macos.rs @@ -0,0 +1,182 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Luis Liu. All rights reserved. + * Licensed under the MIT License. See License in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Thanks to https://github.com/jorangreef/sudo-prompt/blob/master/index.js +// MIT License +// +// Copyright (c) 2015 Joran Dirk Greef +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// ... +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use super::Command; +use anyhow::Result; +use std::env; +use std::path::PathBuf; +use std::process::{ExitStatus, Output}; + +use std::ffi::{CString, OsString}; +use std::io; +use std::mem; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::ptr; + +use libc::{fcntl, fileno, waitpid, EINTR, F_GETOWN}; +use security_framework_sys::authorization::{ + errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights, + AuthorizationCreate, AuthorizationExecuteWithPrivileges, AuthorizationFree, AuthorizationRef, +}; + +const ENV_PATH: &str = "PATH"; + +fn get_exe_path>(exe_name: P) -> Option { + let exe_name = exe_name.as_ref(); + if exe_name.has_root() { + return Some(exe_name.into()); + } + + if let Ok(abs_path) = exe_name.canonicalize() { + if abs_path.is_file() { + return Some(abs_path); + } + } + + env::var_os(ENV_PATH).and_then(|paths| { + env::split_paths(&paths) + .filter_map(|dir| { + let full_path = dir.join(exe_name); + if full_path.is_file() { + Some(full_path) + } else { + None + } + }) + .next() + }) +} + +macro_rules! make_cstring { + ($s:expr) => { + match CString::new($s.as_bytes()) { + Ok(s) => s, + Err(_) => { + return Err(io::Error::new(io::ErrorKind::Other, "null byte in string")); + } + } + }; +} + +unsafe fn gui_runas(prog: *const i8, argv: *const *const i8) -> i32 { + let mut authref: AuthorizationRef = ptr::null_mut(); + let mut pipe: *mut libc::FILE = ptr::null_mut(); + + if AuthorizationCreate( + ptr::null(), + ptr::null(), + kAuthorizationFlagDefaults, + &mut authref, + ) != errAuthorizationSuccess + { + return -1; + } + if AuthorizationExecuteWithPrivileges( + authref, + prog, + kAuthorizationFlagDefaults, + argv as *const *mut _, + &mut pipe, + ) != errAuthorizationSuccess + { + AuthorizationFree(authref, kAuthorizationFlagDestroyRights); + return -1; + } + + let pid = fcntl(fileno(pipe), F_GETOWN, 0); + let mut status = 0; + loop { + let r = waitpid(pid, &mut status, 0); + if r == -1 && io::Error::last_os_error().raw_os_error() == Some(EINTR) { + continue; + } else { + break; + } + } + + AuthorizationFree(authref, kAuthorizationFlagDestroyRights); + status +} + +fn runas_root_gui(cmd: &Command) -> io::Result { + let exe: OsString = match get_exe_path(&cmd.cmd.get_program()) { + Some(exe) => exe.into(), + None => unsafe { + return Ok(mem::transmute(!0)); + }, + }; + let prog = make_cstring!(exe); + let mut args = vec![]; + for arg in cmd.cmd.get_args() { + args.push(make_cstring!(arg)) + } + let mut argv: Vec<_> = args.iter().map(|x| x.as_ptr()).collect(); + argv.push(ptr::null()); + + unsafe { Ok(mem::transmute(gui_runas(prog.as_ptr(), argv.as_ptr()))) } +} + +/// The implementation of state check and elevated executing varies on each platform +impl Command { + /// Check the state the current program running + /// + /// Return `true` if the program is running as root, otherwise false + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// + /// fn main() { + /// let is_elevated = Command::is_elevated(); + /// + /// } + /// ``` + pub fn is_elevated() -> bool { + let uid = unsafe { libc::getuid() }; + let euid = unsafe { libc::geteuid() }; + + match (uid, euid) { + (0, 0) => true, + (_, 0) => true, + (_, _) => false, + } + } + + /// Prompting the user with a graphical OS dialog for the root password, + /// excuting the command with escalated privileges, and return the output + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// use std::process::Command as StdCommand; + /// + /// fn main() { + /// let mut cmd = StdCommand::new("path to the application"); + /// let elevated_cmd = Command::new(cmd); + /// let output = elevated_cmd.output().unwrap(); + /// } + /// ``` + pub fn output(&self) -> Result { + let status = runas_root_gui(self)?; + Ok(Output { + status, + stdout: Vec::new(), + stderr: Vec::new(), + }) + } +} diff --git a/easytier-gui/src-tauri/src/elevate/mod.rs b/easytier-gui/src-tauri/src/elevate/mod.rs new file mode 100644 index 0000000..f3ff06c --- /dev/null +++ b/easytier-gui/src-tauri/src/elevate/mod.rs @@ -0,0 +1,182 @@ +#![allow(dead_code)] +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Luis Liu. All rights reserved. + * Licensed under the MIT License. See License in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +use std::convert::From; +use std::process::Command as StdCommand; + +/// Wrap of std::process::command and escalate privileges while executing +pub struct Command { + cmd: StdCommand, + #[allow(dead_code)] + icon: Option>, + #[allow(dead_code)] + name: Option, +} + +/// Command initialization shares the same logic across all the platforms +impl Command { + /// Constructs a new `Command` from a std::process::Command + /// instance, it would read the following configuration from + /// the instance while executing: + /// + /// * The instance's path to the program + /// * The instance's arguments + /// * The instance's environment variables + /// + /// So far, the new `Command` would only take the environment variables explicitly + /// set by std::process::Command::env and std::process::Command::env, + /// without the ones inherited from the parent process + /// + /// And the environment variables would only be taken on Linux and MacOS, + /// they would be ignored on Windows + /// + /// Current working directory would be the following while executing the command: + /// - %SystemRoot%\System32 on Windows + /// - /root on Linux + /// - $TMPDIR/sudo_prompt_applet/applet.app/Contents/MacOS on MacOS + /// + /// To pass environment variables on Windows, + /// to inherit environment variables from the parent process and + /// to change the working directory will be supported in later versions + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// use std::process::Command as StdCommand; + /// + /// fn main() { + /// let mut cmd = StdCommand::new("path to the application"); + /// + /// cmd.arg("some arg"); + /// cmd.env("some key", "some value"); + /// + /// let elevated_cmd = Command::new(cmd); + /// } + /// ``` + pub fn new(cmd: StdCommand) -> Self { + Self { + cmd, + icon: None, + name: None, + } + } + + /// Consumes the `Take`, returning the wrapped std::process::Command + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// use std::process::Command as StdCommand; + /// + /// fn main() { + /// let mut cmd = StdCommand::new("path to the application"); + /// let elevated_cmd = Command::new(cmd); + /// let cmd = elevated_cmd.into_inner(); + /// } + /// ``` + pub fn into_inner(self) -> StdCommand { + self.cmd + } + + /// Gets a mutable reference to the underlying std::process::Command + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// use std::process::Command as StdCommand; + /// + /// fn main() { + /// let mut cmd = StdCommand::new("path to the application"); + /// let elevated_cmd = Command::new(cmd); + /// let cmd = elevated_cmd.get_ref(); + /// } + /// ``` + pub fn get_ref(&self) -> &StdCommand { + &self.cmd + } + + /// Gets a reference to the underlying std::process::Command + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// use std::process::Command as StdCommand; + /// + /// fn main() { + /// let mut cmd = StdCommand::new("path to the application"); + /// let elevated_cmd = Command::new(cmd); + /// let cmd = elevated_cmd.get_mut(); + /// } + /// ``` + pub fn get_mut(&mut self) -> &mut StdCommand { + &mut self.cmd + } + + /// Set the `icon` for the pop-up graphical OS dialog + /// + /// This method is only applicable on `MacOS` + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// use std::process::Command as StdCommand; + /// + /// fn main() { + /// let mut cmd = StdCommand::new("path to the application"); + /// let elevated_cmd = Command::new(cmd); + /// elevated_cmd.icon(include_bytes!("path to the icon").to_vec()); + /// } + /// ``` + pub fn icon(&mut self, icon: Vec) -> &mut Self { + self.icon = Some(icon); + self + } + + /// Set the name for the pop-up graphical OS dialog + /// + /// This method is only applicable on `MacOS` + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// use std::process::Command as StdCommand; + /// + /// fn main() { + /// let mut cmd = StdCommand::new("path to the application"); + /// let elevated_cmd = Command::new(cmd); + /// elevated_cmd.name("some name".to_string()); + /// } + /// ``` + pub fn name(&mut self, name: String) -> &mut Self { + self.name = Some(name); + self + } +} + +impl From for Command { + /// Converts from a std::process::Command + /// + /// It is similiar with the construct method + fn from(cmd: StdCommand) -> Self { + Self { + cmd, + icon: None, + name: None, + } + } +} + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "windows")] +mod windows; diff --git a/easytier-gui/src-tauri/src/elevate/windows.rs b/easytier-gui/src-tauri/src/elevate/windows.rs new file mode 100644 index 0000000..7678cc3 --- /dev/null +++ b/easytier-gui/src-tauri/src/elevate/windows.rs @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Luis Liu. All rights reserved. + * Licensed under the MIT License. See License in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use super::Command; +use anyhow::Result; +use std::mem; +use std::os::windows::process::ExitStatusExt; +use std::process::{ExitStatus, Output}; +use winapi::shared::minwindef::{DWORD, LPVOID}; +use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken}; +use winapi::um::securitybaseapi::GetTokenInformation; +use winapi::um::winnt::{TokenElevation, HANDLE, TOKEN_ELEVATION, TOKEN_QUERY}; +use windows::core::{w, HSTRING, PCWSTR}; +use windows::Win32::Foundation::HWND; +use windows::Win32::UI::Shell::ShellExecuteW; +use windows::Win32::UI::WindowsAndMessaging::SW_HIDE; + +/// The implementation of state check and elevated executing varies on each platform +impl Command { + /// Check the state the current program running + /// + /// Return `true` if the program is running as root, otherwise false + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// + /// fn main() { + /// let is_elevated = Command::is_elevated(); + /// + /// } + /// ``` + pub fn is_elevated() -> bool { + // Thanks to https://stackoverflow.com/a/8196291 + unsafe { + let mut current_token_ptr: HANDLE = mem::zeroed(); + let mut token_elevation: TOKEN_ELEVATION = mem::zeroed(); + let token_elevation_type_ptr: *mut TOKEN_ELEVATION = &mut token_elevation; + let mut size: DWORD = 0; + + let result = OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut current_token_ptr); + + if result != 0 { + let result = GetTokenInformation( + current_token_ptr, + TokenElevation, + token_elevation_type_ptr as LPVOID, + mem::size_of::() as u32, + &mut size, + ); + if result != 0 { + return token_elevation.TokenIsElevated != 0; + } + } + } + false + } + + /// Prompting the user with a graphical OS dialog for the root password, + /// excuting the command with escalated privileges, and return the output + /// + /// On Windows, according to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew#return-value, + /// Output.status.code() shoudl be greater than 32 if the function succeeds, + /// otherwise the value indicates the cause of the failure + /// + /// On Windows, Output.stdout and Output.stderr will always be empty as of now + /// + /// # Examples + /// + /// ```no_run + /// use elevated_command::Command; + /// use std::process::Command as StdCommand; + /// + /// fn main() { + /// let mut cmd = StdCommand::new("path to the application"); + /// let elevated_cmd = Command::new(cmd); + /// let output = elevated_cmd.output().unwrap(); + /// } + /// ``` + pub fn output(&self) -> Result { + let args = self + .cmd + .get_args() + .map(|c| c.to_str().unwrap().to_string()) + .collect::>(); + let parameters = if args.is_empty() { + HSTRING::new() + } else { + let arg_str = args.join(" "); + HSTRING::from(arg_str) + }; + + // according to https://stackoverflow.com/a/38034535 + // the cwd always point to %SystemRoot%\System32 and cannot be changed by settting lpdirectory param + let r = unsafe { + ShellExecuteW( + HWND(0), + w!("runas"), + &HSTRING::from(self.cmd.get_program()), + &HSTRING::from(parameters), + PCWSTR::null(), + SW_HIDE, + ) + }; + Ok(Output { + status: ExitStatus::from_raw(r.0 as u32), + stdout: Vec::::new(), + stderr: Vec::::new(), + }) + } +} diff --git a/easytier-gui/src-tauri/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs index ac84b26..1333fe8 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -1,6 +1,8 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +mod elevate; + use std::collections::BTreeMap; use easytier::{ @@ -128,7 +130,7 @@ fn toggle_window_visibility(app: &tauri::AppHandle) { #[cfg(not(target_os = "android"))] fn check_sudo() -> bool { - let is_elevated = elevated_command::Command::is_elevated(); + let is_elevated = elevate::Command::is_elevated(); if !is_elevated { let exe_path = std::env::var("APPIMAGE") .ok() @@ -139,7 +141,7 @@ fn check_sudo() -> bool { if args.contains(&AUTOSTART_ARG.to_owned()) { stdcmd.arg(AUTOSTART_ARG); } - elevated_command::Command::new(stdcmd) + elevate::Command::new(stdcmd) .output() .expect("Failed to run elevated command"); }