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