roowho2_lib/server/fingerd/
local_user_info.rs1use std::{
2 net::hostname,
3 os::unix::fs::{MetadataExt, PermissionsExt},
4 path::Path,
5};
6
7use chrono::{DateTime, Duration, Timelike, Utc};
8use itertools::Itertools;
9use nix::sys::stat::stat;
10use users::all_users;
11use uucore::utmpx::{Utmpx, UtmpxRecord};
12
13use crate::{
14 proto::finger_protocol::{FingerResponseStructuredUserEntry, FingerResponseUserSession},
15 server::fingerd::{FingerRequestInfo, local_email},
16};
17
18pub fn search_for_user(
20 search_string: &str,
21 match_fullnames: bool,
22 _request_info: &FingerRequestInfo,
23) -> Vec<anyhow::Result<FingerResponseStructuredUserEntry>> {
24 (unsafe { all_users() })
25 .filter_map(|user| {
26 let user = match nix::unistd::User::from_uid(user.uid().into()) {
27 Ok(Some(user)) => user,
28 Ok(None) => {
29 tracing::warn!(
30 "User with UID {} exists but could not retrieve user entry",
31 user.uid()
32 );
33 return None;
34 }
35 Err(e) => {
36 return Some(Err(anyhow::anyhow!(
37 "Failed to get user entry for UID {}: {}",
38 user.uid(),
39 e
40 )));
41 }
42 };
43
44 let username = user.name;
45 let full_name = String::from_utf8_lossy(
46 user.gecos
47 .as_bytes()
48 .split(|&b| b == b',')
49 .next()
50 .unwrap_or(&[]),
51 )
52 .to_string();
53
54 let matches_username = username.contains(search_string);
55 let matches_fullname = match_fullnames && full_name.contains(search_string);
56 if matches_username || matches_fullname {
57 match get_local_user(&username, None) {
58 Ok(Some(user_entry)) => Some(Ok(user_entry)),
59 Ok(None) => None, Err(err) => Some(Err(err)),
61 }
62 } else {
63 None
64 }
65 })
66 .collect()
67}
68
69pub fn finger_utmp_users(
71 _request_info: &FingerRequestInfo,
72) -> Vec<anyhow::Result<FingerResponseStructuredUserEntry>> {
73 Utmpx::iter_all_records()
74 .filter(|entry| entry.is_user_process())
75 .into_group_map_by(|entry| entry.user())
76 .into_iter()
77 .map(|(username, records)| get_local_user(&username, Some(records)))
78 .filter_map(|result| match result {
79 Ok(Some(user_entry)) => Some(Ok(user_entry)),
80 Ok(None) => None, Err(err) => Some(Err(err)),
82 })
83 .collect()
84}
85
86fn read_file_content_if_exists(path: &Path) -> anyhow::Result<Option<String>> {
89 let file_is_readable = path.exists()
90 && path.is_file()
91 && (((path.metadata()?.permissions().mode() & 0o400 != 0
92 && nix::unistd::geteuid().as_raw() == path.metadata()?.uid())
93 || (path.metadata()?.permissions().mode() & 0o040 != 0
94 && nix::unistd::getegid().as_raw() == path.metadata()?.gid())
95 || (path.metadata()?.permissions().mode() & 0o004 != 0))
96 || caps::has_cap(
97 None,
98 caps::CapSet::Effective,
99 caps::Capability::CAP_DAC_READ_SEARCH,
100 )?)
101 && path.metadata()?.len() > 0;
102
103 if file_is_readable {
104 Ok(Some(std::fs::read_to_string(path)?.trim().to_string()))
105 } else {
106 Ok(None)
107 }
108}
109
110fn get_local_user(
114 username: &str,
115 utmp_records: Option<Vec<UtmpxRecord>>,
116) -> anyhow::Result<Option<FingerResponseStructuredUserEntry>> {
117 tracing::trace!(
118 "Retrieving local user information for username: {}",
119 username
120 );
121 let username = username.to_string();
122 let user_entry = match nix::unistd::User::from_name(&username) {
123 Ok(Some(user)) => user,
124 Ok(None) => return Ok(None),
125 Err(err) => {
126 return Err(anyhow::anyhow!(
127 "Failed to get user entry for {}: {}",
128 username,
129 err
130 ));
131 }
132 };
133
134 let nofinger_path = user_entry.dir.join(".nofinger");
135 if nofinger_path.exists() {
136 return Ok(None);
137 }
138
139 let full_name = user_entry.name;
140 let home_dir = user_entry.dir.clone();
141 let shell = user_entry.shell;
142
143 let gecos_fields: Vec<&str> = full_name.split(',').collect();
144
145 let office = gecos_fields.get(1).map(|s| s.to_string());
146 let office_phone = gecos_fields.get(2).map(|s| s.to_string());
147 let home_phone = gecos_fields.get(3).map(|s| s.to_string());
148
149 let hostname = hostname()?.to_str().unwrap_or("localhost").to_string();
150
151 let utmpx_records = match utmp_records {
152 Some(records) => records,
153 None => Utmpx::iter_all_records()
154 .filter(|entry| entry.user() == username)
155 .filter(|entry| entry.is_user_process())
156 .collect::<Vec<_>>(),
157 };
158
159 let user_never_logged_in = utmpx_records.is_empty();
161
162 let now = Utc::now().with_nanosecond(0).unwrap_or(Utc::now());
163 let sessions: Vec<FingerResponseUserSession> = utmpx_records
164 .into_iter()
165 .filter_map(|entry| {
166 let login_time = entry
167 .login_time()
168 .checked_to_utc()
169 .and_then(|t| DateTime::<Utc>::from_timestamp_secs(t.unix_timestamp()))?;
170
171 let tty_device_path = Path::new("/dev").join(entry.tty_device());
172 let tty_device_stat = stat(&tty_device_path).ok();
173
174 let tty_is_x_console = entry.tty_device().starts_with(':');
175
176 let idle_time = if tty_is_x_console {
177 None
178 } else {
179 tty_device_stat.and_then(|st| {
180 let last_active = DateTime::<Utc>::from_timestamp_secs(st.st_atime)?;
181 let result = (now - last_active).max(Duration::zero());
182 if result == Duration::zero() {
183 None
184 } else {
185 debug_assert!(
186 result.num_seconds() >= 0,
187 "Idle time should never be negative"
188 );
189
190 Some(result)
191 }
192 })
193 };
194
195 let messages_on =
196 !tty_is_x_console &&
198 tty_device_stat
201 .map(|st| st.st_mode & 0o220 == 0o220)
202 .unwrap_or(false);
203
204 Some(FingerResponseUserSession::new(
205 entry.tty_device(),
206 login_time,
207 Some(hostname.clone()),
208 idle_time,
209 messages_on,
210 ))
211 })
212 .collect();
213
214 let email_status = local_email::detect_new_mail_for_user(&username, &home_dir)?;
215
216 let forward_path = user_entry.dir.join(".forward");
217 let forward = read_file_content_if_exists(&forward_path)?;
218
219 let pgpkey_path = user_entry.dir.join(".pgpkey");
220 let pgpkey = read_file_content_if_exists(&pgpkey_path)?;
221
222 let project_path = user_entry.dir.join(".project");
223 let project = read_file_content_if_exists(&project_path)?;
224
225 let plan_path = user_entry.dir.join(".plan");
226 let plan = read_file_content_if_exists(&plan_path)?;
227
228 Ok(Some(FingerResponseStructuredUserEntry::new(
229 username,
230 full_name,
231 home_dir,
232 shell,
233 office,
234 office_phone,
235 home_phone,
236 user_never_logged_in,
237 sessions,
238 forward,
239 email_status,
240 pgpkey,
241 project,
242 plan,
243 )))
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_finger_root() {
252 let user_entry = get_local_user("root", None).unwrap().unwrap();
253 assert_eq!(user_entry.username, "root");
254 }
255
256 }