1
use std::{
2
    net::hostname,
3
    os::unix::fs::{MetadataExt, PermissionsExt},
4
    path::Path,
5
};
6

            
7
use chrono::{DateTime, Duration, Timelike, Utc};
8
use nix::sys::stat::stat;
9
use uucore::utmpx::Utmpx;
10

            
11
use crate::proto::finger_protocol::{FingerResponseUserEntry, FingerResponseUserSession};
12

            
13
4
fn read_file_content_if_exists(path: &Path) -> anyhow::Result<Option<String>> {
14
4
    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
4
    if file_is_readable {
29
        Ok(Some(std::fs::read_to_string(path)?.trim().to_string()))
30
    } else {
31
4
        Ok(None)
32
    }
33
4
}
34

            
35
/// Retrieve local user information for the given username.
36
///
37
/// Returns None if the user does not exist.
38
1
pub fn get_local_user(username: &str) -> anyhow::Result<Option<FingerResponseUserEntry>> {
39
1
    let username = username.to_string();
40
1
    let user_entry = match nix::unistd::User::from_name(&username) {
41
1
        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
1
    let nofinger_path = user_entry.dir.join(".nofinger");
53
1
    if nofinger_path.exists() {
54
        return Ok(None);
55
1
    }
56

            
57
1
    let full_name = user_entry.name;
58
1
    let home_dir = user_entry.dir.clone();
59
1
    let shell = user_entry.shell;
60

            
61
1
    let gecos_fields: Vec<&str> = full_name.split(',').collect();
62

            
63
1
    let office = gecos_fields.get(1).map(|s| s.to_string());
64
1
    let office_phone = gecos_fields.get(2).map(|s| s.to_string());
65
1
    let home_phone = gecos_fields.get(3).map(|s| s.to_string());
66

            
67
1
    let hostname = hostname()?.to_str().unwrap_or("localhost").to_string();
68

            
69
1
    let mut utmpx_records = Utmpx::iter_all_records()
70
1
        .filter(|entry| entry.user() == username)
71
1
        .filter(|entry| entry.is_user_process())
72
1
        .peekable();
73

            
74
    // TODO: Don't use utmp entries for this, read from lastlog instead
75
1
    let user_never_logged_in = utmpx_records.peek().is_none();
76

            
77
1
    let now = Utc::now().with_nanosecond(0).unwrap_or(Utc::now());
78
1
    let sessions: Vec<FingerResponseUserSession> = utmpx_records
79
1
        .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
1
        .collect();
113

            
114
1
    let forward_path = user_entry.dir.join(".forward");
115
1
    let forward = read_file_content_if_exists(&forward_path)?;
116

            
117
1
    let pgpkey_path = user_entry.dir.join(".pgpkey");
118
1
    let pgpkey = read_file_content_if_exists(&pgpkey_path)?;
119

            
120
1
    let project_path = user_entry.dir.join(".project");
121
1
    let project = read_file_content_if_exists(&project_path)?;
122

            
123
1
    let plan_path = user_entry.dir.join(".plan");
124
1
    let plan = read_file_content_if_exists(&plan_path)?;
125

            
126
1
    Ok(Some(FingerResponseUserEntry::new(
127
1
        username,
128
1
        full_name,
129
1
        home_dir,
130
1
        shell,
131
1
        office,
132
1
        office_phone,
133
1
        home_phone,
134
1
        user_never_logged_in,
135
1
        sessions,
136
1
        forward,
137
1
        None,
138
1
        pgpkey,
139
1
        project,
140
1
        plan,
141
1
    )))
142
1
}
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)]
179
mod tests {
180
    use super::*;
181

            
182
    #[test]
183
1
    fn test_finger_root() {
184
1
        let user_entry = get_local_user("root").unwrap().unwrap();
185
1
        assert_eq!(user_entry.username, "root");
186
1
    }
187

            
188
    // TODO: test serialization roundtrip
189
}