fix macos elevate (#1177)

This commit is contained in:
Sijie.Sun
2025-08-01 09:36:10 +08:00
committed by GitHub
parent 583c768f40
commit 43b9e6e6e9
7 changed files with 592 additions and 18 deletions

19
Cargo.lock generated
View File

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

View File

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

View File

@@ -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<Output> {
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)
}
}

View File

@@ -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<P: AsRef<Path>>(exe_name: P) -> Option<PathBuf> {
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<ExitStatus> {
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<Output> {
let status = runas_root_gui(self)?;
Ok(Output {
status,
stdout: Vec::new(),
stderr: Vec::new(),
})
}
}

View File

@@ -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<Vec<u8>>,
#[allow(dead_code)]
name: Option<String>,
}
/// 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<u8>) -> &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<StdCommand> 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;

View File

@@ -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::<winapi::um::winnt::TOKEN_ELEVATION_TYPE>() 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<Output> {
let args = self
.cmd
.get_args()
.map(|c| c.to_str().unwrap().to_string())
.collect::<Vec<String>>();
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::<u8>::new(),
stderr: Vec::<u8>::new(),
})
}
}

View File

@@ -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<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
#[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");
}