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 itertools::Itertools;
9
use nix::sys::stat::stat;
10
use users::all_users;
11
use uucore::utmpx::{Utmpx, UtmpxRecord};
12

            
13
use crate::{
14
    proto::finger_protocol::{FingerResponseStructuredUserEntry, FingerResponseUserSession},
15
    server::fingerd::{FingerRequestInfo, local_email},
16
};
17

            
18
/// Search for users whose username or full name contains the search string.
19
pub 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, // User exists but has .nofinger, skip
60
                    Err(err) => Some(Err(err)),
61
                }
62
            } else {
63
                None
64
            }
65
        })
66
        .collect()
67
}
68

            
69
/// Retrieve information about all users currently logged in, based on utmpx records.
70
pub 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, // User has .nofinger, skip
81
            Err(err) => Some(Err(err)),
82
        })
83
        .collect()
84
}
85

            
86
/// Helper function to read the content of a file if it exists and is readable,
87
/// returning None if the file does not exist or is not readable.
88
4
fn read_file_content_if_exists(path: &Path) -> anyhow::Result<Option<String>> {
89
4
    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
4
    if file_is_readable {
104
        Ok(Some(std::fs::read_to_string(path)?.trim().to_string()))
105
    } else {
106
4
        Ok(None)
107
    }
108
4
}
109

            
110
/// Retrieve local user information for the given username.
111
///
112
/// Returns None if the user does not exist.
113
1
fn get_local_user(
114
1
    username: &str,
115
1
    utmp_records: Option<Vec<UtmpxRecord>>,
116
1
) -> anyhow::Result<Option<FingerResponseStructuredUserEntry>> {
117
1
    tracing::trace!(
118
        "Retrieving local user information for username: {}",
119
        username
120
    );
121
1
    let username = username.to_string();
122
1
    let user_entry = match nix::unistd::User::from_name(&username) {
123
1
        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
1
    let nofinger_path = user_entry.dir.join(".nofinger");
135
1
    if nofinger_path.exists() {
136
        return Ok(None);
137
1
    }
138

            
139
1
    let full_name = user_entry.name;
140
1
    let home_dir = user_entry.dir.clone();
141
1
    let shell = user_entry.shell;
142

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

            
145
1
    let office = gecos_fields.get(1).map(|s| s.to_string());
146
1
    let office_phone = gecos_fields.get(2).map(|s| s.to_string());
147
1
    let home_phone = gecos_fields.get(3).map(|s| s.to_string());
148

            
149
1
    let hostname = hostname()?.to_str().unwrap_or("localhost").to_string();
150

            
151
1
    let utmpx_records = match utmp_records {
152
        Some(records) => records,
153
1
        None => Utmpx::iter_all_records()
154
1
            .filter(|entry| entry.user() == username)
155
1
            .filter(|entry| entry.is_user_process())
156
1
            .collect::<Vec<_>>(),
157
    };
158

            
159
    // TODO: Don't use utmp entries for this, read from lastlog instead
160
1
    let user_never_logged_in = utmpx_records.is_empty();
161

            
162
1
    let now = Utc::now().with_nanosecond(0).unwrap_or(Utc::now());
163
1
    let sessions: Vec<FingerResponseUserSession> = utmpx_records
164
1
        .into_iter()
165
1
        .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
              // X console logins does not show the tty, so messages should be considered off in that case
197
              !tty_is_x_console &&
198
              // Check if the user has write permissions to the tty device,
199
              // indicating whether messages are on or off
200
              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
1
        .collect();
213

            
214
1
    let email_status = local_email::detect_new_mail_for_user(&username, &home_dir)?;
215

            
216
1
    let forward_path = user_entry.dir.join(".forward");
217
1
    let forward = read_file_content_if_exists(&forward_path)?;
218

            
219
1
    let pgpkey_path = user_entry.dir.join(".pgpkey");
220
1
    let pgpkey = read_file_content_if_exists(&pgpkey_path)?;
221

            
222
1
    let project_path = user_entry.dir.join(".project");
223
1
    let project = read_file_content_if_exists(&project_path)?;
224

            
225
1
    let plan_path = user_entry.dir.join(".plan");
226
1
    let plan = read_file_content_if_exists(&plan_path)?;
227

            
228
1
    Ok(Some(FingerResponseStructuredUserEntry::new(
229
1
        username,
230
1
        full_name,
231
1
        home_dir,
232
1
        shell,
233
1
        office,
234
1
        office_phone,
235
1
        home_phone,
236
1
        user_never_logged_in,
237
1
        sessions,
238
1
        forward,
239
1
        email_status,
240
1
        pgpkey,
241
1
        project,
242
1
        plan,
243
1
    )))
244
1
}
245

            
246
#[cfg(test)]
247
mod tests {
248
    use super::*;
249

            
250
    #[test]
251
1
    fn test_finger_root() {
252
1
        let user_entry = get_local_user("root", None).unwrap().unwrap();
253
1
        assert_eq!(user_entry.username, "root");
254
1
    }
255

            
256
    // TODO: test serialization roundtrip
257
}