use std::time::{Duration, Instant};
use zbus::blocking::{Connection, Proxy};
use zbus::zvariant::OwnedObjectPath;
const LOGIN_DEST: &str = "org.freedesktop.login1";
const LOGIN_PATH: &str = "/org/freedesktop/login1";
const LOGIN_MANAGER: &str = "org.freedesktop.login1.Manager";
const LOGIN_SESSION: &str = "org.freedesktop.login1.Session";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GnomeSession {
pub id: String,
pub started_at_micros: u64,
pub user: String,
pub uid: u32,
pub seat: String,
pub lock_state: LockState,
}
impl GnomeSession {
pub fn selector(&self) -> String {
format!("linux-logind:{}:{}:{}", self.id, self.uid, self.started_at_micros)
}
pub fn same_login(&self, current: &Self) -> bool {
self.id == current.id
&& self.started_at_micros == current.started_at_micros
&& self.uid == current.uid
&& self.user == current.user
&& self.seat == current.seat
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LockState {
Locked,
Usable,
Unknown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnlockOutcome {
AlreadyUsable,
UnlockedExistingSession,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum UnlockError {
#[error("logind session inventory is unavailable")]
ServiceUnavailable,
#[error("the selected GNOME session is stale")]
StaleSession,
#[error("the selected session is not a physical GNOME Wayland session owned by this host identity")]
UnsupportedOsState,
#[error("logind session unlock could not be verified")]
OutcomeUnverified,
}
pub fn list_user_sessions() -> Result<Vec<GnomeSession>, UnlockError> {
let system = system_connection()?;
let manager = Proxy::new(&system, LOGIN_DEST, LOGIN_PATH, LOGIN_MANAGER).map_err(|_| UnlockError::ServiceUnavailable)?;
let sessions: Vec<(String, u32, String, String, OwnedObjectPath)> =
manager.call("ListSessions", &()).map_err(|_| UnlockError::ServiceUnavailable)?;
let effective_uid = unsafe { libc::geteuid() };
let mut eligible = Vec::new();
for (id, uid, user, _seat, path) in sessions {
if uid != effective_uid {
continue;
}
let session = Proxy::new(&system, LOGIN_DEST, path.as_str(), LOGIN_SESSION).map_err(|_| UnlockError::ServiceUnavailable)?;
let current_id: String = session.get_property("Id").map_err(|_| UnlockError::ServiceUnavailable)?;
let (current_uid, _): (u32, OwnedObjectPath) = session.get_property("User").map_err(|_| UnlockError::ServiceUnavailable)?;
let current_user: String = session.get_property("Name").map_err(|_| UnlockError::ServiceUnavailable)?;
if current_id != id || current_uid != uid || current_user != user {
return Err(UnlockError::StaleSession);
}
let kind: String = session.get_property("Type").map_err(|_| UnlockError::ServiceUnavailable)?;
let class: String = session.get_property("Class").map_err(|_| UnlockError::ServiceUnavailable)?;
let remote: bool = session.get_property("Remote").map_err(|_| UnlockError::ServiceUnavailable)?;
let active: bool = session.get_property("Active").map_err(|_| UnlockError::ServiceUnavailable)?;
let (seat, _): (String, OwnedObjectPath) = session.get_property("Seat").map_err(|_| UnlockError::ServiceUnavailable)?;
if !eligible_session(&kind, &class, remote, active) || seat.is_empty() {
continue;
}
let locked_hint: bool = session.get_property("LockedHint").map_err(|_| UnlockError::ServiceUnavailable)?;
let started_at_micros: u64 = session.get_property("Timestamp").map_err(|_| UnlockError::ServiceUnavailable)?;
if started_at_micros == 0 {
return Err(UnlockError::ServiceUnavailable);
}
eligible.push(GnomeSession {
id,
started_at_micros,
user,
uid,
seat,
lock_state: observed_lock_state(locked_hint),
});
}
Ok(eligible)
}
pub fn unlock_user_session(session: &GnomeSession) -> Result<UnlockOutcome, UnlockError> {
let before = selected_session(session)?;
match before.lock_state {
LockState::Usable => return Ok(UnlockOutcome::AlreadyUsable),
LockState::Unknown => return Err(UnlockError::OutcomeUnverified),
LockState::Locked => {}
}
let system = system_connection()?;
let manager = Proxy::new(&system, LOGIN_DEST, LOGIN_PATH, LOGIN_MANAGER).map_err(|_| UnlockError::ServiceUnavailable)?;
let path: OwnedObjectPath = manager.call("GetSession", &(session.id.as_str(),)).map_err(|_| UnlockError::StaleSession)?;
let selected = Proxy::new(&system, LOGIN_DEST, path.as_str(), LOGIN_SESSION).map_err(|_| UnlockError::ServiceUnavailable)?;
require_exact_active_session(&selected, session)?;
let locked_hint: bool = selected.get_property("LockedHint").map_err(|_| UnlockError::ServiceUnavailable)?;
if !locked_hint {
return Ok(UnlockOutcome::AlreadyUsable);
}
selected.call::<_, _, ()>("Unlock", &()).map_err(|_| UnlockError::ServiceUnavailable)?;
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match selected_session(session) {
Ok(current) if current.lock_state == LockState::Usable => return Ok(UnlockOutcome::UnlockedExistingSession),
Ok(_) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(100)),
Ok(_) => return Err(UnlockError::OutcomeUnverified),
Err(UnlockError::StaleSession) => return Err(UnlockError::StaleSession),
Err(_) => return Err(UnlockError::OutcomeUnverified),
}
}
}
pub fn lock_existing_session(session: &GnomeSession) -> Result<(), UnlockError> {
let before = selected_session(session)?;
if before.lock_state != LockState::Usable {
return Err(UnlockError::StaleSession);
}
let system = system_connection()?;
let manager = Proxy::new(&system, LOGIN_DEST, LOGIN_PATH, LOGIN_MANAGER).map_err(|_| UnlockError::ServiceUnavailable)?;
let path: OwnedObjectPath = manager.call("GetSession", &(session.id.as_str(),)).map_err(|_| UnlockError::StaleSession)?;
let selected = Proxy::new(&system, LOGIN_DEST, path.as_str(), LOGIN_SESSION).map_err(|_| UnlockError::ServiceUnavailable)?;
require_exact_active_session(&selected, session)?;
let locked_hint: bool = selected.get_property("LockedHint").map_err(|_| UnlockError::ServiceUnavailable)?;
if locked_hint {
return Err(UnlockError::StaleSession);
}
selected.call::<_, _, ()>("Lock", &()).map_err(|_| UnlockError::ServiceUnavailable)?;
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match selected_session(session) {
Ok(current) if current.lock_state == LockState::Locked => return Ok(()),
Ok(_) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(100)),
Ok(_) => return Err(UnlockError::OutcomeUnverified),
Err(UnlockError::StaleSession) => return Err(UnlockError::StaleSession),
Err(_) => return Err(UnlockError::OutcomeUnverified),
}
}
}
fn require_exact_active_session(selected: &Proxy<'_>, session: &GnomeSession) -> Result<(), UnlockError> {
let current_id: String = selected.get_property("Id").map_err(|_| UnlockError::StaleSession)?;
let started_at_micros: u64 = selected.get_property("Timestamp").map_err(|_| UnlockError::StaleSession)?;
let (uid, _): (u32, OwnedObjectPath) = selected.get_property("User").map_err(|_| UnlockError::StaleSession)?;
let user: String = selected.get_property("Name").map_err(|_| UnlockError::StaleSession)?;
let (seat, _): (String, OwnedObjectPath) = selected.get_property("Seat").map_err(|_| UnlockError::StaleSession)?;
let kind: String = selected.get_property("Type").map_err(|_| UnlockError::StaleSession)?;
let class: String = selected.get_property("Class").map_err(|_| UnlockError::StaleSession)?;
let remote: bool = selected.get_property("Remote").map_err(|_| UnlockError::StaleSession)?;
let active: bool = selected.get_property("Active").map_err(|_| UnlockError::StaleSession)?;
if current_id != session.id
|| started_at_micros != session.started_at_micros
|| uid != session.uid
|| user != session.user
|| seat != session.seat
|| !eligible_session(&kind, &class, remote, active)
{
return Err(UnlockError::StaleSession);
}
Ok(())
}
fn selected_session(selected: &GnomeSession) -> Result<GnomeSession, UnlockError> {
let current = list_user_sessions()?.into_iter().find(|current| current.id == selected.id).ok_or(UnlockError::StaleSession)?;
if !selected.same_login(¤t) {
return Err(UnlockError::StaleSession);
}
Ok(current)
}
fn observed_lock_state(locked_hint: bool) -> LockState {
if locked_hint {
LockState::Locked
} else {
LockState::Usable
}
}
fn eligible_session(kind: &str, class: &str, remote: bool, active: bool) -> bool {
kind == "wayland" && class == "user" && !remote && active
}
fn system_connection() -> Result<Connection, UnlockError> {
zbus::blocking::connection::Builder::system()
.map_err(|_| UnlockError::ServiceUnavailable)?
.method_timeout(Duration::from_secs(3))
.build()
.map_err(|_| UnlockError::ServiceUnavailable)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reports_the_logind_hint_as_session_state() {
assert_eq!(observed_lock_state(false), LockState::Usable);
assert_eq!(observed_lock_state(true), LockState::Locked);
}
#[test]
fn reused_logind_id_does_not_identify_the_old_session() {
let old = GnomeSession {
id: "52".into(),
started_at_micros: 1_000,
user: "neko".into(),
uid: 1000,
seat: "seat0".into(),
lock_state: LockState::Locked,
};
let renewed = GnomeSession {
started_at_micros: 2_000,
..old.clone()
};
assert_eq!(old.selector(), "linux-logind:52:1000:1000");
assert!(!old.same_login(&renewed));
}
#[test]
fn native_delivery_requires_still_active_local_gnome_session() {
assert!(eligible_session("wayland", "user", false, true));
assert!(!eligible_session("wayland", "user", false, false));
assert!(!eligible_session("wayland", "user", true, true));
assert!(!eligible_session("x11", "user", false, true));
assert!(!eligible_session("wayland", "greeter", false, true));
}
}