diff --git a/crates/ahfail-display/Cargo.toml b/crates/ahfail-display/Cargo.toml index 688eaa1..7727fc5 100644 --- a/crates/ahfail-display/Cargo.toml +++ b/crates/ahfail-display/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" name = "ahfail-display" [dependencies] -ahfail-ui = { path = "../ahfail-ui" } +ahfail-ui = { path = "../ahfail-ui", default-features = false } gtk = { version = "0.15", package = "gtk", features = ["v3_24"] } gdk = { version = "0.15", package = "gdk", features = ["v3_24"] } gstreamer = { version = "0.18", package = "gstreamer", features = ["v1_18"] } @@ -15,6 +15,10 @@ glib = { version = "0.15", package = "glib" } cairo = { version = "0.15", package = "cairo-rs" } libc = "0.2" +[features] +default = ["update-check"] +update-check = ["ahfail-ui/update-check"] + [build-dependencies] cc = "1" pkg-config = "0.3" diff --git a/crates/ahfail-display/src/main.rs b/crates/ahfail-display/src/main.rs index 623ab70..368e9d0 100644 --- a/crates/ahfail-display/src/main.rs +++ b/crates/ahfail-display/src/main.rs @@ -78,15 +78,30 @@ mod x11_grab { } } +fn display_lock_path() -> std::path::PathBuf { + match std::env::var("XDG_RUNTIME_DIR") { + Ok(dir) if !dir.is_empty() => std::path::Path::new(&dir).join("ahfail-display.lock"), + _ => std::path::PathBuf::from(format!("/tmp/ahfail-display-{}.lock", unsafe { libc::getuid() })), + } +} + fn main() { unsafe { libc::signal(libc::SIGTERM, handle_sigterm as *const () as libc::sighandler_t); } - // Single-instance guard + // Never run as root (the PAM module refuses to spawn us there, but the + // binary can also be started by hand). + if unsafe { libc::getuid() == 0 || libc::geteuid() == 0 } { + eprintln!("[ahfail] ahfail-display refuses to run as root"); + return; + } + + // Single-instance guard, per user: a fixed name in the shared /tmp could be + // pre-created by another user to block us. let lock_file = std::fs::OpenOptions::new() .create(true).write(true) - .open("/tmp/ahfail-display.lock"); + .open(display_lock_path()); let lock_file = match lock_file { Ok(f) => f, Err(_) => return, diff --git a/crates/ahfail-gtklock/Cargo.toml b/crates/ahfail-gtklock/Cargo.toml index 539330f..e9a14e0 100644 --- a/crates/ahfail-gtklock/Cargo.toml +++ b/crates/ahfail-gtklock/Cargo.toml @@ -8,7 +8,7 @@ name = "ahfail_module" crate-type = ["staticlib", "rlib"] [dependencies] -ahfail-ui = { path = "../ahfail-ui" } +ahfail-ui = { path = "../ahfail-ui", default-features = false } gtk = { version = "0.15", package = "gtk", features = ["v3_24"] } gdk = { version = "0.15", package = "gdk", features = ["v3_24"] } gstreamer = { version = "0.18", package = "gstreamer", features = ["v1_18"] } @@ -17,5 +17,9 @@ glib = { version = "0.15", package = "glib" } gdk-pixbuf = "0.15" rand = "0.8" +[features] +default = ["update-check"] +update-check = ["ahfail-ui/update-check"] + [build-dependencies] pkg-config = "0.3" diff --git a/crates/ahfail-pam/src/lib.rs b/crates/ahfail-pam/src/lib.rs index 3ac0705..424bde8 100644 --- a/crates/ahfail-pam/src/lib.rs +++ b/crates/ahfail-pam/src/lib.rs @@ -56,7 +56,13 @@ pub unsafe extern "C" fn pam_sm_authenticate( .map(|s| s.trim().to_string()) .unwrap_or_default(); - spawn_display(display_path.clone(), &locker_name); + // Never start a GTK program as root. Inside a greeter or display manager + // (lightdm and friends) this module runs in a root process; spawning the + // display there would run a whole GUI stack with root rights. Skip the effect + // and let authentication carry on untouched. + if !running_as_root() { + spawn_display(display_path.clone(), &locker_name); + } let key = match CString::new("dk.weircon.ahfail") { Ok(k) => k, @@ -98,6 +104,11 @@ pub unsafe extern "C" fn pam_sm_chauthtok( _pamh: *mut PamHandle, _flags: c_int, _argc: c_int, _argv: *const *const c_char, ) -> c_int { PAM_IGNORE } +/// True if either the real or the effective uid is root. +fn running_as_root() -> bool { + unsafe { libc::getuid() == 0 || libc::geteuid() == 0 } +} + unsafe fn argc_argv<'a>(argc: c_int, argv: *const *const c_char) -> &'a [*const c_char] { if argv.is_null() || argc <= 0 { return &[]; } std::slice::from_raw_parts(argv, argc as usize) diff --git a/crates/ahfail-pam/tests/pam_tests.rs b/crates/ahfail-pam/tests/pam_tests.rs deleted file mode 100644 index 48874cb..0000000 --- a/crates/ahfail-pam/tests/pam_tests.rs +++ /dev/null @@ -1,21 +0,0 @@ -use ahfail_pam::{is_failure, is_success, is_replace, PAM_AUTH_ERR, PAM_SUCCESS, PAM_DATA_REPLACE}; - -#[test] -fn status_classification() { - assert!(is_failure(PAM_AUTH_ERR)); - assert!(is_success(PAM_SUCCESS)); - assert!(!is_failure(PAM_SUCCESS)); - assert!(!is_success(PAM_AUTH_ERR)); -} - -#[test] -fn replace_flag_detected() { - let replace_status = PAM_AUTH_ERR | PAM_DATA_REPLACE; - assert!(is_replace(replace_status)); - assert!(!is_replace(PAM_SUCCESS)); -} - -#[test] -fn display_path_default_is_set() { - assert!(!ahfail_pam::default_display_path().is_empty()); -} diff --git a/crates/ahfail-ui/Cargo.toml b/crates/ahfail-ui/Cargo.toml index 7097b94..a7b4009 100644 --- a/crates/ahfail-ui/Cargo.toml +++ b/crates/ahfail-ui/Cargo.toml @@ -16,8 +16,15 @@ glib = { version = "0.15", package = "glib" } gio = { version = "0.15", package = "gio" } gdk-pixbuf = "0.15" rand = "0.8" -ureq = "2" +ureq = { version = "2", optional = true } libc = "0.2" +[features] +# Checks Gitea for a newer release once a day and shows a desktop notification. +# Distro packages build without it (--no-default-features): there the package +# manager handles updates, and a locker should not phone home. +default = ["update-check"] +update-check = ["dep:ureq"] + [dev-dependencies] tempfile = "3" diff --git a/crates/ahfail-ui/src/update.rs b/crates/ahfail-ui/src/update.rs index 41ae421..fb6d15a 100644 --- a/crates/ahfail-ui/src/update.rs +++ b/crates/ahfail-ui/src/update.rs @@ -1,16 +1,29 @@ +//! Optional update check (cargo feature `update-check`, on by default). +//! Without the feature `check_for_update` is a no-op and nothing touches the +//! network, which is how distro packages are built. + +#[cfg(feature = "update-check")] use std::path::PathBuf; +#[cfg(feature = "update-check")] use std::process::Command; +#[cfg(feature = "update-check")] use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "update-check")] use std::time::SystemTime; +#[cfg(feature = "update-check")] use std::fs; +#[cfg(feature = "update-check")] const GITEA_API: &str = "https://gitea.weircon.dk/api/v1/repos/agw/gtk-ahfail/releases/latest"; +#[cfg(feature = "update-check")] const UPDATE_NOTIFY_MSG: &str = "Update available — visit https://gitea.weircon.dk/agw/gtk-ahfail/releases"; +#[cfg(feature = "update-check")] const CHECK_INTERVAL_SECS: u64 = 86_400; // Guards against multiple windows spawning duplicate checks on the same failure. +#[cfg(feature = "update-check")] static CHECK_IN_FLIGHT: AtomicBool = AtomicBool::new(false); pub fn is_newer(latest: &str, current: &str) -> bool { @@ -26,6 +39,7 @@ pub fn is_newer(latest: &str, current: &str) -> bool { } } +#[cfg(feature = "update-check")] fn cache_file() -> PathBuf { let dir = std::env::var("HOME") .map(|h| PathBuf::from(h).join(".cache/ahfail")) @@ -37,6 +51,7 @@ fn cache_file() -> PathBuf { dir.join("last_update_check") } +#[cfg(feature = "update-check")] fn rate_limited() -> bool { let path = cache_file(); if let Ok(meta) = fs::metadata(&path) { @@ -50,10 +65,12 @@ fn rate_limited() -> bool { false } +#[cfg(feature = "update-check")] fn touch_cache() { let _ = fs::write(cache_file(), b""); } +#[cfg(feature = "update-check")] fn send_notification() { #[cfg(target_os = "linux")] let _ = Command::new("notify-send") @@ -69,6 +86,7 @@ fn send_notification() { .status(); } +#[cfg(feature = "update-check")] /// Run in a background thread. Checks Gitea for a newer release and sends a /// desktop notification if one exists. Rate-limited to once per 24 hours. /// Fails silently on any error. @@ -104,6 +122,7 @@ pub fn check_for_update(current_version: &str) { } } +#[cfg(feature = "update-check")] fn extract_tag_name(json: &str) -> Option { // Handle both compact ("tag_name":"v1.0") and spaced ("tag_name": "v1.0") JSON. let key = "\"tag_name\""; @@ -113,3 +132,7 @@ fn extract_tag_name(json: &str) -> Option { let end = rest.find('"')?; Some(rest[..end].to_string()) } + +/// Built without the `update-check` feature: never contacts the network. +#[cfg(not(feature = "update-check"))] +pub fn check_for_update(_current_version: &str) {}