From 2c646af2369d3a0ef031d21e2cb1b7d710effe43 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Thu, 12 Feb 2026 11:05:05 +0900 Subject: [PATCH] fingerd: use structs from proto, fix clippy lints --- src/proto/finger_protocol.rs | 11 +- src/server/fingerd.rs | 386 ++++------------------------------- src/server/varlink_api.rs | 6 +- 3 files changed, 53 insertions(+), 350 deletions(-) diff --git a/src/proto/finger_protocol.rs b/src/proto/finger_protocol.rs index 39776b7..7fcb13d 100644 --- a/src/proto/finger_protocol.rs +++ b/src/proto/finger_protocol.rs @@ -202,7 +202,7 @@ fn parse_bsd_finger_time(time: &str) -> anyhow::Result> { // if not, try to get the local timezone offset. // if not, assume UTC. - return Ok(DateTime::::from_utc(dt, Utc)); + return Ok(DateTime::::from_naive_utc_and_offset(dt, Utc)); } } @@ -259,6 +259,7 @@ pub struct FingerResponseUserEntry { } impl FingerResponseUserEntry { + #[allow(clippy::too_many_arguments)] pub fn new( username: String, full_name: String, @@ -400,8 +401,10 @@ impl FingerResponseUserEntry { .filter_map(|line| { match FingerResponseUserSession::try_from_finger_response_line(line) { Ok(session) => Some(session), - // TODO: log warning if parsing fails - Err(_) => None, + Err(_) => { + tracing::warn!("Failed to parse user session from line: {}", line); + None + } } }) .collect(); @@ -509,8 +512,6 @@ impl FingerResponseUserSession { } } - /// Parse the login time from the text string generated by bsd-finger - /// Parse the idle time from the text string generated by bsd-finger fn parse_idle_time(str: &str) -> anyhow::Result { // Parse idle time from finger response format. diff --git a/src/server/fingerd.rs b/src/server/fingerd.rs index 931d3ed..ec43025 100644 --- a/src/server/fingerd.rs +++ b/src/server/fingerd.rs @@ -1,321 +1,15 @@ -use std::{ - net::{SocketAddr, ToSocketAddrs, hostname}, - path::{Path, PathBuf}, -}; +use std::{net::hostname, path::Path}; -use chrono::{ - DateTime, Datelike, Duration, NaiveDate, NaiveTime, TimeDelta, Timelike, Utc, Weekday, -}; +use chrono::{DateTime, Duration, Timelike, Utc}; use nix::sys::stat::stat; -use serde::{Deserialize, Serialize}; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::TcpStream, -}; use uucore::utmpx::Utmpx; -use crate::proto::finger_protocol::{FingerRequest, RawFingerResponse}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct FingerUserEntry { - pub username: String, - pub full_name: String, - pub home_dir: PathBuf, - pub shell: PathBuf, - pub sessions: Vec, - pub pgp_key: Option, - pub forward_status: Option, - pub mail_status: Option, - pub plan: Option, -} - -impl FingerUserEntry { - pub fn new( - username: String, - full_name: String, - home_dir: PathBuf, - shell: PathBuf, - sessions: Vec, - pgp_key: Option, - forward_status: Option, - mail_status: Option, - plan: Option, - ) -> Self { - Self { - username, - full_name, - home_dir, - shell, - sessions, - pgp_key, - forward_status, - mail_status, - plan, - } - } - - /// Try parsing a FingerUserEntry from the text format used by the classic finger implementations. - pub fn try_from_finger_response( - response: &RawFingerResponse, - username: String, - ) -> anyhow::Result { - let content = response.get_inner(); - let lines: Vec<&str> = content.lines().collect(); - - if lines.len() < 2 { - return Err(anyhow::anyhow!( - "Unexpected finger response format for user {}", - username - )); - } - - let first_line = lines[0]; - let second_line = lines[1]; - - let full_name = first_line - .split("Name:") - .nth(1) - .ok_or_else(|| { - anyhow::anyhow!( - "Failed to parse full name from finger response for user {}", - username - ) - })? - .trim() - .to_string(); - - let home_dir = second_line - .split("Directory:") - .nth(1) - .and_then(|s| s.split("Shell:").next()) - .map(|s| s.trim()) - .map(PathBuf::from) - .ok_or_else(|| { - anyhow::anyhow!( - "Failed to parse home directory from finger response for user {}", - username - ) - })?; - - let shell = second_line - .split("Shell:") - .nth(1) - .map(|s| s.trim()) - .map(PathBuf::from) - .ok_or_else(|| { - anyhow::anyhow!( - "Failed to parse shell from finger response for user {}", - username - ) - })?; - - let sessions = lines - .iter() - .skip(2) - .take_while(|line| line.starts_with("On since")) - .filter_map(|line| { - match FingerUserSession::try_from_finger_response_line(line) { - Ok(session) => Some(session), - // TODO: log warning if parsing fails - Err(_) => None, - } - }) - .collect(); - - // TODO: parse forward_status, mail_status, plan from remaining lines - - Ok(Self::new( - username, full_name, home_dir, shell, sessions, None, None, None, None, - )) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct FingerUserSession { - pub tty: String, - pub login_time: DateTime, - pub idle_time: TimeDelta, - pub host: String, -} - -impl FingerUserSession { - pub fn new(tty: String, login_time: DateTime, idle_time: TimeDelta, host: String) -> Self { - Self { - tty, - login_time, - idle_time, - host, - } - } - - fn parse_login_time(time: &str, _timezone: &str) -> anyhow::Result> { - let now = Utc::now(); - let mut parts = time.split_whitespace(); - - let weekday = match parts.next() { - Some("Mon") => Weekday::Mon, - Some("Tue") => Weekday::Tue, - Some("Wed") => Weekday::Wed, - Some("Thu") => Weekday::Thu, - Some("Fri") => Weekday::Fri, - Some("Sat") => Weekday::Sat, - Some("Sun") => Weekday::Sun, - _ => anyhow::bail!("Invalid weekday in login time: {}", time), - }; - - let month = match parts.next() { - Some("Jan") => 1, - Some("Feb") => 2, - Some("Mar") => 3, - Some("Apr") => 4, - Some("May") => 5, - Some("Jun") => 6, - Some("Jul") => 7, - Some("Aug") => 8, - Some("Sep") => 9, - Some("Oct") => 10, - Some("Nov") => 11, - Some("Dec") => 12, - _ => anyhow::bail!("Invalid month in login time: {}", time), - }; - - let day: u32 = parts - .next() - .and_then(|d| d.parse().ok()) - .ok_or_else(|| anyhow::anyhow!("Invalid day in login time: {}", time))?; - - let time_part = parts - .next() - .ok_or_else(|| anyhow::anyhow!("Missing time in login time: {}", time))?; - - let clock = NaiveTime::parse_from_str(time_part, "%H:%M").map_err(|e| { - anyhow::anyhow!( - "Failed to parse time component in login time: {}: {}", - time, - e - ) - })?; - - const MAX_YEARS_BACK: i32 = 10; - - for offset in 0..=MAX_YEARS_BACK { - let year = now.year() - offset; - - let date = match NaiveDate::from_ymd_opt(year, month, day) { - Some(d) => d, - None => continue, - }; - - if date.weekday() != weekday { - continue; - } - - let dt = date.and_time(clock); - - if dt <= now.naive_utc() { - // TODO: apply timezone if we are able to parse it. - // if not, try to get the local timezone offset. - // if not, assume UTC. - - return Ok(DateTime::::from_utc(dt, Utc)); - } - } - - Err(anyhow::anyhow!( - "Could not infer year for login time {} within {} years", - time, - MAX_YEARS_BACK - )) - } - - fn parse_idle_time(str: &str) -> anyhow::Result { - // Parse idle time from finger response format. - // This has four cases: " ", "MMMMM", "HH:MM", "DDd" - if str.trim().is_empty() { - Ok(Duration::zero()) - } else if str.contains(':') { - let parts: Vec<&str> = str.split(':').collect(); - if parts.len() != 2 { - return Err(anyhow::anyhow!("Invalid idle time format: {}", str)); - } - let hours: i64 = parts[0].parse().map_err(|e| { - anyhow::anyhow!("Failed to parse hours from idle time {}: {}", str, e) - })?; - let minutes: i64 = parts[1].parse().map_err(|e| { - anyhow::anyhow!("Failed to parse minutes from idle time {}: {}", str, e) - })?; - Ok(Duration::hours(hours) + Duration::minutes(minutes)) - } else if str.ends_with('d') { - let days_str = str.trim_end_matches('d'); - let days: i64 = days_str.parse().map_err(|e| { - anyhow::anyhow!("Failed to parse days from idle time {}: {}", str, e) - })?; - Ok(Duration::days(days)) - } else { - let minutes: i64 = str.parse().map_err(|e| { - anyhow::anyhow!("Failed to parse minutes from idle time {}: {}", str, e) - })?; - Ok(Duration::minutes(minutes)) - } - } - - /// Try parsing a user session from the text format used by the classic finger implementations. - pub fn try_from_finger_response_line(line: &str) -> anyhow::Result { - let parts: Vec<&str> = line.split_whitespace().collect(); - - debug_assert!(parts[0] == "On"); - debug_assert!(parts[1] == "since"); - - let login_time_parts = parts - .iter() - .take_while(|&&s| s != "on") - .skip(2) - .cloned() - .collect::>(); - - let login_time = Self::parse_login_time( - &login_time_parts[..login_time_parts.len() - 1].join(" "), - login_time_parts[login_time_parts.len() - 1], - )?; - - let tty = parts - .iter() - .skip_while(|&&s| s != "on") - .nth(1) - .ok_or_else(|| anyhow::anyhow!("Failed to find tty in finger session line: {line}"))? - .trim_end_matches(',') - .to_string(); - - let idle_time_str = parts - .iter() - .skip_while(|&&s| s != "idle") - .nth(1) - .ok_or_else(|| { - anyhow::anyhow!("Failed to find idle time in finger session line: {line}") - })? - .trim_end_matches(','); - let idle_time = Self::parse_idle_time(idle_time_str)?; - - let host = parts - .iter() - .skip_while(|&&s| s != "from") - .nth(1) - .unwrap_or(&"") - .to_string(); - - Ok(Self { - tty, - login_time, - idle_time, - host, - }) - } -} +use crate::proto::finger_protocol::{FingerResponseUserEntry, FingerResponseUserSession}; /// Retrieve local user information for the given username. /// /// Returns None if the user does not exist. -pub fn get_local_user(username: &str) -> anyhow::Result> { +pub fn get_local_user(username: &str) -> anyhow::Result> { let username = username.to_string(); let user_entry = match nix::unistd::User::from_name(&username) { Ok(Some(user)) => user, @@ -336,7 +30,7 @@ pub fn get_local_user(username: &str) -> anyhow::Result> let hostname = hostname()?.to_str().unwrap_or("localhost").to_string(); let now = Utc::now().with_nanosecond(0).unwrap_or(Utc::now()); - let sessions: Vec = Utmpx::iter_all_records() + let sessions: Vec = Utmpx::iter_all_records() .filter(|entry| entry.user() == username) .filter(|entry| entry.is_user_process()) .filter_map(|entry| { @@ -345,24 +39,31 @@ pub fn get_local_user(username: &str) -> anyhow::Result> .checked_to_utc() .and_then(|t| DateTime::::from_timestamp_secs(t.unix_timestamp()))?; - let idle_time = stat(&Path::new("/dev").join(entry.tty_device())) - .ok() + let tty_device_stat = stat(&Path::new("/dev").join(entry.tty_device())).ok(); + + let idle_time = tty_device_stat .and_then(|st| { let last_active = DateTime::::from_timestamp_secs(st.st_atime)?; Some((now - last_active).max(Duration::zero())) }) .unwrap_or(Duration::zero()); + // Check if the write permission for "others" is set + let messages_on = tty_device_stat + .map(|st| st.st_mode & 0o002 != 0) + .unwrap_or(false); + debug_assert!( idle_time.num_seconds() >= 0, "Idle time should never be negative" ); - Some(FingerUserSession::new( + Some(FingerResponseUserSession::new( entry.tty_device(), login_time, idle_time, hostname.clone(), + messages_on, )) }) .collect(); @@ -382,44 +83,45 @@ pub fn get_local_user(username: &str) -> anyhow::Result> None }; - Ok(Some(FingerUserEntry::new( - username, full_name, home_dir, shell, sessions, None, forward, plan, None, + Ok(Some(FingerResponseUserEntry::new( + username, full_name, home_dir, shell, None, None, None, false, sessions, forward, None, + None, None, plan, ))) } -/// Retrieve remote user information for the given username on the specified host. -/// -/// Returns None if the user does not exist or no information is available. -async fn get_remote_user(username: &str, host: &str) -> anyhow::Result> { - let addr = format!("{}:79", host); - let socket_addrs: Vec = addr.to_socket_addrs()?.collect(); +// /// Retrieve remote user information for the given username on the specified host. +// /// +// /// Returns None if the user does not exist or no information is available. +// async fn get_remote_user(username: &str, host: &str) -> anyhow::Result> { +// let addr = format!("{}:79", host); +// let socket_addrs: Vec = addr.to_socket_addrs()?.collect(); - if socket_addrs.is_empty() { - return Err(anyhow::anyhow!( - "Could not resolve address for host {}", - host - )); - } +// if socket_addrs.is_empty() { +// return Err(anyhow::anyhow!( +// "Could not resolve address for host {}", +// host +// )); +// } - let socket_addr = socket_addrs[0]; +// let socket_addr = socket_addrs[0]; - let mut stream = TcpStream::connect(socket_addr).await?; +// let mut stream = TcpStream::connect(socket_addr).await?; - let request = FingerRequest::new(false, username.to_string()); - let request_bytes = request.to_bytes(); - stream.write_all(&request_bytes).await?; +// let request = FingerRequest::new(false, username.to_string()); +// let request_bytes = request.to_bytes(); +// stream.write_all(&request_bytes).await?; - let mut response_bytes = Vec::new(); - stream.read_to_end(&mut response_bytes).await?; +// let mut response_bytes = Vec::new(); +// stream.read_to_end(&mut response_bytes).await?; - let response = RawFingerResponse::from_bytes(&response_bytes); +// let response = RawFingerResponse::from_bytes(&response_bytes); - if response.is_empty() { - Ok(None) - } else { - Ok(Some(response)) - } -} +// if response.is_empty() { +// Ok(None) +// } else { +// Ok(Some(response)) +// } +// } #[cfg(test)] mod tests { diff --git a/src/server/varlink_api.rs b/src/server/varlink_api.rs index b738422..08c5b6b 100644 --- a/src/server/varlink_api.rs +++ b/src/server/varlink_api.rs @@ -3,9 +3,9 @@ use serde::{Deserialize, Serialize}; use zlink::{ReplyError, service::MethodReply}; use crate::{ - proto::{WhodStatusUpdate, WhodUserEntry}, + proto::{WhodStatusUpdate, WhodUserEntry, finger_protocol::FingerResponseUserEntry}, server::{ - fingerd::{self, FingerUserEntry}, + fingerd::{self}, rwhod::RwhodStatusStore, }, }; @@ -76,7 +76,7 @@ pub enum VarlinkFingerClientResponse { Finger(VarlinkFingerResponse), } -pub type VarlinkFingerResponse = Vec>; +pub type VarlinkFingerResponse = Vec>; #[derive(Debug, Clone, PartialEq, ReplyError)] #[zlink(interface = "no.ntnu.pvv.roowho2.finger")]