From 53195a6dea782f0c99e6458b4a6df75237d0b912 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Tue, 28 Jul 2026 19:13:57 +0900 Subject: [PATCH] server/varlink_api: split into multiple files --- src/server/varlink_api.rs | 361 ++---------------------------- src/server/varlink_api/fingerd.rs | 117 ++++++++++ src/server/varlink_api/rwhod.rs | 107 +++++++++ src/server/varlink_api/walld.rs | 124 ++++++++++ 4 files changed, 361 insertions(+), 348 deletions(-) create mode 100644 src/server/varlink_api/fingerd.rs create mode 100644 src/server/varlink_api/rwhod.rs create mode 100644 src/server/varlink_api/walld.rs diff --git a/src/server/varlink_api.rs b/src/server/varlink_api.rs index c2d9ab7..7dd96f5 100644 --- a/src/server/varlink_api.rs +++ b/src/server/varlink_api.rs @@ -1,255 +1,24 @@ -use std::{collections::HashMap, os::fd::OwnedFd, time::Duration}; +mod fingerd; +mod rwhod; +mod walld; + +use std::{os::fd::OwnedFd, time::Duration}; use anyhow::Context; use futures_util::stream; -use itertools::Itertools; use serde::{Deserialize, Serialize}; use tokio::time::timeout; use zlink::{ - ReplyError, service::MethodReply, tokio::unix::{Listener as UnixListener, Stream as UnixStream}, }; -use crate::{ - proto::{WhodStatusUpdate, WhodUserEntry, finger_protocol::FingerResponseUserEntry}, - server::{ - fingerd::{self, FingerRequestInfo, FingerRequestNetworking, finger_utmp_users}, - ignore_list::IgnoreList, - rwhod::RwhodStatusStore, - }, -}; +use crate::server::{ignore_list::IgnoreList, rwhod::RwhodStatusStore}; + +pub use crate::server::varlink_api::{fingerd::*, rwhod::*, walld::*}; pub const DEFAULT_CLIENT_SERVER_SOCKET_PATH: &str = "/run/roowho2/roowho2.varlink"; -// Types for 'no.ntnu.pvv.roowho2.rwhod' - -#[zlink::proxy("no.ntnu.pvv.roowho2.rwhod")] -pub trait VarlinkRwhodClientProxy { - /// `max_idle_seconds` is the maximum idle time (in seconds) for a user - /// to be included in the response. `None` means no limit (i.e. the old - /// `all: true`). The client is expected to always send an explicit - /// value, defaulting to whatever it considers reasonable if the user - /// didn't ask for a specific limit. - async fn rwho( - &mut self, - max_idle_seconds: Option, - ) -> zlink::Result>; - - /// See [`VarlinkRwhodClientProxy::rwho`] for the meaning of `max_idle_seconds`. - async fn ruptime( - &mut self, - max_idle_seconds: Option, - ) -> zlink::Result>; -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "method", content = "parameters")] -pub enum VarlinkRwhodClientRequest { - #[serde(rename = "no.ntnu.pvv.roowho2.rwhod.Rwho")] - Rwho { - /// Maximum idle time (in seconds) for a user to be included. - /// `None` means no limit (i.e. return all users). - max_idle_seconds: Option, - }, - - #[serde(rename = "no.ntnu.pvv.roowho2.rwhod.Ruptime")] - Ruptime { - /// Maximum idle time (in seconds) for a user to be counted. - /// `None` means no limit (i.e. return all users). - max_idle_seconds: Option, - }, -} - -#[derive(Debug, Clone, PartialEq, Serialize)] -#[serde(untagged)] -pub enum VarlinkRwhodClientResponse { - Rwho(VarlinkRwhoResponse), - Ruptime(VarlinkRuptimeResponse), -} - -pub type VarlinkRwhoResponse = HashMap>; -pub type VarlinkRuptimeResponse = Vec; - -#[derive(Debug, Clone, PartialEq, ReplyError)] -#[zlink(interface = "no.ntnu.pvv.roowho2.rwhod")] -pub enum VarlinkRwhodClientError { - InvalidRequest, - TimedOut, - Disabled, -} - -// Types for 'no.ntnu.pvv.roowho2.finger' - -#[zlink::proxy("no.ntnu.pvv.roowho2.finger")] -pub trait VarlinkFingerClientProxy { - async fn finger( - &mut self, - user_queries: Option>, - match_fullnames: bool, - request_info: FingerRequestInfo, - request_networking: FingerRequestNetworking, - disable_user_account_db: bool, - raw_remote_output: bool, - ) -> zlink::Result>; -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "method", content = "parameters")] -pub enum VarlinkFingerClientRequest { - #[serde(rename = "no.ntnu.pvv.roowho2.finger.Finger")] - Finger { - user_queries: Option>, - match_fullnames: bool, - request_info: FingerRequestInfo, - request_networking: FingerRequestNetworking, - disable_user_account_db: bool, - raw_remote_output: bool, - }, -} - -#[derive(Debug, Serialize)] -#[serde(untagged)] -pub enum VarlinkFingerClientResponse { - Finger(VarlinkFingerResponse), -} - -pub type VarlinkFingerResponse = Vec; - -#[derive(Debug, Clone, PartialEq, ReplyError)] -#[zlink(interface = "no.ntnu.pvv.roowho2.finger")] -pub enum VarlinkFingerClientError { - InvalidRequest, - TimedOut, - Disabled, -} - -// Types for 'no.ntnu.pvv.roowho2.walld' - -#[derive(Debug, Deserialize)] -#[serde(tag = "method", content = "parameters")] -pub enum VarlinkWalldClientRequest { - #[serde(rename = "no.ntnu.pvv.roowho2.walld.Wall")] - Wall { - message: String, - group: Option, - nobanner: bool, - timeout_secs: u32, - }, - - #[serde(rename = "no.ntnu.pvv.roowho2.walld.Write")] - Write { - user: String, - tty: Option, - message: String, - }, -} - -#[zlink::proxy("no.ntnu.pvv.roowho2.walld")] -pub trait VarlinkWalldClientProxy { - /// Broadcast `message` to the tty of every logged in user, optionally restricted to `group`. - async fn wall( - &mut self, - message: String, - group: Option, - nobanner: bool, - timeout_secs: u32, - ) -> zlink::Result>; - - /// Send `message` to a the tty of a single `user`, optionally targeting a specific `tty`. - async fn write( - &mut self, - user: String, - tty: Option, - message: String, - ) -> zlink::Result>; -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum VarlinkWalldClientResponse { - Wall(VarlinkWallResponse), - Write(VarlinkWriteResponse), -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub struct VarlinkWallResponse { - pub delivered: Vec, - pub failures: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct VarlinkWallDelivery { - pub user: String, - pub tty: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct VarlinkWallDeliveryFailure { - pub user: String, - pub tty: String, - pub reason: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub struct VarlinkWriteResponse { - /// The tty the message was actually delivered to. - /// This is relevant when the caller didn't specify one, and it was picked automatically. - pub tty: String, -} - -#[derive(Debug, Clone, PartialEq, ReplyError)] -#[zlink(interface = "no.ntnu.pvv.roowho2.walld")] -pub enum VarlinkWalldClientError { - /// The walld service is not enabled on the server. - Disabled, - /// Caller was not authorized by polkit to perform the request. - NotAuthorized, - /// The target user is not logged in anywhere. - UserNotLoggedIn { user: String }, - /// The target user has disabled incoming messages on the relevant tty/ttys. - MessagesDisabled { user: String }, - /// The requested tty does not belong to the target user, or does not exist. - TtyNotFound { user: String, tty: String }, - /// The request parameters were invalid (e.g. unknown group). - InvalidRequest { message: String }, - /// The request timed out. - TimedOut, - /// Something went wrong while talking to the tty (open/write failure). - Io { message: String }, -} - -// TODO: use `thiserror` to derive `Display` the impl instead. - -impl std::fmt::Display for VarlinkWalldClientError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VarlinkWalldClientError::Disabled => { - write!(f, "The walld service is disabled on the server") - } - VarlinkWalldClientError::NotAuthorized => write!(f, "Caller is not authorized"), - VarlinkWalldClientError::UserNotLoggedIn { user } => { - write!(f, "User '{}' is not logged in", user) - } - VarlinkWalldClientError::MessagesDisabled { user } => { - write!(f, "User '{}' has disabled incoming messages", user) - } - VarlinkWalldClientError::TtyNotFound { user, tty } => { - write!(f, "Tty '{}' not found for user '{}'", tty, user) - } - VarlinkWalldClientError::InvalidRequest { message } => { - write!(f, "Invalid request: {}", message) - } - VarlinkWalldClientError::TimedOut => write!(f, "Request timed out"), - VarlinkWalldClientError::Io { message } => { - write!(f, "I/O error while sending message: {}", message) - } - } - } -} - -// -------------------- - #[derive(Debug, Deserialize)] #[serde(untagged)] #[allow(unused)] @@ -299,112 +68,7 @@ impl VarlinkRoowhoo2ClientServer { } } -impl VarlinkRoowhoo2ClientServer { - async fn handle_rwho_request( - &self, - max_idle_time: Option, - ) -> VarlinkRwhoResponse { - tracing::debug!(?max_idle_time, "Handling Rwho request"); - let store = self.whod_status_store.read().await; - - store - .values() - .filter_map(|status_update| { - let users: Vec = status_update - .users - .iter() - .filter(|user| max_idle_time.is_none_or(|max| user.idle_time < max)) - .cloned() - .collect(); - - (!users.is_empty()).then(|| (status_update.hostname.clone(), users)) - }) - .collect() - } - - async fn handle_ruptime_request( - &self, - max_idle_time: Option, - ) -> VarlinkRuptimeResponse { - tracing::debug!(?max_idle_time, "Handling Ruptime request"); - let store = self.whod_status_store.read().await; - - store - .values() - .cloned() - .map(|mut status_update| { - if let Some(max_idle_time) = max_idle_time { - status_update - .users - .retain(|user| user.idle_time < max_idle_time); - } - status_update - }) - .collect() - } - - async fn handle_finger_request( - &self, - user_queries: Option>, - match_fullnames: bool, - request_info: FingerRequestInfo, - _request_networking: FingerRequestNetworking, - _disable_user_account_db: bool, - _raw_remote_output: bool, - ) -> VarlinkFingerResponse { - tracing::debug!( - user_queries = ?user_queries, - match_fullnames = match_fullnames, - request_info = ?request_info, - "Handling Finger request", - ); - match user_queries { - Some(usernames) => usernames - .into_iter() - .flat_map::, _>(|username| { - fingerd::search_for_user( - &username, - match_fullnames, - &request_info, - self.finger_ignore_list.as_ref(), - ) - .into_iter() - .map(|res| (username.clone(), res)) - .collect() - }) - .dedup_by(|a, b| match (&a.1, &b.1) { - (Ok(user_a), Ok(user_b)) => user_a.username == user_b.username, - _ => false, - }) - .filter_map(|(username, user)| match user { - Ok(user_info) => Some(user_info), - Err(err) => { - tracing::error!( - "Error retrieving local user information for '{}': {}", - username, - err - ); - None - } - }) - .map(Box::new) - .map(FingerResponseUserEntry::Structured) - .collect(), - None => finger_utmp_users(&request_info, self.finger_ignore_list.as_ref()) - .into_iter() - .filter_map(|res| match res { - Ok(user_info) => Some(user_info), - Err(err) => { - tracing::error!("Error retrieving local user information: {}", err); - None - } - }) - .map(Box::new) - .map(FingerResponseUserEntry::Structured) - .collect(), - } - } -} +impl VarlinkRoowhoo2ClientServer {} impl zlink::Service for VarlinkRoowhoo2ClientServer { type MethodCall<'de> = VarlinkMethod; @@ -459,7 +123,7 @@ impl zlink::Service for VarlinkRoowhoo2ClientServer { let result = match timeout( Duration::from_secs(2), - self.handle_rwho_request(max_idle_time), + handle_rwho_request(&self.whod_status_store, max_idle_time), ) .await { @@ -513,7 +177,7 @@ impl zlink::Service for VarlinkRoowhoo2ClientServer { let result = match timeout( Duration::from_secs(2), - self.handle_ruptime_request(max_idle_time), + handle_ruptime_request(&self.whod_status_store, max_idle_time), ) .await { @@ -555,7 +219,8 @@ impl zlink::Service for VarlinkRoowhoo2ClientServer { let result = match timeout( Duration::from_secs(2), - self.handle_finger_request( + handle_finger_request( + &self.finger_ignore_list, user_queries.clone(), *match_fullnames, request_info.clone(), diff --git a/src/server/varlink_api/fingerd.rs b/src/server/varlink_api/fingerd.rs new file mode 100644 index 0000000..b1b433a --- /dev/null +++ b/src/server/varlink_api/fingerd.rs @@ -0,0 +1,117 @@ +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use zlink::ReplyError; + +use crate::{ + proto::finger_protocol::FingerResponseUserEntry, + server::{ + fingerd::{self, FingerRequestInfo, FingerRequestNetworking, finger_utmp_users}, + ignore_list::IgnoreList, + }, +}; + +#[zlink::proxy("no.ntnu.pvv.roowho2.finger")] +pub trait VarlinkFingerClientProxy { + async fn finger( + &mut self, + user_queries: Option>, + match_fullnames: bool, + request_info: FingerRequestInfo, + request_networking: FingerRequestNetworking, + disable_user_account_db: bool, + raw_remote_output: bool, + ) -> zlink::Result>; +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "method", content = "parameters")] +pub enum VarlinkFingerClientRequest { + #[serde(rename = "no.ntnu.pvv.roowho2.finger.Finger")] + Finger { + user_queries: Option>, + match_fullnames: bool, + request_info: FingerRequestInfo, + request_networking: FingerRequestNetworking, + disable_user_account_db: bool, + raw_remote_output: bool, + }, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum VarlinkFingerClientResponse { + Finger(VarlinkFingerResponse), +} + +pub type VarlinkFingerResponse = Vec; + +#[derive(Debug, Clone, PartialEq, ReplyError)] +#[zlink(interface = "no.ntnu.pvv.roowho2.finger")] +pub enum VarlinkFingerClientError { + InvalidRequest, + TimedOut, + Disabled, +} + +pub async fn handle_finger_request( + finger_ignore_list: &Option, + user_queries: Option>, + match_fullnames: bool, + request_info: FingerRequestInfo, + _request_networking: FingerRequestNetworking, + _disable_user_account_db: bool, + _raw_remote_output: bool, +) -> VarlinkFingerResponse { + tracing::debug!( + user_queries = ?user_queries, + match_fullnames = match_fullnames, + request_info = ?request_info, + "Handling Finger request", + ); + match user_queries { + Some(usernames) => usernames + .into_iter() + .flat_map::, _>(|username| { + fingerd::search_for_user( + &username, + match_fullnames, + &request_info, + finger_ignore_list.as_ref(), + ) + .into_iter() + .map(|res| (username.clone(), res)) + .collect() + }) + .dedup_by(|a, b| match (&a.1, &b.1) { + (Ok(user_a), Ok(user_b)) => user_a.username == user_b.username, + _ => false, + }) + .filter_map(|(username, user)| match user { + Ok(user_info) => Some(user_info), + Err(err) => { + tracing::error!( + "Error retrieving local user information for '{}': {}", + username, + err + ); + None + } + }) + .map(Box::new) + .map(FingerResponseUserEntry::Structured) + .collect(), + + None => finger_utmp_users(&request_info, finger_ignore_list.as_ref()) + .into_iter() + .filter_map(|res| match res { + Ok(user_info) => Some(user_info), + Err(err) => { + tracing::error!("Error retrieving local user information: {}", err); + None + } + }) + .map(Box::new) + .map(FingerResponseUserEntry::Structured) + .collect(), + } +} diff --git a/src/server/varlink_api/rwhod.rs b/src/server/varlink_api/rwhod.rs new file mode 100644 index 0000000..be08691 --- /dev/null +++ b/src/server/varlink_api/rwhod.rs @@ -0,0 +1,107 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use zlink::ReplyError; + +use crate::{ + proto::{WhodStatusUpdate, WhodUserEntry}, + server::rwhod::RwhodStatusStore, +}; + +#[zlink::proxy("no.ntnu.pvv.roowho2.rwhod")] +pub trait VarlinkRwhodClientProxy { + /// `max_idle_seconds` is the maximum idle time (in seconds) for a user + /// to be included in the response. `None` means no limit (i.e. the old + /// `all: true`). The client is expected to always send an explicit + /// value, defaulting to whatever it considers reasonable if the user + /// didn't ask for a specific limit. + async fn rwho( + &mut self, + max_idle_seconds: Option, + ) -> zlink::Result>; + + /// See [`VarlinkRwhodClientProxy::rwho`] for the meaning of `max_idle_seconds`. + async fn ruptime( + &mut self, + max_idle_seconds: Option, + ) -> zlink::Result>; +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "method", content = "parameters")] +pub enum VarlinkRwhodClientRequest { + #[serde(rename = "no.ntnu.pvv.roowho2.rwhod.Rwho")] + Rwho { + /// Maximum idle time (in seconds) for a user to be included. + /// `None` means no limit (i.e. return all users). + max_idle_seconds: Option, + }, + + #[serde(rename = "no.ntnu.pvv.roowho2.rwhod.Ruptime")] + Ruptime { + /// Maximum idle time (in seconds) for a user to be counted. + /// `None` means no limit (i.e. return all users). + max_idle_seconds: Option, + }, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(untagged)] +pub enum VarlinkRwhodClientResponse { + Rwho(VarlinkRwhoResponse), + Ruptime(VarlinkRuptimeResponse), +} + +pub type VarlinkRwhoResponse = HashMap>; +pub type VarlinkRuptimeResponse = Vec; + +#[derive(Debug, Clone, PartialEq, ReplyError)] +#[zlink(interface = "no.ntnu.pvv.roowho2.rwhod")] +pub enum VarlinkRwhodClientError { + InvalidRequest, + TimedOut, + Disabled, +} + +pub async fn handle_rwho_request( + whod_status_store: &RwhodStatusStore, + max_idle_time: Option, +) -> VarlinkRwhoResponse { + tracing::debug!(?max_idle_time, "Handling Rwho request"); + let store = whod_status_store.read().await; + + store + .values() + .filter_map(|status_update| { + let users: Vec = status_update + .users + .iter() + .filter(|user| max_idle_time.is_none_or(|max| user.idle_time < max)) + .cloned() + .collect(); + + (!users.is_empty()).then(|| (status_update.hostname.clone(), users)) + }) + .collect() +} + +pub async fn handle_ruptime_request( + whod_status_store: &RwhodStatusStore, + max_idle_time: Option, +) -> VarlinkRuptimeResponse { + tracing::debug!(?max_idle_time, "Handling Ruptime request"); + let store = whod_status_store.read().await; + + store + .values() + .cloned() + .map(|mut status_update| { + if let Some(max_idle_time) = max_idle_time { + status_update + .users + .retain(|user| user.idle_time < max_idle_time); + } + status_update + }) + .collect() +} diff --git a/src/server/varlink_api/walld.rs b/src/server/varlink_api/walld.rs new file mode 100644 index 0000000..5d68ab1 --- /dev/null +++ b/src/server/varlink_api/walld.rs @@ -0,0 +1,124 @@ +use serde::{Deserialize, Serialize}; +use zlink::ReplyError; + +#[derive(Debug, Deserialize)] +#[serde(tag = "method", content = "parameters")] +pub enum VarlinkWalldClientRequest { + #[serde(rename = "no.ntnu.pvv.roowho2.walld.Wall")] + Wall { + message: String, + group: Option, + nobanner: bool, + timeout_secs: u32, + }, + + #[serde(rename = "no.ntnu.pvv.roowho2.walld.Write")] + Write { + user: String, + tty: Option, + message: String, + }, +} + +#[zlink::proxy("no.ntnu.pvv.roowho2.walld")] +pub trait VarlinkWalldClientProxy { + /// Broadcast `message` to the tty of every logged in user, optionally restricted to `group`. + async fn wall( + &mut self, + message: String, + group: Option, + nobanner: bool, + timeout_secs: u32, + ) -> zlink::Result>; + + /// Send `message` to a the tty of a single `user`, optionally targeting a specific `tty`. + async fn write( + &mut self, + user: String, + tty: Option, + message: String, + ) -> zlink::Result>; +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum VarlinkWalldClientResponse { + Wall(VarlinkWallResponse), + Write(VarlinkWriteResponse), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct VarlinkWallResponse { + pub delivered: Vec, + pub failures: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct VarlinkWallDelivery { + pub user: String, + pub tty: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct VarlinkWallDeliveryFailure { + pub user: String, + pub tty: String, + pub reason: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct VarlinkWriteResponse { + /// The tty the message was actually delivered to. + /// This is relevant when the caller didn't specify one, and it was picked automatically. + pub tty: String, +} + +#[derive(Debug, Clone, PartialEq, ReplyError)] +#[zlink(interface = "no.ntnu.pvv.roowho2.walld")] +pub enum VarlinkWalldClientError { + /// The walld service is not enabled on the server. + Disabled, + /// Caller was not authorized by polkit to perform the request. + NotAuthorized, + /// The target user is not logged in anywhere. + UserNotLoggedIn { user: String }, + /// The target user has disabled incoming messages on the relevant tty/ttys. + MessagesDisabled { user: String }, + /// The requested tty does not belong to the target user, or does not exist. + TtyNotFound { user: String, tty: String }, + /// The request parameters were invalid (e.g. unknown group). + InvalidRequest { message: String }, + /// The request timed out. + TimedOut, + /// Something went wrong while talking to the tty (open/write failure). + Io { message: String }, +} + +// TODO: use `thiserror` to derive `Display` the impl instead. + +impl std::fmt::Display for VarlinkWalldClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VarlinkWalldClientError::Disabled => { + write!(f, "The walld service is disabled on the server") + } + VarlinkWalldClientError::NotAuthorized => write!(f, "Caller is not authorized"), + VarlinkWalldClientError::UserNotLoggedIn { user } => { + write!(f, "User '{}' is not logged in", user) + } + VarlinkWalldClientError::MessagesDisabled { user } => { + write!(f, "User '{}' has disabled incoming messages", user) + } + VarlinkWalldClientError::TtyNotFound { user, tty } => { + write!(f, "Tty '{}' not found for user '{}'", tty, user) + } + VarlinkWalldClientError::InvalidRequest { message } => { + write!(f, "Invalid request: {}", message) + } + VarlinkWalldClientError::TimedOut => write!(f, "Request timed out"), + VarlinkWalldClientError::Io { message } => { + write!(f, "I/O error while sending message: {}", message) + } + } + } +}