diff --git a/INSTALL.md b/INSTALL.md index e0e1769..d90d65d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -10,7 +10,6 @@ We have packaged `swhkd-git`. `swhkd-bin` has been packaged separately by a user **Runtime:** -- Policy Kit Daemon ( polkit ) - Uinput kernel module - Evdev kernel module @@ -34,5 +33,5 @@ We have packaged `swhkd-git`. `swhkd-bin` has been packaged separately by a user ``` swhks & -pkexec swhkd +swhkd ``` diff --git a/Makefile b/Makefile index 98147ac..93b3b07 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,6 @@ DESTDIR ?= "/" DAEMON_BINARY := swhkd SERVER_BINARY := swhks BUILDFLAGS := --release -POLKIT_DIR := /usr/share/polkit-1/actions -POLKIT_POLICY_FILE := com.github.swhkd.pkexec.policy TARGET_DIR := /usr/bin MAN1_DIR := /usr/share/man/man1 MAN5_DIR := /usr/share/man/man5 @@ -15,9 +13,6 @@ all: build build: @cargo build $(BUILDFLAGS) - @./scripts/build-polkit-policy.sh \ - --policy-path=$(POLKIT_POLICY_FILE) \ - --swhkd-path=$(TARGET_DIR)/$(DAEMON_BINARY) install: @find ./docs -type f -iname "*.1.gz" \ @@ -25,9 +20,10 @@ install: @find ./docs -type f -iname "*.5.gz" \ -exec install -Dm 644 {} -t $(DESTDIR)/$(MAN5_DIR) \; @install -Dm 755 ./target/release/$(DAEMON_BINARY) -t $(DESTDIR)/$(TARGET_DIR) + @sudo chown root:root $(DESTDIR)/$(TARGET_DIR)/$(DAEMON_BINARY) + @sudo chmod u+s $(DESTDIR)/$(TARGET_DIR)/$(DAEMON_BINARY) @install -Dm 755 ./target/release/$(SERVER_BINARY) -t $(DESTDIR)/$(TARGET_DIR) - @install -Dm 644 -o root ./$(POLKIT_POLICY_FILE) -t $(DESTDIR)/$(POLKIT_DIR) -# Ideally, we would have a default config file instead of an empty one + # Ideally, we would have a default config file instead of an empty one @if [ ! -f $(DESTDIR)/etc/$(DAEMON_BINARY)/$(DAEMON_BINARY)rc ]; then \ touch ./$(DAEMON_BINARY)rc; \ install -Dm 644 ./$(DAEMON_BINARY)rc -t $(DESTDIR)/etc/$(DAEMON_BINARY); \ @@ -38,7 +34,6 @@ uninstall: @$(RM) -f /usr/share/man/**/swhks.* @$(RM) $(TARGET_DIR)/$(SERVER_BINARY) @$(RM) $(TARGET_DIR)/$(DAEMON_BINARY) - @$(RM) $(POLKIT_DIR)/$(POLKIT_POLICY_FILE) check: @cargo fmt @@ -57,7 +52,6 @@ clean: @cargo clean @$(RM) -f ./docs/*.gz @$(RM) -f $(DAEMON_BINARY)rc - @$(RM) -f $(POLKIT_POLICY_FILE) setup: @rustup install stable diff --git a/README.md b/README.md index 7bc5855..9ebf6c4 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Xorg or Wayland desktops, and you can even use `swhkd` in a TTY. ```bash swhks & -pkexec swhkd +swhkd ``` ## Runtime signals @@ -50,9 +50,7 @@ After opening `swhkd`, you can control the program through signals: `swhkd` closely follows `sxhkd` syntax, so most existing `sxhkd` configs should be functional with `swhkd`. -The default configuration file is in `/etc/swhkd/swhkdrc`. If you don't like -having to edit the file as root every single time, you can create a symlink from -`~/.config/swhkd/swhkdrc` to `/etc/swhkd/swhkdrc`. +The default configuration file is in `~/.config/swhkd/swhkdrc` with a fallback to `etc/swhkd/swhkdrc`. If you use Vim, you can get `swhkd` config syntax highlighting with the [swhkd-vim](https://github.com/waycrate/swhkd-vim) plugin. Install it in @@ -75,13 +73,13 @@ All supported key and modifier names are listed in `man 5 swhkd-keys`. ## Security We use a server-client model to keep you safe. The daemon (`swhkd` — privileged -process) communicates to the server (`swhks` — running as non-root user) after -checking for valid keybindings. Since the daemon is totally separate from the -server, no other process can read your keystrokes. As for shell commands, you -might be thinking that any program can send shell commands to the server and -that's true! But the server runs the commands as the currently logged-in user, -so no extra permissions are provided (This is essentially the same as any app on -your desktop calling shell commands). +process) is responsible for listening to key events and running shell commands. +The server (`swhks` — non-privileged process) is responsible for keeping a track of the +environment variables and sending them to the daemon. The daemon +uses these environment variables while running the shell commands. +The daemon only runs shell commands that have been parsed from the config file and there is no way to +run arbitrary shell commands. The server is responsible for only sending the environment variables to the daemon and nothing else. +This seperation of responsibilities ensures security. So yes, you're safe! diff --git a/docs/swhkd.1.scd b/docs/swhkd.1.scd index 8b3f3dc..f33f9ea 100644 --- a/docs/swhkd.1.scd +++ b/docs/swhkd.1.scd @@ -6,18 +6,11 @@ swhkd - Hotkey daemon inspired by sxhkd written in Rust # SYNOPSIS -*pkexec swhkd* [_flags_] +*swhkd* [_flags_] # CONFIG FILE -The config file goes in */etc/swhkd/swhkdrc*. Since swhkd is written with a pkexec privilege escalation model in mind, we can't detect -*$XDG_CONFIG_HOME*. - -This can be avoided as such: - - Using the *include* statement in your config file. - - Using the *-c* flag to mention a custom config file at runtime. - - Symlinking the config file from *~/.config/swhkd/swhkdrc* or any other directory of choice to */etc/swhkd/swhkdrc*. - +The config file goes in *~/.config/swhkd/swhkdrc* with a fallback to */etc/swhkd/swhkdrc*. More about the config file syntax in `swhkd(5)` # OPTIONS diff --git a/docs/swhkd.5.scd b/docs/swhkd.5.scd index 47fc42b..405c21f 100644 --- a/docs/swhkd.5.scd +++ b/docs/swhkd.5.scd @@ -6,7 +6,8 @@ swhkd - Hotkey daemon inspired by sxhkd written in Rust # CONFIG FILE - - A global config can be defined in */etc/swhkd/swhkdrc*. Swhkd attempts to look in your *$XDG_CONFIG_HOME*, failing which it defaults to *~/.config*. + - A global config can be defined in *~/.config/swhkd/swhkdrc*, with a + fallback to */etc/swhkd/swhkdrc*. Swhkd attempts to look in your *$XDG_CONFIG_HOME*, failing which it defaults to *~/.config*. - A local config overrides the global one. Local configs should be placed in the root of the project. # SYNTAX diff --git a/docs/swhks.1.scd b/docs/swhks.1.scd index 4bcd640..f08f892 100644 --- a/docs/swhks.1.scd +++ b/docs/swhks.1.scd @@ -16,12 +16,6 @@ swhks - Server for swhkd, used to run user level commands over IPC. *-V*, *--version* Print version information. -*-l*, *--log* - Set a log file path. - If *XDG_DATA_HOME* exists then we use *swhks/swhks-current_time.log* relative to - it, else we use *.local/share/swhks/swhks-current_time.log* relative to the - user home directory. - *-d*, *--debug* Enable debug mode. diff --git a/swhkd/src/daemon.rs b/swhkd/src/daemon.rs index d155b3c..e23efe1 100644 --- a/swhkd/src/daemon.rs +++ b/swhkd/src/daemon.rs @@ -4,7 +4,7 @@ use config::Hotkey; use evdev::{AttributeSet, Device, InputEventKind, Key}; use nix::{ sys::stat::{umask, Mode}, - unistd::{Group, Uid}, + unistd::{setgid, setuid, Gid, Uid}, }; use signal_hook::consts::signal::*; use signal_hook_tokio::Signals; @@ -12,17 +12,18 @@ use std::{ collections::{HashMap, HashSet}, env, error::Error, - fs, - fs::Permissions, - io::prelude::*, + fs::{self, File, OpenOptions, Permissions}, + io::{Read, Write}, os::unix::{fs::PermissionsExt, net::UnixStream}, path::{Path, PathBuf}, - process::{exit, id}, + process::{exit, id, Command, Stdio}, + sync::{Arc, Mutex}, + time::{SystemTime, UNIX_EPOCH}, }; use sysinfo::{ProcessExt, System, SystemExt}; -use tokio::select; use tokio::time::Duration; use tokio::time::{sleep, Instant}; +use tokio::{select, sync::mpsc}; use tokio_stream::{StreamExt, StreamMap}; use tokio_udev::{AsyncMonitorSocket, EventType, MonitorBuilder}; @@ -51,8 +52,8 @@ struct Args { config: Option, /// Set a custom repeat cooldown duration. Default is 250ms. - #[arg(short = 'C', long)] - cooldown: Option, + #[arg(short = 'C', long, default_value_t = 250)] + cooldown: u64, /// Enable Debug Mode #[arg(short, long)] @@ -61,12 +62,15 @@ struct Args { /// Take a list of devices from the user #[arg(short = 'D', long, num_args = 0.., value_delimiter = ' ')] device: Vec, + + /// Set a custom log file. (Defaults to ${XDG_DATA_HOME:-$HOME/.local/share}/swhks-current_unix_time.log) + #[arg(short, long, value_name = "FILE")] + log: Option, } #[tokio::main] async fn main() -> Result<(), Box> { let args = Args::parse(); - let default_cooldown: u64 = 250; env::set_var("RUST_LOG", "swhkd=warn"); if args.debug { @@ -76,20 +80,168 @@ async fn main() -> Result<(), Box> { env_logger::init(); log::trace!("Logger initialized."); - let env = environ::Env::construct(); + // Just to double check that we are in root + perms::raise_privileges(); + + // Get the UID of the user that is not a system user + let invoking_uid = get_uid()?; + + log::debug!("Wating for server to start..."); + // The first and the most important request for the env + // Without this request, the environmental variables responsible for the reading for the config + // file will not be available. + // Thus, it is important to wait for the server to start before proceeding. + let mut env = environ::Env::construct(None); + let mut env_hash = 0; + loop { + match refresh_env(invoking_uid, 0) { + Ok((Some(new_env), hash)) => { + env_hash = hash; + env = new_env; + break; + } + Ok((None, hash)) => { + env_hash = hash; + log::debug!("Waiting for env..."); + continue; + } + Err(_) => {} + } + } log::trace!("Environment Aquired"); - let invoking_uid = env.pkexec_id; + // Now that we have the env, we can safely proceed with the rest of the program. + // Log handling + let log_file_name = if let Some(val) = args.log { + val + } else { + let time = match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(n) => n.as_secs().to_string(), + Err(_) => { + log::error!("SystemTime before UnixEpoch!"); + exit(1); + } + }; - setup_swhkd(invoking_uid, env.xdg_runtime_dir.clone().to_string_lossy().to_string()); + format!("{}/swhkd/swhkd-{}.log", env.fetch_xdg_data_path().to_string_lossy(), time).into() + }; - let load_config = || { - // Drop privileges to the invoking user. - perms::drop_privileges(invoking_uid); + let log_path = PathBuf::from(&log_file_name); + if let Some(p) = log_path.parent() { + if !p.exists() { + if let Err(e) = fs::create_dir_all(p) { + log::error!("Failed to create log dir: {}", e); + } + } + } + // if file doesnt exist, create it with 0666 permissions + if !log_path.exists() { + if let Err(e) = OpenOptions::new().append(true).create(true).open(&log_path) { + log::error!("Failed to create log file: {}", e); + exit(1); + } + fs::set_permissions(&log_path, Permissions::from_mode(0o666)).unwrap(); + } + + // Calculate a server cooldown at which the server will be pinged to check for env changes. + let cooldown = args.cooldown; + let delta = (cooldown as f64 * 0.1) as u64; + let server_cooldown = std::cmp::max(0, cooldown - delta); + + // Set up a channel to communicate with the server + // The channel can have upto 100 commands in the queue + let (tx, mut rx) = tokio::sync::mpsc::channel::(100); + + // We use a arc mutex to make sure that our pairs are valid and also concurrent + // while being used by the threads. + let pairs = Arc::new(Mutex::new(env.pairs.clone())); + let pairs_clone = Arc::clone(&pairs); + let log = log_path.clone(); + + // We spawn a new thread in the user space to act as the execution thread + // This again has a thread for running the env refresh module when a change is detected from + // the server. + tokio::spawn(async move { + // This is the thread that is responsible for refreshing the env + // It's sleep time is determined by the server cooldown. + tokio::spawn(async move { + loop { + { + let mut pairs = pairs_clone.lock().unwrap(); + match refresh_env(invoking_uid, env_hash) { + Ok((Some(env), hash)) => { + pairs.clone_from(&env.pairs); + env_hash = hash; + } + Ok((None, hash)) => { + env_hash = hash; + } + Err(e) => { + log::error!("Error: {}", e); + _ = Command::new("notify-send").arg(format!("ERROR {}", e)).spawn(); + exit(1); + } + } + } + sleep(Duration::from_millis(server_cooldown)).await; + } + }); + + // When we do receive a command, we spawn a new thread to execute the command + // This thread is spawned in the user space and is used to execute the command and it + // exits after the command is executed. + while let Some(command) = rx.recv().await { + // Clone the arc references to be used in the thread + let pairs = pairs.clone(); + let log = log.clone(); + + // Set the user and group id to the invoking user for the thread + setgid(Gid::from_raw(invoking_uid)).unwrap(); + setuid(Uid::from_raw(invoking_uid)).unwrap(); + + // Command execution + let mut cmd = Command::new("sh"); + cmd.arg("-c") + .arg(command) + .stdin(Stdio::null()) + .stdout(match File::open(&log) { + Ok(file) => file, + Err(e) => { + println!("Error: {}", e); + _ = Command::new("notify-send").arg(format!("ERROR {}", e)).spawn(); + exit(1); + } + }) + .stderr(match File::open(&log) { + Ok(file) => file, + Err(e) => { + println!("Error: {}", e); + _ = Command::new("notify-send").arg(format!("ERROR {}", e)).spawn(); + exit(1); + } + }); + + // Set the environment variables for the command + for (key, value) in pairs.lock().unwrap().iter() { + cmd.env(key, value); + } + + match cmd.spawn() { + Ok(_) => { + log::info!("Command executed successfully."); + } + Err(e) => log::error!("Failed to execute command: {}", e), + } + } + }); - let config_file_path: PathBuf = - args.config.as_ref().map_or_else(|| env.fetch_xdg_config_path(), |file| file.clone()); + // With the threads responsible for refresh and execution being in place, we can finally + // start the main loop of the program. + setup_swhkd(invoking_uid, env.xdg_runtime_dir(invoking_uid)); + let config_file_path: PathBuf = + args.config.as_ref().map_or_else(|| env.fetch_xdg_config_path(), |file| file.clone()); + let load_config = || { log::debug!("Using config file path: {:#?}", config_file_path); match config::load(&config_file_path) { @@ -97,11 +249,7 @@ async fn main() -> Result<(), Box> { log::error!("Config Error: {}", e); exit(1) } - Ok(out) => { - // Escalate back to the root user after reading the config file. - perms::raise_privileges(); - out - } + Ok(out) => out, } }; @@ -161,11 +309,11 @@ async fn main() -> Result<(), Box> { (Key::KEY_RIGHTSHIFT, config::Modifier::Shift), ]); - let repeat_cooldown_duration: u64 = args.cooldown.unwrap_or(default_cooldown); + let repeat_cooldown_duration: u64 = args.cooldown; let mut signals = Signals::new([ - SIGUSR1, SIGUSR2, SIGHUP, SIGABRT, SIGBUS, SIGCHLD, SIGCONT, SIGINT, SIGPIPE, SIGQUIT, - SIGSYS, SIGTERM, SIGTRAP, SIGTSTP, SIGVTALRM, SIGXCPU, SIGXFSZ, + SIGUSR1, SIGUSR2, SIGHUP, SIGABRT, SIGBUS, SIGCONT, SIGINT, SIGPIPE, SIGQUIT, SIGSYS, + SIGTERM, SIGTRAP, SIGTSTP, SIGVTALRM, SIGXCPU, SIGXFSZ, ])?; let mut execution_is_paused = false; @@ -190,8 +338,6 @@ async fn main() -> Result<(), Box> { let hotkey_repeat_timer = sleep(Duration::from_millis(0)); tokio::pin!(hotkey_repeat_timer); - // The socket we're sending the commands to. - let socket_file_path = env.fetch_xdg_runtime_socket_path(); loop { select! { _ = &mut hotkey_repeat_timer, if &last_hotkey.is_some() => { @@ -199,10 +345,12 @@ async fn main() -> Result<(), Box> { if hotkey.keybinding.on_release { continue; } - send_command(hotkey.clone(), &socket_file_path, &modes, &mut mode_stack); + send_command(hotkey.clone(), &modes, &mut mode_stack, tx.clone()).await; hotkey_repeat_timer.as_mut().reset(Instant::now() + Duration::from_millis(repeat_cooldown_duration)); } + + Some(signal) = signals.next() => { match signal { SIGUSR1 => { @@ -319,7 +467,7 @@ async fn main() -> Result<(), Box> { 0 => { if last_hotkey.is_some() && pending_release { pending_release = false; - send_command(last_hotkey.clone().unwrap(), &socket_file_path, &modes, &mut mode_stack); + send_command(last_hotkey.clone().unwrap(), &modes, &mut mode_stack, tx.clone()).await; last_hotkey = None; } if let Some(modifier) = modifiers_map.get(&key) { @@ -382,7 +530,7 @@ async fn main() -> Result<(), Box> { pending_release = true; break; } - send_command(hotkey.clone(), &socket_file_path, &modes, &mut mode_stack); + send_command(hotkey.clone(), &modes, &mut mode_stack, tx.clone()).await; hotkey_repeat_timer.as_mut().reset(Instant::now() + Duration::from_millis(repeat_cooldown_duration)); continue; } @@ -392,32 +540,6 @@ async fn main() -> Result<(), Box> { } } -fn socket_write(command: &str, socket_path: PathBuf) -> Result<(), Box> { - let mut stream = UnixStream::connect(socket_path)?; - stream.write_all(command.as_bytes())?; - Ok(()) -} - -pub fn check_input_group() -> Result<(), Box> { - if !Uid::current().is_root() { - let groups = nix::unistd::getgroups(); - for groups in groups.iter() { - for group in groups { - let group = Group::from_gid(*group); - if group.unwrap().unwrap().name == "input" { - log::error!("Note: INVOKING USER IS IN INPUT GROUP!!!!"); - log::error!("THIS IS A HUGE SECURITY RISK!!!!"); - } - } - } - log::error!("Consider using `pkexec swhkd ...`"); - exit(1); - } else { - log::warn!("Running swhkd as root!"); - Ok(()) - } -} - pub fn check_device_is_keyboard(device: &Device) -> bool { if device.supported_keys().map_or(false, |keys| keys.contains(Key::KEY_ENTER)) { if device.name() == Some("swhkd virtual output") { @@ -431,7 +553,7 @@ pub fn check_device_is_keyboard(device: &Device) -> bool { } } -pub fn setup_swhkd(invoking_uid: u32, runtime_path: String) { +pub fn setup_swhkd(invoking_uid: u32, runtime_path: PathBuf) { // Set a sane process umask. log::trace!("Setting process umask."); umask(Mode::S_IWGRP | Mode::S_IWOTH); @@ -451,7 +573,7 @@ pub fn setup_swhkd(invoking_uid: u32, runtime_path: String) { } // Get the PID file path for instance tracking. - let pidfile: String = format!("{}swhkd_{}.pid", runtime_path, invoking_uid); + let pidfile: String = format!("{}/swhkd_{}.pid", runtime_path.to_string_lossy(), invoking_uid); if Path::new(&pidfile).exists() { log::trace!("Reading {} file and checking for running instances.", pidfile); let swhkd_pid = match fs::read_to_string(&pidfile) { @@ -484,21 +606,16 @@ pub fn setup_swhkd(invoking_uid: u32, runtime_path: String) { exit(1); } } - - // Check if the user is in input group. - if check_input_group().is_err() { - exit(1); - } } -pub fn send_command( +pub async fn send_command( hotkey: Hotkey, - socket_path: &Path, modes: &[config::Mode], mode_stack: &mut Vec, + tx: mpsc::Sender, ) { log::info!("Hotkey pressed: {:#?}", hotkey); - let command = hotkey.command; + let mut command = hotkey.command; if modes[*mode_stack.last().unwrap()].options.oneoff { mode_stack.pop(); } @@ -515,9 +632,77 @@ pub fn send_command( } } } - if let Err(e) = socket_write(&command, socket_path.to_path_buf()) { - log::error!("Failed to send command to swhks through IPC."); - log::error!("Please make sure that swhks is running."); - log::error!("Err: {:#?}", e) - }; + if command.ends_with(" &&") { + command = command.strip_suffix(" &&").unwrap().to_string(); + } + + match tx.send(command).await { + Ok(_) => {} + Err(e) => { + log::error!("Failed to send command: {}", e); + } + } +} + +/// Get the UID of the user that is not a system user +fn get_uid() -> Result> { + let status_content = fs::read_to_string(format!("/proc/{}/loginuid", std::process::id()))?; + let uid = status_content.trim().parse::()?; + Ok(uid) +} + +fn get_file_paths(runtime_dir: &str) -> (String, String) { + let pid_file_path = format!("{}/swhks.pid", runtime_dir); + let sock_file_path = format!("{}/swhkd.sock", runtime_dir); + + (pid_file_path, sock_file_path) +} + +/// Refreshes the environment variables from the server +pub fn refresh_env( + invoking_uid: u32, + prev_hash: u64, +) -> Result<(Option, u64), Box> { + // A simple placeholder for the env that is to be refreshed + let env = environ::Env::construct(None); + + let (_pid_path, sock_path) = + get_file_paths(env.xdg_runtime_dir(invoking_uid).to_str().unwrap()); + + let mut buff: String = String::new(); + + // Follows a two part process to recieve the env hash and the env itself + // First part: Send a "1" as a byte to the socket to request the hash + if let Ok(mut stream) = UnixStream::connect(&sock_path) { + let n = stream.write(&[1])?; + if n != 1 { + log::error!("Failed to write to socket."); + return Ok((None, prev_hash)); + } + stream.read_to_string(&mut buff)?; + } + + let env_hash = buff.parse().unwrap_or_default(); + + // If the hash is the same as the previous hash, return early + // no need to refresh the env + if env_hash == prev_hash { + return Ok((None, prev_hash)); + } + + // Now that we know the env hash is different, we can request the env + // Second part: Send a "2" as a byte to the socket to request the env + if let Ok(mut stream) = UnixStream::connect(&sock_path) { + let n = stream.write(&[2])?; + if n != 1 { + log::error!("Failed to write to socket."); + return Ok((None, prev_hash)); + } + stream.read_to_string(&mut buff)?; + } + + log::info!("Env refreshed"); + + // Construct the env from the recieved env and return it + Ok((Some(environ::Env::construct(Some(&buff))), env_hash)) } diff --git a/swhkd/src/environ.rs b/swhkd/src/environ.rs index 3d2e9c6..4f92e05 100644 --- a/swhkd/src/environ.rs +++ b/swhkd/src/environ.rs @@ -1,141 +1,73 @@ -use std::{ - env::VarError, - path::{Path, PathBuf}, -}; +use std::{collections::HashMap, error::Error, path::PathBuf, process::Command}; +#[derive(Debug, Clone)] pub struct Env { - pub pkexec_id: u32, - pub xdg_config_home: PathBuf, - pub xdg_runtime_socket: PathBuf, - pub xdg_runtime_dir: PathBuf, -} - -#[derive(Debug)] -pub enum EnvError { - PkexecNotFound, - XdgConfigNotFound, - XdgRuntimeNotFound, - PathNotFound, - GenericError(String), + pub pairs: HashMap, } impl Env { - pub fn construct() -> Self { - let pkexec_id = match Self::get_env("PKEXEC_UID") { - Ok(val) => match val.parse::() { - Ok(val) => val, - Err(_) => { - log::error!("Failed to launch swhkd!!!"); - log::error!("Make sure to launch the binary with pkexec."); - std::process::exit(1); - } - }, - Err(_) => { - log::error!("Failed to launch swhkd!!!"); - log::error!("Make sure to launch the binary with pkexec."); - std::process::exit(1); - } - }; + fn get_env() -> Result> { + let cmd = Command::new("env").output()?; + let stdout = String::from_utf8(cmd.stdout)?; + Ok(stdout) + } - let xdg_config_home = match Self::get_env("XDG_CONFIG_HOME") { - Ok(val) => match validate_path(&PathBuf::from(val)) { - Ok(val) => val, - Err(e) => match e { - EnvError::PathNotFound => { - log::warn!("XDG_CONFIG_HOME does not exist, using hardcoded /etc"); - PathBuf::from("/etc") - } - _ => { - eprintln!("Failed to get XDG_CONFIG_HOME: {:?}", e); - std::process::exit(1); - } - }, - }, - Err(e) => match e { - EnvError::XdgConfigNotFound => { - log::warn!("XDG_CONFIG_HOME not found, using hardcoded /etc"); - PathBuf::from("/etc") - } - _ => { - eprintln!("Failed to get XDG_CONFIG_HOME: {:?}", e); - std::process::exit(1); - } - }, - }; + fn parse_env(env: &str) -> HashMap { + let mut pairs = HashMap::new(); + for line in env.lines() { + let mut parts = line.splitn(2, '='); + if let (Some(key), Some(value)) = (parts.next(), parts.next()) { + pairs.insert(key.to_string(), value.to_string()); + } + } + pairs + } - let xdg_runtime_socket = match Self::get_env("XDG_RUNTIME_DIR") { - Ok(val) => match validate_path(&PathBuf::from(val).join("swhkd.sock")) { - Ok(val) => val, - Err(e) => match e { - EnvError::PathNotFound => { - log::warn!("XDG_RUNTIME_DIR does not exist, using hardcoded /run/user"); - PathBuf::from(format!("/run/user/{}", pkexec_id)) - } - _ => { - eprintln!("Failed to get XDG_RUNTIME_DIR: {:?}", e); - std::process::exit(1); - } - }, - }, - Err(e) => match e { - EnvError::XdgRuntimeNotFound => { - log::warn!("XDG_RUNTIME_DIR not found, using hardcoded /run/user"); - PathBuf::from(format!("/run/user/{}", pkexec_id)) - } - _ => { - eprintln!("Failed to get XDG_RUNTIME_DIR: {:?}", e); - std::process::exit(1); - } - }, + /// Construct the env from the environment variables + pub fn construct(env: Option<&str>) -> Self { + let env = match env { + Some(env) => env.to_string(), + None => Self::get_env().unwrap(), }; + let pairs = Self::parse_env(&env); + Self { pairs } + } - let xdg_runtime_dir = match Self::get_env("XDG_RUNTIME_DIR") { - Ok(val) => PathBuf::from(val), - Err(e) => match e { - EnvError::XdgRuntimeNotFound => { - log::warn!("XDG_RUNTIME_DIR not found, using hardcoded /run/user"); - PathBuf::from(format!("/run/user/{}", pkexec_id)) - } - _ => { - eprintln!("Failed to get XDG_RUNTIME_DIR: {:?}", e); - std::process::exit(1); - } - }, + pub fn fetch_home(&self) -> Option { + let home = match self.pairs.get("HOME") { + Some(it) => it, + None => return None, }; - - Self { pkexec_id, xdg_config_home, xdg_runtime_dir, xdg_runtime_socket } + Some(PathBuf::from(home)) } - fn get_env(name: &str) -> Result { - match std::env::var(name) { - Ok(val) => Ok(val), - Err(e) => match e { - VarError::NotPresent => match name { - "PKEXEC_UID" => Err(EnvError::PkexecNotFound), - "XDG_CONFIG_HOME" => Err(EnvError::XdgConfigNotFound), - "XDG_RUNTIME_DIR" => Err(EnvError::XdgRuntimeNotFound), - _ => Err(EnvError::GenericError(e.to_string())), - }, - VarError::NotUnicode(_) => { - Err(EnvError::GenericError("Not a valid unicode".to_string())) - } - }, + pub fn fetch_xdg_config_path(&self) -> PathBuf { + let default = match self.fetch_home() { + Some(x) => x.join(".config"), + None => PathBuf::from("/etc"), } - } + .to_string_lossy() + .to_string(); + let xdg_config_home = self.pairs.get("XDG_CONFIG_HOME").unwrap_or(&default); - pub fn fetch_xdg_config_path(&self) -> PathBuf { - PathBuf::from(&self.xdg_config_home).join("swhkd/swhkdrc") + PathBuf::from(xdg_config_home).join("swhkd").join("swhkdrc") } - pub fn fetch_xdg_runtime_socket_path(&self) -> PathBuf { - PathBuf::from(&self.xdg_runtime_dir).join("swhkd.sock") + pub fn fetch_xdg_data_path(&self) -> PathBuf { + let default = match self.fetch_home() { + Some(x) => x.join(".local").join("share"), + None => PathBuf::from("/etc"), + } + .to_string_lossy() + .to_string(); + let xdg_config_home = self.pairs.get("XDG_DATA_HOME").unwrap_or(&default); + + PathBuf::from(xdg_config_home) } -} -fn validate_path(path: &Path) -> Result { - if path.exists() { - Ok(path.to_path_buf()) - } else { - Err(EnvError::PathNotFound) + pub fn xdg_runtime_dir(&self, uid: u32) -> PathBuf { + let default = format!("/run/user/{}", uid); + let xdg_runtime_dir = self.pairs.get("XDG_RUNTIME_DIR").unwrap_or(&default); + PathBuf::from(xdg_runtime_dir) } } diff --git a/swhkd/src/perms.rs b/swhkd/src/perms.rs index cd5a80b..3b17c60 100644 --- a/swhkd/src/perms.rs +++ b/swhkd/src/perms.rs @@ -1,7 +1,7 @@ use nix::unistd::{Gid, Uid, User}; use std::process::exit; -pub fn drop_privileges(user_uid: u32) { +pub fn _drop_privileges(user_uid: u32) { let user_uid = Uid::from_raw(user_uid); let user = User::from_uid(user_uid).unwrap().unwrap(); diff --git a/swhks/src/ipc.rs b/swhks/src/ipc.rs new file mode 100644 index 0000000..df4479f --- /dev/null +++ b/swhks/src/ipc.rs @@ -0,0 +1,73 @@ +use std::{ + hash::{DefaultHasher, Hash, Hasher}, + io::{Read, Write}, + os::unix::net::UnixListener, + process::Command, +}; + +/// Get the environment variables +/// These would be requested from the default shell to make sure that the environment is up-to-date +fn get_env() -> Result> { + let shell = std::env::var("SHELL")?; + let cmd = Command::new(shell).arg("-c").arg("env").output()?; + let stdout = String::from_utf8(cmd.stdout)?; + Ok(stdout) +} + +/// Calculates a simple hash of the string +/// Uses the DefaultHasher from the std::hash module which is not a cryptographically secure hash, +/// however, it is good enough for our use case. +pub fn calculate_hash(t: String) -> u64 { + let mut hasher = DefaultHasher::new(); + t.hash(&mut hasher); + hasher.finish() +} + +pub fn server_loop(sock_file_path: &str) -> std::io::Result<()> { + let mut prev_hash = calculate_hash(String::new()); + + let listener = UnixListener::bind(sock_file_path)?; + // Init a buffer to read the incoming message + let mut buff = [0; 1]; + log::debug!("Listening for incoming connections..."); + + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + stream.read_exact(&mut buff)?; + // If the buffer is [1] then it is a VERIFY message + // the hash of the environment variables is sent back to the client + // then the stream is flushed and the loop continues + if buff == [1] { + log::debug!("Received VERIFY message from swhkd"); + let _ = stream.write_all(prev_hash.to_string().as_bytes()); + log::debug!("Sent hash to swhkd"); + stream.flush()?; + continue; + } + // If the buffer is [2] then it is a GET message + // the environment variables are sent back to the client + // then the stream is flushed and the loop continues + if buff == [2] { + log::debug!("Received GET message from swhkd"); + let env = get_env().unwrap(); + if prev_hash == calculate_hash(env.clone()) { + log::debug!("No changes in environment variables"); + } else { + log::debug!("Changes in environment variables"); + } + prev_hash = calculate_hash(env.clone()); + let _ = stream.write_all(env.as_bytes()); + stream.flush()?; + continue; + } + } + Err(e) => { + log::error!("Error: {}", e); + break; + } + } + } + + Ok(()) +} diff --git a/swhks/src/main.rs b/swhks/src/main.rs index f66fc32..38ba79d 100644 --- a/swhks/src/main.rs +++ b/swhks/src/main.rs @@ -1,30 +1,25 @@ -use clap::Parser; -use environ::Env; -use nix::{ - sys::stat::{umask, Mode}, - unistd::daemon, -}; -use std::io::Read; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::error::Error; +use std::fs::Permissions; use std::{ - env, fs, - fs::OpenOptions, - os::unix::net::UnixListener, + fs::{self}, path::{Path, PathBuf}, - process::{exit, id, Command, Stdio}, }; -use sysinfo::{ProcessExt, System, SystemExt}; -mod environ; +use clap::Parser; +use std::{ + env, + os::unix::fs::PermissionsExt, + process::{exit, id}, +}; +use sysinfo::System; +use sysinfo::{ProcessExt, SystemExt}; + +mod ipc; /// IPC Server for swhkd #[derive(Parser)] #[command(version, about, long_about = None)] struct Args { - /// Set a custom log file. (Defaults to ${XDG_DATA_HOME:-$HOME/.local/share}/swhks-current_unix_time.log) - #[arg(short, long, value_name = "FILE")] - log: Option, - /// Enable Debug Mode #[arg(short, long)] debug: bool, @@ -40,40 +35,47 @@ fn main() -> std::io::Result<()> { .init(); } - log::trace!("Setting process umask."); - umask(Mode::S_IWGRP | Mode::S_IWOTH); + let invoking_uid = get_uid().unwrap(); + let runtime_dir = format!("/run/user/{}", invoking_uid); - // This is used to initialize the environment variables only once - let environment = environ::Env::construct(); + let (_pid_file_path, sock_file_path) = get_file_paths(&runtime_dir); - let (pid_file_path, sock_file_path) = get_file_paths(&environment); + log::info!("Started SWHKS placeholder server"); - let log_file_name = if let Some(val) = args.log { - val - } else { - let time = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(n) => n.as_secs().to_string(), - Err(_) => { - log::error!("SystemTime before UnixEpoch!"); - exit(1); - } - }; + // Daemonize the process + let _ = nix::unistd::daemon(true, false); + + setup_swhks(invoking_uid, PathBuf::from(runtime_dir)); + + if Path::new(&sock_file_path).exists() { + fs::remove_file(&sock_file_path)?; + } - format!("{}/swhks/swhks-{}.log", environment.data_home.to_string_lossy(), time).into() - }; + ipc::server_loop(&sock_file_path)?; - let log_path = Path::new(&log_file_name); - if let Some(p) = log_path.parent() { - if !p.exists() { - if let Err(e) = fs::create_dir_all(p) { - log::error!("Failed to create log dir: {}", e); + Ok(()) +} + +pub fn setup_swhks(invoking_uid: u32, runtime_path: PathBuf) { + // Get the runtime path and create it if needed. + if !Path::new(&runtime_path).exists() { + match fs::create_dir_all(Path::new(&runtime_path)) { + Ok(_) => { + log::debug!("Created runtime directory."); + match fs::set_permissions(Path::new(&runtime_path), Permissions::from_mode(0o600)) { + Ok(_) => log::debug!("Set runtime directory to readonly."), + Err(e) => log::error!("Failed to set runtime directory to readonly: {}", e), + } } + Err(e) => log::error!("Failed to create runtime directory: {}", e), } } - if Path::new(&pid_file_path).exists() { - log::trace!("Reading {} file and checking for running instances.", pid_file_path); - let swhks_pid = match fs::read_to_string(&pid_file_path) { + // Get the PID file path for instance tracking. + let pidfile: String = format!("{}/swhks_{}.pid", runtime_path.to_string_lossy(), invoking_uid); + if Path::new(&pidfile).exists() { + log::trace!("Reading {} file and checking for running instances.", pidfile); + let swhks_pid = match fs::read_to_string(&pidfile) { Ok(swhks_pid) => swhks_pid, Err(e) => { log::error!("Unable to read {} to check all running instances", e); @@ -82,83 +84,38 @@ fn main() -> std::io::Result<()> { }; log::debug!("Previous PID: {}", swhks_pid); + // Check if swhkd is already running! let mut sys = System::new_all(); sys.refresh_all(); for (pid, process) in sys.processes() { if pid.to_string() == swhks_pid && process.exe() == env::current_exe().unwrap() { - log::error!("Server is already running!"); + log::error!("Swhks is already running!"); + log::error!("There is no need to run another instance since there is already one running with PID: {}", swhks_pid); exit(1); } } } - if Path::new(&sock_file_path).exists() { - log::trace!("Sockfile exists, attempting to remove it."); - match fs::remove_file(&sock_file_path) { - Ok(_) => { - log::debug!("Removed old socket file"); - } - Err(e) => { - log::error!("Error removing the socket file!: {}", e); - log::error!("You can manually remove the socket file: {}", sock_file_path); - exit(1); - } - }; - } - - match fs::write(&pid_file_path, id().to_string()) { + // Write to the pid file. + match fs::write(&pidfile, id().to_string()) { Ok(_) => {} Err(e) => { - log::error!("Unable to write to {}: {}", pid_file_path, e); + log::error!("Unable to write to {}: {}", pidfile, e); exit(1); } } - - let listener = UnixListener::bind(sock_file_path)?; - loop { - match listener.accept() { - Ok((mut socket, address)) => { - let mut response = String::new(); - socket.read_to_string(&mut response)?; - run_system_command(&response, log_path); - log::debug!("Socket: {:?} Address: {:?} Response: {}", socket, address, response); - } - Err(e) => log::error!("accept function failed: {:?}", e), - } - } } -fn get_file_paths(env: &Env) -> (String, String) { - let pid_file_path = format!("{}/swhks.pid", env.runtime_dir.to_string_lossy()); - let sock_file_path = format!("{}/swhkd.sock", env.runtime_dir.to_string_lossy()); +fn get_file_paths(runtime_dir: &str) -> (String, String) { + let pid_file_path = format!("{}/swhks.pid", runtime_dir); + let sock_file_path = format!("{}/swhkd.sock", runtime_dir); (pid_file_path, sock_file_path) } -fn run_system_command(command: &str, log_path: &Path) { - _ = daemon(true, false); - - if let Err(e) = Command::new("sh") - .arg("-c") - .arg(command) - .stdin(Stdio::null()) - .stdout(match OpenOptions::new().append(true).create(true).open(log_path) { - Ok(file) => file, - Err(e) => { - _ = Command::new("notify-send").arg(format!("ERROR {}", e)).spawn(); - exit(1); - } - }) - .stderr(match OpenOptions::new().append(true).create(true).open(log_path) { - Ok(file) => file, - Err(e) => { - _ = Command::new("notify-send").arg(format!("ERROR {}", e)).spawn(); - exit(1); - } - }) - .spawn() - { - log::error!("Failed to execute {}", command); - log::error!("Error: {}", e); - } +/// Get the UID of the user that is not a system user +fn get_uid() -> Result> { + let status_content = fs::read_to_string(format!("/proc/{}/loginuid", std::process::id()))?; + let uid = status_content.trim().parse::()?; + Ok(uid) }