fingerd: add basic mailbox parsing functionality
This commit is contained in:
@@ -117,9 +117,15 @@ in {
|
|||||||
BindReadOnlyPaths = [
|
BindReadOnlyPaths = [
|
||||||
builtins.storeDir
|
builtins.storeDir
|
||||||
"/etc"
|
"/etc"
|
||||||
|
|
||||||
# NOTE: need logind socket for utmp entries
|
# NOTE: need logind socket for utmp entries
|
||||||
"/run/systemd"
|
"/run/systemd"
|
||||||
|
|
||||||
"/home"
|
"/home"
|
||||||
|
|
||||||
|
# NOTE: finger might need access to mail directories
|
||||||
|
"-/var/spool"
|
||||||
|
"-/var/mail"
|
||||||
];
|
];
|
||||||
|
|
||||||
UMask = "0077";
|
UMask = "0077";
|
||||||
|
|||||||
+5
-265
@@ -1,17 +1,10 @@
|
|||||||
use std::{
|
mod local_email;
|
||||||
net::hostname,
|
mod local_user_info;
|
||||||
os::unix::fs::{MetadataExt, PermissionsExt},
|
mod remote_user_info;
|
||||||
path::Path,
|
|
||||||
};
|
pub use local_user_info::{finger_utmp_users, search_for_user};
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Timelike, Utc};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use nix::sys::stat::stat;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use users::all_users;
|
|
||||||
use uucore::utmpx::{Utmpx, UtmpxRecord};
|
|
||||||
|
|
||||||
use crate::proto::finger_protocol::{FingerResponseUserEntry, FingerResponseUserSession};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum FingerRequestNetworking {
|
pub enum FingerRequestNetworking {
|
||||||
@@ -26,256 +19,3 @@ pub enum FingerRequestInfo {
|
|||||||
ShortOffice { restrict_gecos: bool },
|
ShortOffice { restrict_gecos: bool },
|
||||||
Long { prevent_files: bool },
|
Long { prevent_files: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search for users whose username or full name contains the search string.
|
|
||||||
pub fn search_for_user(
|
|
||||||
search_string: &str,
|
|
||||||
match_fullnames: bool,
|
|
||||||
_request_info: &FingerRequestInfo,
|
|
||||||
) -> Vec<anyhow::Result<FingerResponseUserEntry>> {
|
|
||||||
(unsafe { all_users() })
|
|
||||||
.filter_map(|user| {
|
|
||||||
let user = match nix::unistd::User::from_uid(user.uid().into()) {
|
|
||||||
Ok(Some(user)) => user,
|
|
||||||
Ok(None) => return None, // User disappeared, skip
|
|
||||||
Err(e) => {
|
|
||||||
return Some(Err(anyhow::anyhow!(
|
|
||||||
"Failed to get user entry for UID {}: {}",
|
|
||||||
user.uid(),
|
|
||||||
e
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let username = user.name;
|
|
||||||
let full_name = String::from_utf8_lossy(
|
|
||||||
user.gecos
|
|
||||||
.as_bytes()
|
|
||||||
.split(|&b| b == b',')
|
|
||||||
.next()
|
|
||||||
.unwrap_or(&[]),
|
|
||||||
)
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let matches_username = username.contains(search_string);
|
|
||||||
let matches_fullname = match_fullnames && full_name.contains(search_string);
|
|
||||||
if matches_username || matches_fullname {
|
|
||||||
match get_local_user(&username, None) {
|
|
||||||
Ok(Some(user_entry)) => Some(Ok(user_entry)),
|
|
||||||
Ok(None) => None, // User exists but has .nofinger, skip
|
|
||||||
Err(err) => Some(Err(err)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve information about all users currently logged in, based on utmpx records.
|
|
||||||
pub fn finger_utmp_users(
|
|
||||||
_request_info: &FingerRequestInfo,
|
|
||||||
) -> Vec<anyhow::Result<FingerResponseUserEntry>> {
|
|
||||||
Utmpx::iter_all_records()
|
|
||||||
.filter(|entry| entry.is_user_process())
|
|
||||||
.into_group_map_by(|entry| entry.user())
|
|
||||||
.into_iter()
|
|
||||||
.map(|(username, records)| get_local_user(&username, Some(records)))
|
|
||||||
.filter_map(|result| match result {
|
|
||||||
Ok(Some(user_entry)) => Some(Ok(user_entry)),
|
|
||||||
Ok(None) => None, // User has .nofinger, skip
|
|
||||||
Err(err) => Some(Err(err)),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to read the content of a file if it exists and is readable,
|
|
||||||
/// returning None if the file does not exist or is not readable.
|
|
||||||
fn read_file_content_if_exists(path: &Path) -> anyhow::Result<Option<String>> {
|
|
||||||
let file_is_readable = path.exists()
|
|
||||||
&& path.is_file()
|
|
||||||
&& (((path.metadata()?.permissions().mode() & 0o400 != 0
|
|
||||||
&& nix::unistd::geteuid().as_raw() == path.metadata()?.uid())
|
|
||||||
|| (path.metadata()?.permissions().mode() & 0o040 != 0
|
|
||||||
&& nix::unistd::getegid().as_raw() == path.metadata()?.gid())
|
|
||||||
|| (path.metadata()?.permissions().mode() & 0o004 != 0))
|
|
||||||
|| caps::has_cap(
|
|
||||||
None,
|
|
||||||
caps::CapSet::Effective,
|
|
||||||
caps::Capability::CAP_DAC_READ_SEARCH,
|
|
||||||
)?)
|
|
||||||
&& path.metadata()?.len() > 0;
|
|
||||||
|
|
||||||
if file_is_readable {
|
|
||||||
Ok(Some(std::fs::read_to_string(path)?.trim().to_string()))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve local user information for the given username.
|
|
||||||
///
|
|
||||||
/// Returns None if the user does not exist.
|
|
||||||
fn get_local_user(
|
|
||||||
username: &str,
|
|
||||||
utmp_records: Option<Vec<UtmpxRecord>>,
|
|
||||||
) -> anyhow::Result<Option<FingerResponseUserEntry>> {
|
|
||||||
tracing::trace!(
|
|
||||||
"Retrieving local user information for username: {}",
|
|
||||||
username
|
|
||||||
);
|
|
||||||
let username = username.to_string();
|
|
||||||
let user_entry = match nix::unistd::User::from_name(&username) {
|
|
||||||
Ok(Some(user)) => user,
|
|
||||||
Ok(None) => return Ok(None),
|
|
||||||
Err(err) => {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Failed to get user entry for {}: {}",
|
|
||||||
username,
|
|
||||||
err
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let nofinger_path = user_entry.dir.join(".nofinger");
|
|
||||||
if nofinger_path.exists() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let full_name = user_entry.name;
|
|
||||||
let home_dir = user_entry.dir.clone();
|
|
||||||
let shell = user_entry.shell;
|
|
||||||
|
|
||||||
let gecos_fields: Vec<&str> = full_name.split(',').collect();
|
|
||||||
|
|
||||||
let office = gecos_fields.get(1).map(|s| s.to_string());
|
|
||||||
let office_phone = gecos_fields.get(2).map(|s| s.to_string());
|
|
||||||
let home_phone = gecos_fields.get(3).map(|s| s.to_string());
|
|
||||||
|
|
||||||
let hostname = hostname()?.to_str().unwrap_or("localhost").to_string();
|
|
||||||
|
|
||||||
let utmpx_records = match utmp_records {
|
|
||||||
Some(records) => records,
|
|
||||||
None => Utmpx::iter_all_records()
|
|
||||||
.filter(|entry| entry.user() == username)
|
|
||||||
.filter(|entry| entry.is_user_process())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Don't use utmp entries for this, read from lastlog instead
|
|
||||||
let user_never_logged_in = utmpx_records.is_empty();
|
|
||||||
|
|
||||||
let now = Utc::now().with_nanosecond(0).unwrap_or(Utc::now());
|
|
||||||
let sessions: Vec<FingerResponseUserSession> = utmpx_records
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|entry| {
|
|
||||||
let login_time = entry
|
|
||||||
.login_time()
|
|
||||||
.checked_to_utc()
|
|
||||||
.and_then(|t| DateTime::<Utc>::from_timestamp_secs(t.unix_timestamp()))?;
|
|
||||||
|
|
||||||
let tty_device_stat = stat(&Path::new("/dev").join(entry.tty_device())).ok();
|
|
||||||
|
|
||||||
let idle_time = tty_device_stat
|
|
||||||
.and_then(|st| {
|
|
||||||
let last_active = DateTime::<Utc>::from_timestamp_secs(st.st_atime)?;
|
|
||||||
Some((now - last_active).max(Duration::zero()))
|
|
||||||
})
|
|
||||||
.unwrap_or(Duration::zero());
|
|
||||||
|
|
||||||
// Check if the write permission for "others" is set
|
|
||||||
let messages_on = tty_device_stat
|
|
||||||
.map(|st| st.st_mode & 0o002 != 0)
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
debug_assert!(
|
|
||||||
idle_time.num_seconds() >= 0,
|
|
||||||
"Idle time should never be negative"
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(FingerResponseUserSession::new(
|
|
||||||
entry.tty_device(),
|
|
||||||
login_time,
|
|
||||||
idle_time,
|
|
||||||
hostname.clone(),
|
|
||||||
messages_on,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let forward_path = user_entry.dir.join(".forward");
|
|
||||||
let forward = read_file_content_if_exists(&forward_path)?;
|
|
||||||
|
|
||||||
let pgpkey_path = user_entry.dir.join(".pgpkey");
|
|
||||||
let pgpkey = read_file_content_if_exists(&pgpkey_path)?;
|
|
||||||
|
|
||||||
let project_path = user_entry.dir.join(".project");
|
|
||||||
let project = read_file_content_if_exists(&project_path)?;
|
|
||||||
|
|
||||||
let plan_path = user_entry.dir.join(".plan");
|
|
||||||
let plan = read_file_content_if_exists(&plan_path)?;
|
|
||||||
|
|
||||||
Ok(Some(FingerResponseUserEntry::new(
|
|
||||||
username,
|
|
||||||
full_name,
|
|
||||||
home_dir,
|
|
||||||
shell,
|
|
||||||
office,
|
|
||||||
office_phone,
|
|
||||||
home_phone,
|
|
||||||
user_never_logged_in,
|
|
||||||
sessions,
|
|
||||||
forward,
|
|
||||||
None,
|
|
||||||
pgpkey,
|
|
||||||
project,
|
|
||||||
plan,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// /// Retrieve remote user information for the given username on the specified host.
|
|
||||||
// ///
|
|
||||||
// /// Returns None if the user does not exist or no information is available.
|
|
||||||
// async fn get_remote_user(username: &str, host: &str) -> anyhow::Result<Option<RawFingerResponse>> {
|
|
||||||
// let addr = format!("{}:79", host);
|
|
||||||
// let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
|
|
||||||
|
|
||||||
// if socket_addrs.is_empty() {
|
|
||||||
// return Err(anyhow::anyhow!(
|
|
||||||
// "Could not resolve address for host {}",
|
|
||||||
// host
|
|
||||||
// ));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let socket_addr = socket_addrs[0];
|
|
||||||
|
|
||||||
// let mut stream = TcpStream::connect(socket_addr).await?;
|
|
||||||
|
|
||||||
// let request = FingerRequest::new(false, username.to_string());
|
|
||||||
// let request_bytes = request.to_bytes();
|
|
||||||
// stream.write_all(&request_bytes).await?;
|
|
||||||
|
|
||||||
// let mut response_bytes = Vec::new();
|
|
||||||
// stream.read_to_end(&mut response_bytes).await?;
|
|
||||||
|
|
||||||
// let response = RawFingerResponse::from_bytes(&response_bytes);
|
|
||||||
|
|
||||||
// if response.is_empty() {
|
|
||||||
// Ok(None)
|
|
||||||
// } else {
|
|
||||||
// Ok(Some(response))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_finger_root() {
|
|
||||||
let user_entry = get_local_user("root", None).unwrap().unwrap();
|
|
||||||
assert_eq!(user_entry.username, "root");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: test serialization roundtrip
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::proto::finger_protocol::MailStatus;
|
||||||
|
|
||||||
|
pub fn detect_new_mail_for_user(
|
||||||
|
username: &str,
|
||||||
|
homedir: &Path,
|
||||||
|
) -> anyhow::Result<Option<MailStatus>> {
|
||||||
|
let mail_storage_paths: Vec<PathBuf> = vec![
|
||||||
|
homedir.join("Mail"),
|
||||||
|
homedir.join("Mailbox"),
|
||||||
|
homedir.join("Maildir"),
|
||||||
|
PathBuf::from("/var/mail").join(username),
|
||||||
|
PathBuf::from("/var/spool/mail").join(username),
|
||||||
|
PathBuf::from("/var/maildir").join(username),
|
||||||
|
PathBuf::from("/var/spool/maildir").join(username),
|
||||||
|
];
|
||||||
|
|
||||||
|
for path in mail_storage_paths {
|
||||||
|
tracing::debug!("Checking for mail at path: {}", path.display());
|
||||||
|
if path.is_dir() && path.join("new").is_dir() && path.join("cur").is_dir() {
|
||||||
|
tracing::debug!("Detected maildir structure at path: {}", path.display());
|
||||||
|
if let Ok(status) = detect_new_mail_by_maildir(&path) {
|
||||||
|
return Ok(Some(status));
|
||||||
|
}
|
||||||
|
} else if path.is_file() {
|
||||||
|
tracing::debug!("Detected mailbox file at path: {}", path.display());
|
||||||
|
if let Ok(status) = detect_new_mail_by_mailbox(&path) {
|
||||||
|
return Ok(Some(status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_new_mail_by_mailbox(mailbox_path: &Path) -> anyhow::Result<MailStatus> {
|
||||||
|
let stat = nix::sys::stat::stat(mailbox_path)?;
|
||||||
|
|
||||||
|
let mail_recv = stat.st_mtime;
|
||||||
|
let mail_read = stat.st_atime;
|
||||||
|
|
||||||
|
if mail_recv > mail_read {
|
||||||
|
Ok(MailStatus::NewMailReceived {
|
||||||
|
received_time: Utc
|
||||||
|
.timestamp_opt(mail_recv, 0)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_else(|| Default::default()),
|
||||||
|
unread_since: Utc
|
||||||
|
.timestamp_opt(mail_read, 0)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_else(|| Default::default()),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(MailStatus::MailLastRead(
|
||||||
|
Utc.timestamp_opt(mail_read, 0)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_else(|| Default::default()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_new_mail_by_maildir(maildir_path: &Path) -> anyhow::Result<MailStatus> {
|
||||||
|
let new_mail_path = maildir_path.join("new");
|
||||||
|
let cur_mail_path = maildir_path.join("cur");
|
||||||
|
|
||||||
|
let mail_recv = nix::sys::stat::stat(&new_mail_path)?.st_mtime;
|
||||||
|
let mail_read = nix::sys::stat::stat(&cur_mail_path)?.st_mtime;
|
||||||
|
|
||||||
|
if new_mail_path.read_dir()?.next().is_none() && cur_mail_path.read_dir()?.next().is_none() {
|
||||||
|
Ok(MailStatus::NoMail)
|
||||||
|
} else if mail_recv > mail_read {
|
||||||
|
Ok(MailStatus::NewMailReceived {
|
||||||
|
received_time: Utc
|
||||||
|
.timestamp_opt(mail_recv, 0)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_else(|| Default::default()),
|
||||||
|
unread_since: Utc
|
||||||
|
.timestamp_opt(mail_read, 0)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_else(|| Default::default()),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(MailStatus::MailLastRead(
|
||||||
|
Utc.timestamp_opt(mail_read, 0)
|
||||||
|
.single()
|
||||||
|
.unwrap_or_else(|| Default::default()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
use std::{
|
||||||
|
net::hostname,
|
||||||
|
os::unix::fs::{MetadataExt, PermissionsExt},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Duration, Timelike, Utc};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use nix::sys::stat::stat;
|
||||||
|
use users::all_users;
|
||||||
|
use uucore::utmpx::{Utmpx, UtmpxRecord};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
proto::finger_protocol::{FingerResponseUserEntry, FingerResponseUserSession},
|
||||||
|
server::fingerd::{FingerRequestInfo, local_email},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Search for users whose username or full name contains the search string.
|
||||||
|
pub fn search_for_user(
|
||||||
|
search_string: &str,
|
||||||
|
match_fullnames: bool,
|
||||||
|
_request_info: &FingerRequestInfo,
|
||||||
|
) -> Vec<anyhow::Result<FingerResponseUserEntry>> {
|
||||||
|
(unsafe { all_users() })
|
||||||
|
.filter_map(|user| {
|
||||||
|
let user = match nix::unistd::User::from_uid(user.uid().into()) {
|
||||||
|
Ok(Some(user)) => user,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"User with UID {} exists but could not retrieve user entry",
|
||||||
|
user.uid()
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Some(Err(anyhow::anyhow!(
|
||||||
|
"Failed to get user entry for UID {}: {}",
|
||||||
|
user.uid(),
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let username = user.name;
|
||||||
|
let full_name = String::from_utf8_lossy(
|
||||||
|
user.gecos
|
||||||
|
.as_bytes()
|
||||||
|
.split(|&b| b == b',')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(&[]),
|
||||||
|
)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let matches_username = username.contains(search_string);
|
||||||
|
let matches_fullname = match_fullnames && full_name.contains(search_string);
|
||||||
|
if matches_username || matches_fullname {
|
||||||
|
match get_local_user(&username, None) {
|
||||||
|
Ok(Some(user_entry)) => Some(Ok(user_entry)),
|
||||||
|
Ok(None) => None, // User exists but has .nofinger, skip
|
||||||
|
Err(err) => Some(Err(err)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve information about all users currently logged in, based on utmpx records.
|
||||||
|
pub fn finger_utmp_users(
|
||||||
|
_request_info: &FingerRequestInfo,
|
||||||
|
) -> Vec<anyhow::Result<FingerResponseUserEntry>> {
|
||||||
|
Utmpx::iter_all_records()
|
||||||
|
.filter(|entry| entry.is_user_process())
|
||||||
|
.into_group_map_by(|entry| entry.user())
|
||||||
|
.into_iter()
|
||||||
|
.map(|(username, records)| get_local_user(&username, Some(records)))
|
||||||
|
.filter_map(|result| match result {
|
||||||
|
Ok(Some(user_entry)) => Some(Ok(user_entry)),
|
||||||
|
Ok(None) => None, // User has .nofinger, skip
|
||||||
|
Err(err) => Some(Err(err)),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to read the content of a file if it exists and is readable,
|
||||||
|
/// returning None if the file does not exist or is not readable.
|
||||||
|
fn read_file_content_if_exists(path: &Path) -> anyhow::Result<Option<String>> {
|
||||||
|
let file_is_readable = path.exists()
|
||||||
|
&& path.is_file()
|
||||||
|
&& (((path.metadata()?.permissions().mode() & 0o400 != 0
|
||||||
|
&& nix::unistd::geteuid().as_raw() == path.metadata()?.uid())
|
||||||
|
|| (path.metadata()?.permissions().mode() & 0o040 != 0
|
||||||
|
&& nix::unistd::getegid().as_raw() == path.metadata()?.gid())
|
||||||
|
|| (path.metadata()?.permissions().mode() & 0o004 != 0))
|
||||||
|
|| caps::has_cap(
|
||||||
|
None,
|
||||||
|
caps::CapSet::Effective,
|
||||||
|
caps::Capability::CAP_DAC_READ_SEARCH,
|
||||||
|
)?)
|
||||||
|
&& path.metadata()?.len() > 0;
|
||||||
|
|
||||||
|
if file_is_readable {
|
||||||
|
Ok(Some(std::fs::read_to_string(path)?.trim().to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve local user information for the given username.
|
||||||
|
///
|
||||||
|
/// Returns None if the user does not exist.
|
||||||
|
fn get_local_user(
|
||||||
|
username: &str,
|
||||||
|
utmp_records: Option<Vec<UtmpxRecord>>,
|
||||||
|
) -> anyhow::Result<Option<FingerResponseUserEntry>> {
|
||||||
|
tracing::trace!(
|
||||||
|
"Retrieving local user information for username: {}",
|
||||||
|
username
|
||||||
|
);
|
||||||
|
let username = username.to_string();
|
||||||
|
let user_entry = match nix::unistd::User::from_name(&username) {
|
||||||
|
Ok(Some(user)) => user,
|
||||||
|
Ok(None) => return Ok(None),
|
||||||
|
Err(err) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to get user entry for {}: {}",
|
||||||
|
username,
|
||||||
|
err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let nofinger_path = user_entry.dir.join(".nofinger");
|
||||||
|
if nofinger_path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let full_name = user_entry.name;
|
||||||
|
let home_dir = user_entry.dir.clone();
|
||||||
|
let shell = user_entry.shell;
|
||||||
|
|
||||||
|
let gecos_fields: Vec<&str> = full_name.split(',').collect();
|
||||||
|
|
||||||
|
let office = gecos_fields.get(1).map(|s| s.to_string());
|
||||||
|
let office_phone = gecos_fields.get(2).map(|s| s.to_string());
|
||||||
|
let home_phone = gecos_fields.get(3).map(|s| s.to_string());
|
||||||
|
|
||||||
|
let hostname = hostname()?.to_str().unwrap_or("localhost").to_string();
|
||||||
|
|
||||||
|
let utmpx_records = match utmp_records {
|
||||||
|
Some(records) => records,
|
||||||
|
None => Utmpx::iter_all_records()
|
||||||
|
.filter(|entry| entry.user() == username)
|
||||||
|
.filter(|entry| entry.is_user_process())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Don't use utmp entries for this, read from lastlog instead
|
||||||
|
let user_never_logged_in = utmpx_records.is_empty();
|
||||||
|
|
||||||
|
let now = Utc::now().with_nanosecond(0).unwrap_or(Utc::now());
|
||||||
|
let sessions: Vec<FingerResponseUserSession> = utmpx_records
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let login_time = entry
|
||||||
|
.login_time()
|
||||||
|
.checked_to_utc()
|
||||||
|
.and_then(|t| DateTime::<Utc>::from_timestamp_secs(t.unix_timestamp()))?;
|
||||||
|
|
||||||
|
let tty_device_stat = stat(&Path::new("/dev").join(entry.tty_device())).ok();
|
||||||
|
|
||||||
|
let idle_time = tty_device_stat
|
||||||
|
.and_then(|st| {
|
||||||
|
let last_active = DateTime::<Utc>::from_timestamp_secs(st.st_atime)?;
|
||||||
|
Some((now - last_active).max(Duration::zero()))
|
||||||
|
})
|
||||||
|
.unwrap_or(Duration::zero());
|
||||||
|
|
||||||
|
// Check if the write permission for "others" is set
|
||||||
|
let messages_on = tty_device_stat
|
||||||
|
.map(|st| st.st_mode & 0o002 != 0)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
debug_assert!(
|
||||||
|
idle_time.num_seconds() >= 0,
|
||||||
|
"Idle time should never be negative"
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(FingerResponseUserSession::new(
|
||||||
|
entry.tty_device(),
|
||||||
|
login_time,
|
||||||
|
idle_time,
|
||||||
|
hostname.clone(),
|
||||||
|
messages_on,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let email_status = local_email::detect_new_mail_for_user(&username, &home_dir)?;
|
||||||
|
|
||||||
|
let forward_path = user_entry.dir.join(".forward");
|
||||||
|
let forward = read_file_content_if_exists(&forward_path)?;
|
||||||
|
|
||||||
|
let pgpkey_path = user_entry.dir.join(".pgpkey");
|
||||||
|
let pgpkey = read_file_content_if_exists(&pgpkey_path)?;
|
||||||
|
|
||||||
|
let project_path = user_entry.dir.join(".project");
|
||||||
|
let project = read_file_content_if_exists(&project_path)?;
|
||||||
|
|
||||||
|
let plan_path = user_entry.dir.join(".plan");
|
||||||
|
let plan = read_file_content_if_exists(&plan_path)?;
|
||||||
|
|
||||||
|
Ok(Some(FingerResponseUserEntry::new(
|
||||||
|
username,
|
||||||
|
full_name,
|
||||||
|
home_dir,
|
||||||
|
shell,
|
||||||
|
office,
|
||||||
|
office_phone,
|
||||||
|
home_phone,
|
||||||
|
user_never_logged_in,
|
||||||
|
sessions,
|
||||||
|
forward,
|
||||||
|
email_status,
|
||||||
|
pgpkey,
|
||||||
|
project,
|
||||||
|
plan,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_finger_root() {
|
||||||
|
let user_entry = get_local_user("root", None).unwrap().unwrap();
|
||||||
|
assert_eq!(user_entry.username, "root");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test serialization roundtrip
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// /// Retrieve remote user information for the given username on the specified host.
|
||||||
|
// ///
|
||||||
|
// /// Returns None if the user does not exist or no information is available.
|
||||||
|
// async fn get_remote_user(username: &str, host: &str) -> anyhow::Result<Option<RawFingerResponse>> {
|
||||||
|
// let addr = format!("{}:79", host);
|
||||||
|
// let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
|
||||||
|
|
||||||
|
// if socket_addrs.is_empty() {
|
||||||
|
// return Err(anyhow::anyhow!(
|
||||||
|
// "Could not resolve address for host {}",
|
||||||
|
// host
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let socket_addr = socket_addrs[0];
|
||||||
|
|
||||||
|
// let mut stream = TcpStream::connect(socket_addr).await?;
|
||||||
|
|
||||||
|
// let request = FingerRequest::new(false, username.to_string());
|
||||||
|
// let request_bytes = request.to_bytes();
|
||||||
|
// stream.write_all(&request_bytes).await?;
|
||||||
|
|
||||||
|
// let mut response_bytes = Vec::new();
|
||||||
|
// stream.read_to_end(&mut response_bytes).await?;
|
||||||
|
|
||||||
|
// let response = RawFingerResponse::from_bytes(&response_bytes);
|
||||||
|
|
||||||
|
// if response.is_empty() {
|
||||||
|
// Ok(None)
|
||||||
|
// } else {
|
||||||
|
// Ok(Some(response))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
Reference in New Issue
Block a user