Make the update check optional and harden the legacy paths
The Gitea update check is now the default feature update-check of ahfail-ui; distro builds drop it, and the HTTP client with it. ahfail-display refuses to run as root and keeps its lock file in XDG_RUNTIME_DIR instead of a fixed name in the shared /tmp, and the PAM module no longer spawns it from a root context such as a greeter. Removes pam_tests.rs, which tested helpers dropped in v0.9.0 and has not compiled since. Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -56,7 +56,13 @@ pub unsafe extern "C" fn pam_sm_authenticate(
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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<String> {
|
||||
// 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<String> {
|
||||
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) {}
|
||||
|
||||
Reference in New Issue
Block a user