safing-portmaster/desktop/tauri/src-tauri/src/service/systemd.rs

246 lines
8.2 KiB
Rust

use log::{debug, error};
use super::status::StatusResult;
use super::{Result, ServiceManager, ServiceManagerError};
use std::os::unix::fs::PermissionsExt;
use std::{
fs, io,
process::{Command, ExitStatus, Stdio},
};
static SYSTEMCTL: &str = "systemctl";
// TODO(ppacher): add support for kdesudo and gksudo
enum SudoCommand {
Pkexec,
Gksu,
}
impl From<std::process::Output> for ServiceManagerError {
fn from(output: std::process::Output) -> Self {
let msg = String::from_utf8(output.stderr)
.ok()
.filter(|s| !s.trim().is_empty())
.or_else(|| {
String::from_utf8(output.stdout)
.ok()
.filter(|s| !s.trim().is_empty())
})
.unwrap_or_else(|| format!("Failed to run `systemctl`"));
ServiceManagerError::Other(output.status, msg)
}
}
/// System Service manager implementation for Linux based distros.
pub struct SystemdServiceManager {}
impl SystemdServiceManager {
/// Checks if systemctl is available in /sbin/ /bin, /usr/bin or /usr/sbin.
///
/// Note that we explicitly check those paths to avoid returning true in case
/// there's a systemctl binary in the cwd and PATH includes . since this may
/// pose a security risk of running an untrusted binary with root privileges.
pub fn is_installed() -> bool {
let paths = vec![
"/sbin/systemctl",
"/bin/systemctl",
"/usr/sbin/systemctl",
"/usr/bin/systemctl",
];
for path in paths {
debug!("checking for systemctl at path {}", path);
match fs::metadata(path) {
Ok(md) => {
debug!("found systemctl at path {} ", path);
if md.is_file() && md.permissions().mode() & 0o111 != 0 {
return true;
}
error!(
"systemctl binary found but invalid permissions: {}",
md.permissions().mode().to_string()
);
}
Err(err) => {
error!(
"failed to check systemctl binary at {}: {}",
path,
err.to_string()
);
continue;
}
};
}
error!("failed to find systemctl binary");
false
}
}
impl ServiceManager for SystemdServiceManager {
fn status(&self) -> super::Result<StatusResult> {
let name = "portmaster.service";
let result = systemctl("is-active", name, false);
match result {
// If `systemctl is-active` returns without an error code and stdout matches "active" (just to guard againt
// unhandled cases), the service can be considered running.
Ok(stdout) => {
let mut copy = stdout.to_owned();
trim_newline(&mut copy);
if copy != "active" {
// make sure the output is as we expected
Err(ServiceManagerError::Other(ExitStatus::default(), stdout))
} else {
Ok(StatusResult::Running)
}
}
Err(e) => {
if let ServiceManagerError::Other(_err, ref output) = e {
let mut copy = output.to_owned();
trim_newline(&mut copy);
if copy == "inactive" {
return Ok(StatusResult::Stopped);
}
} else {
error!("failed to run 'systemctl is-active': {}", e.to_string());
}
// Failed to check if the unit is running
match systemctl("cat", name, false) {
// "systemctl cat" seems to no have stable exit codes so we need
// to check the output if it looks like "No files found for yyyy.service"
// At least, the exit code are not documented for systemd v255 (newest at the time of writing)
Err(ServiceManagerError::Other(status, msg)) => {
if msg.contains("No files found for") {
Ok(StatusResult::NotFound)
} else {
Err(ServiceManagerError::Other(status, msg))
}
}
// Any other error type means something went completely wrong while running systemctl altogether.
Err(e) => Err(e),
// Fine, systemctl cat worked so if the output is "inactive" we know the service is installed
// but stopped.
Ok(_) => {
// Unit seems to be installed so check the output of result
let mut stderr = e.to_string();
trim_newline(&mut stderr);
if stderr == "inactive" {
Ok(StatusResult::Stopped)
} else {
Err(e)
}
}
}
}
}
}
fn start(&self) -> Result<StatusResult> {
let name = "portmaster.service";
// This time we need to run as root through pkexec or similar binaries like kdesudo/gksudo.
systemctl("start", name, true)?;
// Check the status again to be sure it's started now
self.status()
}
}
fn systemctl(
cmd: &str,
unit: &str,
run_as_root: bool,
) -> std::result::Result<String, ServiceManagerError> {
let output = run(run_as_root, SYSTEMCTL, vec![cmd, unit])?;
// The command have been able to run (i.e. has been spawned and executed by the kernel).
// We now need to check the exit code and "stdout/stderr" output in case of an error.
if output.status.success() {
Ok(String::from_utf8(output.stdout)?)
} else {
Err(output.into())
}
}
fn run<'a>(root: bool, cmd: &'a str, args: Vec<&'a str>) -> std::io::Result<std::process::Output> {
// clone the args vector so we can insert the actual command in case we're running
// through pkexec or friends. This is just callled a couple of times on start-up
// so cloning the vector does not add any mentionable performance impact here and it's better
// than expecting a mutalble vector in the first place.
let mut args = args.to_vec();
let mut command = match root {
true => {
// if we run through pkexec and friends we need to append cmd as the second argument.
args.insert(0, cmd);
match get_sudo_cmd() {
Ok(cmd) => {
match cmd {
SudoCommand::Pkexec => {
// disable the internal text-based prompt agent from pkexec because it won't work anyway.
args.insert(0, "--disable-internal-agent");
Command::new("/usr/bin/pkexec")
}
SudoCommand::Gksu => {
args.insert(0, "--message=Please enter your password:");
args.insert(1, "--sudo-mode");
Command::new("/usr/bin/gksudo")
}
}
}
Err(err) => return Err(err),
}
}
false => Command::new(cmd),
};
command.env("LC_ALL", "C");
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command.args(args).output()
}
fn trim_newline(s: &mut String) {
if s.ends_with('\n') {
s.pop();
if s.ends_with('\r') {
s.pop();
}
}
}
fn get_sudo_cmd() -> std::result::Result<SudoCommand, std::io::Error> {
if let Ok(_) = fs::metadata("/usr/bin/pkexec") {
return Ok(SudoCommand::Pkexec);
}
if let Ok(_) = fs::metadata("/usr/bin/gksudo") {
return Ok(SudoCommand::Gksu);
}
Err(std::io::Error::new(
io::ErrorKind::NotFound,
"failed to detect sudo command",
))
}