roowho2_lib/server/
fingerd.rs1use std::{
2 net::hostname,
3 os::unix::fs::{MetadataExt, PermissionsExt},
4 path::Path,
5};
6
7use chrono::{DateTime, Duration, Timelike, Utc};
8use nix::sys::stat::stat;
9use uucore::utmpx::Utmpx;
10
11use crate::proto::finger_protocol::{FingerResponseUserEntry, FingerResponseUserSession};
12
13fn read_file_content_if_exists(path: &Path) -> anyhow::Result<Option<String>> {
14 let file_is_readable = path.exists()
15 && path.is_file()
16 && (((path.metadata()?.permissions().mode() & 0o400 != 0
17 && nix::unistd::geteuid().as_raw() == path.metadata()?.uid())
18 || (path.metadata()?.permissions().mode() & 0o040 != 0
19 && nix::unistd::getegid().as_raw() == path.metadata()?.gid())
20 || (path.metadata()?.permissions().mode() & 0o004 != 0))
21 || caps::has_cap(
22 None,
23 caps::CapSet::Effective,
24 caps::Capability::CAP_DAC_READ_SEARCH,
25 )?)
26 && path.metadata()?.len() > 0;
27
28 if file_is_readable {
29 Ok(Some(std::fs::read_to_string(path)?.trim().to_string()))
30 } else {
31 Ok(None)
32 }
33}
34
35pub fn get_local_user(username: &str) -> anyhow::Result<Option<FingerResponseUserEntry>> {
39 let username = username.to_string();
40 let user_entry = match nix::unistd::User::from_name(&username) {
41 Ok(Some(user)) => user,
42 Ok(None) => return Ok(None),
43 Err(err) => {
44 return Err(anyhow::anyhow!(
45 "Failed to get user entry for {}: {}",
46 username,
47 err
48 ));
49 }
50 };
51
52 let nofinger_path = user_entry.dir.join(".nofinger");
53 if nofinger_path.exists() {
54 return Ok(None);
55 }
56
57 let full_name = user_entry.name;
58 let home_dir = user_entry.dir.clone();
59 let shell = user_entry.shell;
60
61 let gecos_fields: Vec<&str> = full_name.split(',').collect();
62
63 let office = gecos_fields.get(1).map(|s| s.to_string());
64 let office_phone = gecos_fields.get(2).map(|s| s.to_string());
65 let home_phone = gecos_fields.get(3).map(|s| s.to_string());
66
67 let hostname = hostname()?.to_str().unwrap_or("localhost").to_string();
68
69 let mut utmpx_records = Utmpx::iter_all_records()
70 .filter(|entry| entry.user() == username)
71 .filter(|entry| entry.is_user_process())
72 .peekable();
73
74 let user_never_logged_in = utmpx_records.peek().is_none();
76
77 let now = Utc::now().with_nanosecond(0).unwrap_or(Utc::now());
78 let sessions: Vec<FingerResponseUserSession> = utmpx_records
79 .filter_map(|entry| {
80 let login_time = entry
81 .login_time()
82 .checked_to_utc()
83 .and_then(|t| DateTime::<Utc>::from_timestamp_secs(t.unix_timestamp()))?;
84
85 let tty_device_stat = stat(&Path::new("/dev").join(entry.tty_device())).ok();
86
87 let idle_time = tty_device_stat
88 .and_then(|st| {
89 let last_active = DateTime::<Utc>::from_timestamp_secs(st.st_atime)?;
90 Some((now - last_active).max(Duration::zero()))
91 })
92 .unwrap_or(Duration::zero());
93
94 let messages_on = tty_device_stat
96 .map(|st| st.st_mode & 0o002 != 0)
97 .unwrap_or(false);
98
99 debug_assert!(
100 idle_time.num_seconds() >= 0,
101 "Idle time should never be negative"
102 );
103
104 Some(FingerResponseUserSession::new(
105 entry.tty_device(),
106 login_time,
107 idle_time,
108 hostname.clone(),
109 messages_on,
110 ))
111 })
112 .collect();
113
114 let forward_path = user_entry.dir.join(".forward");
115 let forward = read_file_content_if_exists(&forward_path)?;
116
117 let pgpkey_path = user_entry.dir.join(".pgpkey");
118 let pgpkey = read_file_content_if_exists(&pgpkey_path)?;
119
120 let project_path = user_entry.dir.join(".project");
121 let project = read_file_content_if_exists(&project_path)?;
122
123 let plan_path = user_entry.dir.join(".plan");
124 let plan = read_file_content_if_exists(&plan_path)?;
125
126 Ok(Some(FingerResponseUserEntry::new(
127 username,
128 full_name,
129 home_dir,
130 shell,
131 office,
132 office_phone,
133 home_phone,
134 user_never_logged_in,
135 sessions,
136 forward,
137 None,
138 pgpkey,
139 project,
140 plan,
141 )))
142}
143
144#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_finger_root() {
184 let user_entry = get_local_user("root").unwrap().unwrap();
185 assert_eq!(user_entry.username, "root");
186 }
187
188 }