Skip to main content

roowho2_lib/server/
fingerd.rs

1use 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
35/// Retrieve local user information for the given username.
36///
37/// Returns None if the user does not exist.
38pub 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    // TODO: Don't use utmp entries for this, read from lastlog instead
75    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            // Check if the write permission for "others" is set
95            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// /// Retrieve remote user information for the given username on the specified host.
145// ///
146// /// Returns None if the user does not exist or no information is available.
147// async fn get_remote_user(username: &str, host: &str) -> anyhow::Result<Option<RawFingerResponse>> {
148//     let addr = format!("{}:79", host);
149//     let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
150
151//     if socket_addrs.is_empty() {
152//         return Err(anyhow::anyhow!(
153//             "Could not resolve address for host {}",
154//             host
155//         ));
156//     }
157
158//     let socket_addr = socket_addrs[0];
159
160//     let mut stream = TcpStream::connect(socket_addr).await?;
161
162//     let request = FingerRequest::new(false, username.to_string());
163//     let request_bytes = request.to_bytes();
164//     stream.write_all(&request_bytes).await?;
165
166//     let mut response_bytes = Vec::new();
167//     stream.read_to_end(&mut response_bytes).await?;
168
169//     let response = RawFingerResponse::from_bytes(&response_bytes);
170
171//     if response.is_empty() {
172//         Ok(None)
173//     } else {
174//         Ok(Some(response))
175//     }
176// }
177
178#[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    // TODO: test serialization roundtrip
189}