Compare commits

..

1 Commits

Author SHA1 Message Date
oysteikt 75cddc708f WIP 2026-01-11 23:05:24 +09:00
20 changed files with 390 additions and 1721 deletions
Generated
+208 -225
View File
File diff suppressed because it is too large Load Diff
+18 -25
View File
@@ -16,27 +16,23 @@ autobins = false
autolib = false
[dependencies]
anyhow = "1.0.102"
bytes = "1.11.1"
chrono = { version = "0.4.44", features = ["serde"] }
clap = { version = "4.6.1", features = ["derive"] }
futures-util = "0.3.32"
nix = { version = "0.31.2", features = ["hostname", "net", "fs", "user"] }
anyhow = "1.0.100"
bytes = "1.11.0"
chrono = { version = "0.4.42", features = ["serde"] }
clap = { version = "4.5.53", features = ["derive"] }
futures-util = "0.3.31"
nix = { version = "0.30.1", features = ["hostname", "net"] }
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.52.1", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
toml = "1.1.2"
tokio = { version = "1.49.0", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
toml = "0.9.10"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
# onc-rpc = "0.3.2"
sd-notify = { version = "0.5.0", optional = true }
serde_json = "1.0.149"
uucore = { version = "0.8.0", features = ["utmpx"] }
zlink = { version = "0.4.1", features = ["introspection"] }
clap_complete = "4.6.2"
itertools = "0.14.0"
tokio-util = "0.7.18"
caps = "0.5.6"
users = { version = "0.11.0", default-features = false }
sd-notify = { version = "0.4.5", optional = true }
serde_json = "1.0.148"
uucore = { version = "0.5.0", features = ["utmpx"] }
zlink = { version = "0.2.0", features = ["introspection"] }
clap_complete = "4.5.65"
[features]
default = ["systemd"]
@@ -51,10 +47,10 @@ name = "roowhod"
bench = false
path = "src/bin/roowhod.rs"
[[bin]]
name = "finger"
bench = false
path = "src/bin/finger.rs"
# [[bin]]
# name = "finger"
# bench = false
# path = "src/bin/finger.rs"
# [[bin]]
# name = "rup"
@@ -86,6 +82,3 @@ inherits = "release"
strip = true
lto = true
codegen-units = 1
[dev-dependencies]
indoc = "2.0.7"
Generated
+6 -22
View File
@@ -1,27 +1,12 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1776635034,
"narHash": "sha256-OEOJrT3ZfwbChzODfIH4GzlNTtOFuZFWPtW7jIeR8xU=",
"owner": "ipetkov",
"repo": "crane",
"rev": "dc7496d8ea6e526b1254b55d09b966e94673750f",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1776548001,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"lastModified": 1767116409,
"narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"rev": "cad22e7d996aea55ecab064e84834289143e44a0",
"type": "github"
},
"original": {
@@ -33,7 +18,6 @@
},
"root": {
"inputs": {
"crane": "crane",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
@@ -45,11 +29,11 @@
]
},
"locked": {
"lastModified": 1776914043,
"narHash": "sha256-qug5r56yW1qOsjSI99l3Jm15JNT9CvS2otkXNRNtrPI=",
"lastModified": 1767322002,
"narHash": "sha256-yHKXXw2OWfIFsyTjduB4EyFwR0SYYF0hK8xI9z4NIn0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "2d35c4358d7de3a0e606a6e8b27925d981c01cc3",
"rev": "03c6e38661c02a27ca006a284813afdc461e9f7e",
"type": "github"
},
"original": {
+6 -24
View File
@@ -4,11 +4,9 @@
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
crane.url = "github:ipetkov/crane";
};
outputs = { self, nixpkgs, rust-overlay, crane }:
outputs = { self, nixpkgs, rust-overlay}:
let
inherit (nixpkgs) lib;
@@ -73,9 +71,6 @@
roowho2 = final: prev: {
inherit (self.packages.${prev.stdenv.hostPlatform.system}) roowho2;
};
roowho2-crane = final: prev: {
roowho2 = self.packages.${prev.stdenv.hostPlatform.system}.roowho2-crane;
};
};
nixosModules.default = ./nix/module.nix;
@@ -84,13 +79,12 @@
let
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
cargoLock = ./Cargo.lock;
craneLib = (crane.mkLib pkgs).overrideToolchain(p: p.rust-bin.nightly.latest.default);
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
(craneLib.fileset.commonCargoSources ./.)
# ./assets
./src
./Cargo.toml
./Cargo.lock
];
};
@@ -99,25 +93,13 @@
cargo = pkgs.rust-bin.nightly.latest.cargo;
};
in {
default = self.packages.${system}.roowho2-crane;
default = self.packages.${system}.roowho2;
roowho2 = pkgs.callPackage ./nix/package.nix {
inherit cargoToml cargoLock src rustPlatform;
};
roowho2-crane = pkgs.callPackage ./nix/package.nix {
useCrane = true;
inherit cargoToml cargoLock src craneLib;
};
roowho2 = pkgs.callPackage ./nix/package.nix { inherit cargoToml cargoLock src rustPlatform; };
filteredSource = pkgs.runCommandLocal "filtered-source" { } ''
ln -s ${src} $out
'';
});
checks = forAllSystems (system: pkgs: _: {
# NOTE: the non-crane build runs tests during checkPhase
inherit (self.packages.${system}) roowho2;
});
};
}
+1 -63
View File
@@ -16,13 +16,6 @@ in {
default = true;
};
# TODO: allow configuring socket config
};
fingerd = {
enable = lib.mkEnableOption "the fingerd service" // {
default = true;
};
# TODO: allow configuring socket config
};
};
@@ -56,66 +49,11 @@ in {
systemd.services.roowho2 = {
serviceConfig = {
Type = "notify";
ExecStart = "${lib.getExe' cfg.package "roowhod"} --config ${format.generate "roowho2-config.toml" cfg.settings}";
Restart = "on-failure";
DynamicUser = true;
# NOTE: roowho2 might at some point need to read from home directories
# to get user settings, so let's keep these disabled for now.
# PrivateUsers = true;
# ProtectHome = true;
AmbientCapabilities = "";
CapabilityBoundingSet = "";
DeviceAllow = "";
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
# NOTE: all ipc traffic is served through the socket activation fds or provided by systemd
PrivateIPC = true;
PrivateMounts = true;
PrivateTmp = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = "strict";
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SocketBindDeny = "any";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
RuntimeDirectory = "roowho2/root-mnt";
RuntimeDirectoryMode = "0700";
RootDirectory = "/run/roowho2/root-mnt";
BindReadOnlyPaths = [
builtins.storeDir
"/etc"
# NOTE: need logind socket for utmp entries
"/run/systemd"
"/home"
];
UMask = "0077";
# TODO: hardening
};
};
+12 -56
View File
@@ -10,62 +10,22 @@
, cargoToml
, cargoLock
, src
, useCrane ? false
, craneLib ? null
}:
let
mainProgram = "roowhod";
buildFunction = if useCrane then craneLib.buildPackage else rustPlatform.buildRustPackage;
pnameCraneSuffix = lib.optionalString useCrane "-crane";
pname = "${cargoToml.package.name}${pnameCraneSuffix}";
rustPlatformArgs = {
buildType = "releaselto";
buildFeatures = lib.optionals stdenv.hostPlatform.isLinux [
"systemd"
];
cargoLock.lockFile = cargoLock;
doCheck = true;
useNextest = true;
nativeCheckInputs = [
versionCheckHook
];
cargoCheckFeatures = lib.optionals stdenv.hostPlatform.isLinux [
"systemd"
];
};
craneArgs = {
cargoLock = cargoLock;
cargoExtraArgs = lib.escapeShellArgs [ "--features" (lib.concatStringsSep "," (
lib.optionals stdenv.hostPlatform.isLinux [
"systemd"
]
)) ];
cargoArtifacts = craneLib.buildDepsOnly {
inherit pname;
inherit (cargoToml.package) version;
src = lib.fileset.toSource {
root = ../.;
fileset = lib.fileset.unions [
(craneLib.fileset.cargoTomlAndLock ../.)
];
};
cargoLock = cargoLock;
};
};
in
buildFunction ({
inherit pname;
rustPlatform.buildRustPackage {
pname = "roowho2";
inherit (cargoToml.package) version;
inherit src;
cargoLock.lockFile = cargoLock;
buildType = "releaselto";
RUSTFLAGS = "-Zhigher-ranked-assumptions";
buildFeatures = lib.optionals stdenv.hostPlatform.isLinux [
"systemd"
];
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
systemdLibs
];
@@ -81,16 +41,12 @@ buildFunction ({
installShellCompletion "--${shell}" --cmd "${command}" "$TMP/${command}.${shell}"
'') {
shell = [ "bash" "zsh" "fish" ];
command = [ "rwho" "ruptime" "finger" ];
command = [ "rwho" "ruptime" ];
};
in lib.concatStringsSep "\n" installShellCompletions;
meta = with lib; {
license = licenses.bsdOriginalUC;
license = licenses.mit;
platforms = platforms.linux ++ platforms.darwin;
inherit mainProgram;
};
}
//
(if useCrane then craneArgs else rustPlatformArgs)
)
+1 -2
View File
@@ -4,7 +4,7 @@ let
pkgs = import nixpkgs {
inherit system;
overlays = [
self.overlays.roowho2-crane
self.overlays.default
];
};
in
@@ -34,7 +34,6 @@ nixpkgs.lib.nixosSystem {
Try running any of:
rwho
ruptime
finger "alice"
To log into other containers, use:
machinectl shell c1
+12 -97
View File
@@ -1,10 +1,4 @@
use anyhow::Context;
use clap::{CommandFactory, Parser, builder::ArgPredicate};
use clap_complete::{Shell, generate};
use roowho2_lib::server::{
fingerd::{FingerRequestInfo, FingerRequestNetworking},
varlink_api::VarlinkFingerClientProxy,
};
use clap::Parser;
/// User information lookup program
///
@@ -25,7 +19,11 @@ use roowho2_lib::server::{
/// If standard output is a socket, finger will emit a carriage return (^M) before every linefeed (^J).
/// This is for processing remote finger requests when invoked by the daemon.
#[derive(Debug, Parser)]
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
#[command(
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
about,
version,
)]
pub struct Args {
/// Forces finger to use IPv4 addresses only.
#[arg(long, short = '4', conflicts_with = "ipv6")]
@@ -53,25 +51,17 @@ pub struct Args {
/// When used in conjunction with the -s option, the name of the remote host
/// is displayed instead of the office location and office phone.
#[arg(long, short = 'H', requires = "short", conflicts_with = "office")]
#[arg(long, short, requires = "short", conflicts_with = "office")]
host: bool,
/// When used in conjunction with the -s option, the office location and
/// office phone information is displayed instead of the name of the remote host.
// TODO: this is default true, should be false when host is true
#[arg(
long,
short,
requires = "short",
conflicts_with = "host",
default_value = "true",
default_value_if("host", ArgPredicate::IsPresent, "false")
)]
#[arg(long, short, requires = "short", conflicts_with = "host")]
office: bool,
/// This option restricts the gecos output to only the users' real name.
/// It also has the side-effect of restricting the output of the remote host
/// when used in conjunction with the -H option.
/// when used in conjunction with the -h option.
#[arg(long, short, requires = "short")]
gecos: bool,
@@ -116,84 +106,9 @@ pub struct Args {
/// Output in JSON format
#[arg(long, short)]
json: bool,
/// Generate shell completion scripts for the specified shell
/// and print them to stdout.
#[arg(long, value_enum, hide = true)]
completions: Option<Shell>,
users: Option<Vec<String>>,
}
fn determine_request_info(args: &Args) -> FingerRequestInfo {
let is_long = if args.long {
true
} else if args.short {
false
} else {
args.users.is_some()
};
if is_long {
FingerRequestInfo::Long {
prevent_files: args.prevent_files,
}
} else {
debug_assert!(
!args.host || !args.office,
"Host and office options cannot both be enabled for short output format"
);
if args.host {
FingerRequestInfo::ShortHost {
restrict_gecos: args.gecos,
}
} else {
FingerRequestInfo::ShortOffice {
restrict_gecos: args.gecos,
}
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
if let Some(shell) = args.completions {
generate(shell, &mut Args::command(), "rwho", &mut std::io::stdout());
return Ok(());
}
let mut conn = zlink::unix::connect("/run/roowho2/roowho2.varlink")
.await
.expect("Failed to connect to fingerd server");
let request_info = determine_request_info(&args);
let request_networking = match (args.ipv4, args.ipv6) {
(true, false) => FingerRequestNetworking::IPv4Only,
(false, true) => FingerRequestNetworking::IPv6Only,
_ => FingerRequestNetworking::Any,
};
let reply = conn
.finger(
args.users,
!args.no_name_match,
request_info,
request_networking,
args.no_acct,
)
.await
.context("Failed to send finger request")?
.map_err(|e| anyhow::anyhow!("Server returned an error for finger request: {:?}", e))?;
if args.json {
println!("{}", serde_json::to_string_pretty(&reply).unwrap());
} else {
for user in reply {
println!("{:#?}", user);
}
}
Ok(())
fn main() {
let _args = Args::parse();
unimplemented!()
}
+4 -18
View File
@@ -8,7 +8,6 @@ use std::{
use anyhow::Context;
use clap::Parser;
use tokio::{net::UdpSocket, sync::RwLock};
use tokio_util::sync::CancellationToken;
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
use roowho2_lib::server::{
@@ -50,30 +49,21 @@ async fn main() -> anyhow::Result<()> {
))?,
)?;
let fd_map: HashMap<String, OwnedFd> =
HashMap::from_iter(sd_notify::listen_fds_with_names()?.map(|(fd_num, name)| {
let fd_map: HashMap<String, OwnedFd> = HashMap::from_iter(
sd_notify::listen_fds_with_names(false)?.map(|(fd_num, name)| {
(
name.clone(),
// SAFETY: please don't mess around with file descriptors in random places
// around the codebase lol
unsafe { std::os::fd::OwnedFd::from_raw_fd(fd_num) },
)
}));
}),
);
let mut join_set = tokio::task::JoinSet::new();
let whod_status_store = Arc::new(RwLock::new(HashMap::new()));
let client_server_token = CancellationToken::new();
let client_server_token_ = client_server_token.clone();
tokio::spawn(async move {
client_server_token_.cancelled().await;
tracing::info!("RWHOD client-server is now accepting connections");
#[cfg(feature = "systemd")]
sd_notify::notify(&[sd_notify::NotifyState::Ready]).ok();
Ok::<(), anyhow::Error>(())
});
if config.rwhod.enable {
tracing::info!("Starting RWHOD server");
@@ -99,7 +89,6 @@ async fn main() -> anyhow::Result<()> {
.try_clone()
.context("Failed to clone RWHOD client-server socket fd")?,
whod_status_store.clone(),
client_server_token,
));
join_set.spawn(ctrl_c_handler());
@@ -137,7 +126,6 @@ async fn rwhod_server(
async fn client_server(
socket_fd: OwnedFd,
whod_status_store: RwhodStatusStore,
startup_token: CancellationToken,
) -> anyhow::Result<()> {
// SAFETY: see above
let std_socket =
@@ -146,8 +134,6 @@ async fn client_server(
let zlink_listener = zlink::unix::Listener::try_from(OwnedFd::from(std_socket))?;
let client_server_task = varlink_client_server_task(zlink_listener, whod_status_store);
startup_token.cancel();
client_server_task.await?;
Ok(())
+5 -1
View File
@@ -6,7 +6,11 @@ use clap::Parser;
/// The output shows the current time of day, how long the system has been up, and the load averages.
/// The load average numbers give the number of jobs in the run queue averaged over 1, 5 and 15 minutes.
#[derive(Debug, Parser)]
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
#[command(
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
about,
version,
)]
pub struct Args {
/// The hosts to query.
hosts: Vec<String>,
+6 -2
View File
@@ -3,7 +3,7 @@ use chrono::{Duration, Utc};
use clap::{CommandFactory, Parser};
use clap_complete::{Shell, generate};
use roowho2_lib::{proto::WhodStatusUpdate, server::varlink_api::VarlinkRwhodClientProxy};
use roowho2_lib::{proto::WhodStatusUpdate, server::varlink_api::RwhodClientProxy};
/// Show host status of local machines.
///
@@ -12,7 +12,11 @@ use roowho2_lib::{proto::WhodStatusUpdate, server::varlink_api::VarlinkRwhodClie
///
/// Machines for which no status report has been received for 11 minutes are shown as being down.
#[derive(Debug, Parser)]
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
#[command(
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
about,
version
)]
pub struct Args {
/// Users idle an hour or more are not counted unless the `-a` flag is given.
#[arg(long, short)]
+5 -1
View File
@@ -8,7 +8,11 @@ use clap::Parser;
/// the host-name with the names of the users currently logged on is printed on each line.
/// The `rusers` command will wait for one minute to catch late responders.
#[derive(Debug, Parser)]
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
#[command(
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
about,
version,
)]
pub struct Args {
/// Print all machines responding even if no one is currently logged in
#[arg(long, short)]
+5 -1
View File
@@ -5,7 +5,11 @@ use clap::Parser;
/// The `rwall` command sends a message to the users logged into the specified host.
/// The message to be sent can be typed in and terminated with EOF or it can be in a file
#[derive(Debug, Parser)]
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
#[command(
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
about,
version,
)]
pub struct Args {
/// The host to send the message to
host: String,
+6 -2
View File
@@ -1,7 +1,7 @@
use anyhow::Context;
use clap::{CommandFactory, Parser};
use clap_complete::{Shell, generate};
use roowho2_lib::{proto::WhodUserEntry, server::varlink_api::VarlinkRwhodClientProxy};
use roowho2_lib::{proto::WhodUserEntry, server::varlink_api::RwhodClientProxy};
/// Check who is logged in on local machines.
///
@@ -13,7 +13,11 @@ use roowho2_lib::{proto::WhodUserEntry, server::varlink_api::VarlinkRwhodClientP
/// If a user hasn't typed to the system for an hour or more,
/// then the user will be omitted from the output of `rwho` unless the `-a` flag is given.
#[derive(Debug, Parser)]
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
#[command(
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
about,
version
)]
pub struct Args {
/// Print all machines responding even if no one is currently logged in
#[arg(long, short)]
-4
View File
@@ -1,6 +1,2 @@
#![feature(iter_map_windows)]
#![feature(gethostname)]
#![feature(trim_prefix_suffix)]
pub mod proto;
pub mod server;
+42 -756
View File
@@ -1,769 +1,55 @@
use std::path::PathBuf;
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, TimeDelta, Utc, Weekday};
use itertools::Itertools;
use chrono::{DateTime, Utc};
use nix::libc::uid_t;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FingerRequest {
long: bool,
pub struct FingerPerson {
/// User id
uid: uid_t,
/// User' home directory
dir: PathBuf,
/// Home phone no.
homephone: String,
/// Login name
name: String,
}
impl FingerRequest {
pub fn new(long: bool, name: String) -> Self {
Self { long, name }
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut result = Vec::new();
if self.long {
result.extend(b"/W ");
}
result.extend(self.name.as_bytes());
result.extend(b"\r\n");
result
}
pub fn from_bytes(bytes: &[u8]) -> Self {
let (long, name) = if &bytes[..3] == b"/W " {
(true, &bytes[3..])
} else {
(false, bytes)
};
let name = match name.strip_suffix(b"\r\n") {
Some(new_name) => new_name,
None => name,
};
Self::new(long, String::from_utf8_lossy(name).to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RawFingerResponse(String);
impl RawFingerResponse {
pub fn new(content: String) -> Self {
Self(content)
}
pub fn get_inner(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn from_bytes(bytes: &[u8]) -> Self {
if bytes.is_empty() {
return Self(String::new());
}
fn normalize(c: u8) -> u8 {
if c == (b'\r' | 0x80) || c == (b'\n' | 0x80) {
c & 0x7f
} else {
c
}
}
let normalized: Vec<u8> = bytes
.iter()
.copied()
.map(normalize)
.chain(std::iter::once(normalize(*bytes.last().unwrap())))
.map_windows(|[a, b]| {
if *a == b'\r' && *b == b'\n' {
None
} else {
Some(*a)
}
})
.flatten()
.collect();
let result = String::from_utf8_lossy(&normalized).to_string();
Self(result)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.0.len() + 2);
for &b in self.0.as_bytes() {
if b == b'\n' {
out.extend_from_slice(b"\r\n");
} else {
out.push(b);
}
}
if !self.0.ends_with('\n') {
out.extend_from_slice(b"\r\n");
}
out
}
}
impl From<String> for RawFingerResponse {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&str> for RawFingerResponse {
fn from(s: &str) -> Self {
Self::new(s.to_string())
}
}
/// Parse the time serialization format commonly used by bsd-finger
fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
let time_parts: Vec<_> = time.split_ascii_whitespace().collect();
let time = &time_parts[..time_parts.len() - 1].join(" ");
let _timezone = time_parts[time_parts.len() - 1];
let now = Utc::now();
let mut parts = time.split_whitespace();
let weekday = match parts.next() {
Some("Mon") => Weekday::Mon,
Some("Tue") => Weekday::Tue,
Some("Wed") => Weekday::Wed,
Some("Thu") => Weekday::Thu,
Some("Fri") => Weekday::Fri,
Some("Sat") => Weekday::Sat,
Some("Sun") => Weekday::Sun,
_ => anyhow::bail!("Invalid weekday in login time: {}", time),
};
let month = match parts.next() {
Some("Jan") => 1,
Some("Feb") => 2,
Some("Mar") => 3,
Some("Apr") => 4,
Some("May") => 5,
Some("Jun") => 6,
Some("Jul") => 7,
Some("Aug") => 8,
Some("Sep") => 9,
Some("Oct") => 10,
Some("Nov") => 11,
Some("Dec") => 12,
_ => anyhow::bail!("Invalid month in login time: {}", time),
};
let day: u32 = parts
.next()
.and_then(|d| d.parse().ok())
.ok_or_else(|| anyhow::anyhow!("Invalid day in login time: {}", time))?;
let time_part = parts
.next()
.ok_or_else(|| anyhow::anyhow!("Missing time in login time: {}", time))?;
let clock = NaiveTime::parse_from_str(time_part, "%H:%M").map_err(|e| {
anyhow::anyhow!(
"Failed to parse time component in login time: {}: {}",
time,
e
)
})?;
const MAX_YEARS_BACK: i32 = 10;
for offset in 0..=MAX_YEARS_BACK {
let year = now.year() - offset;
let date = match NaiveDate::from_ymd_opt(year, month, day) {
Some(d) => d,
None => continue,
};
if date.weekday() != weekday {
continue;
}
let dt = date.and_time(clock);
if dt <= now.naive_utc() {
// TODO: apply timezone if we are able to parse it.
// if not, try to get the local timezone offset.
// if not, assume UTC.
return Ok(DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc));
}
}
Err(anyhow::anyhow!(
"Could not infer year for login time {} within {} years",
time,
MAX_YEARS_BACK
))
/// Office name
office: String,
/// Office phone no.
officephone: String,
/// Full name
realname: String,
/// User's shell
shell: String,
/// Last time mail was read
mailread: DateTime<Utc>,
/// Last time mail was received
mailrecv: DateTime<Utc>,
/// List of where the user is or has been
where_: Vec<FingerWhere>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FingerResponseUserEntry {
/// The unix username of this user, as noted in passwd
pub username: String,
/// The full name of this user, as noted in passwd
pub full_name: String,
/// The path to the home directory of this user, as noted in passwd
pub home_dir: PathBuf,
/// The path to the shell of this user, as noted in passwd
pub shell: PathBuf,
/// Office location, if available
pub office: Option<String>,
/// Office phone number, if available
pub office_phone: Option<String>,
/// Home phone number, if available
pub home_phone: Option<String>,
/// Whether the user has never logged in to this host
pub never_logged_in: bool,
/// A list of user sessions, sourced from utmp entries
pub sessions: Vec<FingerResponseUserSession>,
/// Contents of ~/.forward, if it exists
pub forward_status: Option<String>,
/// Whether the user has new or unread mail
pub mail_status: Option<MailStatus>,
/// Contents of ~/.pgpkey, if it exists
pub pgp_key: Option<String>,
/// Contents of ~/.project, if it exists
pub project: Option<String>,
/// Contents of ~/.plan, if it exists
pub plan: Option<String>,
}
impl FingerResponseUserEntry {
#[allow(clippy::too_many_arguments)]
pub fn new(
username: String,
full_name: String,
home_dir: PathBuf,
shell: PathBuf,
office: Option<String>,
office_phone: Option<String>,
home_phone: Option<String>,
never_logged_in: bool,
sessions: Vec<FingerResponseUserSession>,
forward_status: Option<String>,
mail_status: Option<MailStatus>,
pgp_key: Option<String>,
project: Option<String>,
plan: Option<String>,
) -> Self {
debug_assert!(
!never_logged_in || sessions.is_empty(),
"User cannot be marked as never logged in while having active sessions"
);
Self {
username,
full_name,
home_dir,
shell,
office,
office_phone,
home_phone,
never_logged_in,
sessions,
forward_status,
mail_status,
pgp_key,
project,
plan,
}
}
/// Try parsing a [FingerResponseUserEntry] from the text format used by bsd-finger.
pub fn try_from_raw_finger_response(
response: &RawFingerResponse,
username: String,
) -> anyhow::Result<Self> {
let content = response.get_inner();
let lines: Vec<&str> = content.lines().collect();
if lines.len() < 2 {
return Err(anyhow::anyhow!(
"Unexpected finger response format for user {}",
username
));
}
let first_line = lines[0];
let second_line = lines[1];
let full_name = first_line
.split("Name:")
.nth(1)
.ok_or_else(|| {
anyhow::anyhow!(
"Failed to parse full name from finger response for user {}",
username
)
})?
.trim()
.to_string();
let home_dir = second_line
.split("Directory:")
.nth(1)
.and_then(|s| s.split("Shell:").next())
.map(|s| s.trim())
.map(PathBuf::from)
.ok_or_else(|| {
anyhow::anyhow!(
"Failed to parse home directory from finger response for user {}",
username
)
})?;
let shell = second_line
.split("Shell:")
.nth(1)
.map(|s| s.trim())
.map(PathBuf::from)
.ok_or_else(|| {
anyhow::anyhow!(
"Failed to parse shell from finger response for user {}",
username
)
})?;
let mut current_index = 2;
let mut office: Option<String> = None;
let mut office_phone: Option<String> = None;
let mut home_phone: Option<String> = None;
// TODO: handle case where office details contains comma, use last comma as separator
if let Some(line) = lines.get(current_index)
&& line.trim().starts_with("Office:")
{
let office_line = line.trim().trim_start_matches("Office:").trim();
if let Some((office_loc, phone)) = office_line.split_once(',') {
office = Some(office_loc.trim().to_string());
office_phone = Some(phone.trim().to_string());
} else {
office = Some(office_line.to_string());
}
current_index += 1;
}
if let Some(line) = lines.get(current_index)
&& line.trim().starts_with("Office Phone:")
{
let phone = line.trim().trim_start_matches("Office Phone:").trim();
office_phone = Some(phone.to_string());
current_index += 1;
}
if let Some(line) = lines.get(current_index)
&& line.trim().starts_with("Home Phone:")
{
let phone = line.trim().trim_start_matches("Home Phone:").trim();
home_phone = Some(phone.to_string());
current_index += 1;
}
let never_logged_in = lines
.iter()
.skip(current_index)
.take(1)
.any(|&line| line.trim() == "Never logged in.");
let sessions: Vec<_> = lines
.iter()
.skip(current_index)
.take_while(|line| line.starts_with("On since"))
.filter_map(|line| {
match FingerResponseUserSession::try_from_finger_response_line(line) {
Ok(session) => Some(session),
Err(_) => {
tracing::warn!("Failed to parse user session from line: {}", line);
None
}
}
})
.collect();
if never_logged_in {
debug_assert!(
sessions.is_empty(),
"User cannot be marked as never logged in while having active sessions"
);
}
current_index += if never_logged_in { 1 } else { sessions.len() };
let next_line = lines.get(current_index);
// TODO: handle multi-line case
let forward_status = if let Some(line) = next_line
&& line.trim().starts_with("Mail forwarded to ")
{
Some(line.trim().trim_prefix("Mail forwarded to ").to_string())
} else {
None
};
// TODO: parse forward_status, mail_status, plan from remaining lines
Ok(Self::new(
username,
full_name,
home_dir,
shell,
office,
office_phone,
home_phone,
never_logged_in,
sessions,
forward_status,
None,
None,
None,
None,
))
}
pub struct FingerWhere {
/// Type/status of request
status: FingerWhereStatus,
/// Tty is writable
writable: bool,
/// Time of (last) login
loginat: DateTime<Utc>,
/// how long idle (if logged in)
idletime: DateTime<Utc>,
// TODO: are technically limited by UT_LINESIZE and UT_HOSTSIZE
/// Tty line
tty: String,
/// Remote hostname
host: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MailStatus {
NoMail,
NewMailReceived(DateTime<Utc>),
UnreadSince(DateTime<Utc>),
MailLastRead(DateTime<Utc>),
}
impl MailStatus {
pub fn try_from_finger_response_line(str: &str) -> anyhow::Result<Self> {
if str.trim() == "No mail." {
Ok(Self::NoMail)
} else if str.trim().starts_with("New mail received") {
let datetime = parse_bsd_finger_time(str.trim().trim_prefix("New mail received "))?;
Ok(Self::NewMailReceived(datetime))
} else if str.trim().starts_with("Unread since") {
let datetime = parse_bsd_finger_time(str.trim().trim_prefix("Unread since "))?;
Ok(Self::UnreadSince(datetime))
} else if str.trim().starts_with("Mail last read") {
let datetime = parse_bsd_finger_time(str.trim().trim_prefix("Mail last read "))?;
Ok(Self::MailLastRead(datetime))
} else {
anyhow::bail!("")
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FingerResponseUserSession {
/// The tty on which this session exists
pub tty: String,
/// When the user logged in and created this session
pub login_time: DateTime<Utc>,
/// The amount of time since the use last interacted with the tty
pub idle_time: TimeDelta,
/// The hostname of the machine where this session is running
pub host: String,
/// Whether this tty is writable, and thus can receive messages via `mesg(1)`
pub messages_on: bool,
}
impl FingerResponseUserSession {
pub fn new(
tty: String,
login_time: DateTime<Utc>,
idle_time: TimeDelta,
host: String,
messages_on: bool,
) -> Self {
Self {
tty,
login_time,
idle_time,
host,
messages_on,
}
}
/// Parse the idle time from the text string generated by bsd-finger
fn parse_idle_time(str: &str) -> anyhow::Result<Duration> {
// Parse idle time from finger response format.
// This has four cases: " ", "MMMMM", "HH:MM", "DDd"
if str.trim().is_empty() {
Ok(Duration::zero())
} else if str.contains(':') {
let parts: Vec<&str> = str.split(':').collect();
if parts.len() != 2 {
return Err(anyhow::anyhow!("Invalid idle time format: {}", str));
}
let hours: i64 = parts[0].parse().map_err(|e| {
anyhow::anyhow!("Failed to parse hours from idle time {}: {}", str, e)
})?;
let minutes: i64 = parts[1].parse().map_err(|e| {
anyhow::anyhow!("Failed to parse minutes from idle time {}: {}", str, e)
})?;
Ok(Duration::hours(hours) + Duration::minutes(minutes))
} else if str.ends_with('d') {
let days_str = str.trim_end_matches('d');
let days: i64 = days_str.parse().map_err(|e| {
anyhow::anyhow!("Failed to parse days from idle time {}: {}", str, e)
})?;
Ok(Duration::days(days))
} else {
let minutes: i64 = str.parse().map_err(|e| {
anyhow::anyhow!("Failed to parse minutes from idle time {}: {}", str, e)
})?;
Ok(Duration::minutes(minutes))
}
}
/// Try parsing a [FingerResponseUserSession] from the text format used by bsd-finger.
pub fn try_from_finger_response_line(line: &str) -> anyhow::Result<Self> {
let parts: Vec<&str> = line.split_whitespace().collect();
debug_assert!(parts[0] == "On");
debug_assert!(parts[1] == "since");
let login_time_str = parts
.iter()
.take_while(|&&s| s != "on")
.skip(2)
.cloned()
.join(" ");
let login_time = parse_bsd_finger_time(&login_time_str)?;
let tty = parts
.iter()
.skip_while(|&&s| s != "on")
.nth(1)
.ok_or_else(|| anyhow::anyhow!("Failed to find tty in finger session line: {line}"))?
.trim_end_matches(',')
.to_string();
let idle_time_str = parts
.iter()
.skip_while(|&&s| s != "idle")
.nth(1)
.ok_or_else(|| {
anyhow::anyhow!("Failed to find idle time in finger session line: {line}")
})?
.trim_end_matches(',');
let idle_time = Self::parse_idle_time(idle_time_str)?;
let host = parts
.iter()
.skip_while(|&&s| s != "from")
.nth(1)
.unwrap_or(&"")
.to_string();
let messages_on = !line.ends_with("(messages off)");
Ok(Self::new(tty, login_time, idle_time, host, messages_on))
}
}
#[cfg(test)]
mod tests {
use chrono::Timelike;
use super::*;
#[test]
fn test_finger_raw_serialization_roundrip() {
let request = FingerRequest::new(true, "alice".to_string());
let bytes = request.to_bytes();
let deserialized = FingerRequest::from_bytes(&bytes);
assert_eq!(request, deserialized);
let request2 = FingerRequest::new(false, "bob".to_string());
let bytes2 = request2.to_bytes();
let deserialized2 = FingerRequest::from_bytes(&bytes2);
assert_eq!(request2, deserialized2);
let response = RawFingerResponse::new("Hello, World!\nThis is a test.\n".to_string());
let response_bytes = response.to_bytes();
let deserialized_response = RawFingerResponse::from_bytes(&response_bytes);
assert_eq!(response, deserialized_response);
let response2 = RawFingerResponse::new("Single line response\n".to_string());
let response_bytes2 = response2.to_bytes();
let deserialized_response2 = RawFingerResponse::from_bytes(&response_bytes2);
assert_eq!(response2, deserialized_response2);
}
#[test]
fn test_parse_bsd_finger_time() {
let cases = vec![
"Mon Mar 1 10:00 (UTC)",
"Tue Feb 28 23:59 (UTC)",
"Wed Dec 31 00:00 (UTC)",
"Wed Dec 31 00:00 (GMT)",
];
for input in cases {
let datetime = parse_bsd_finger_time(input);
assert!(
datetime.is_ok(),
"Failed to parse datetime for input: {}",
input
);
}
}
#[test]
fn test_parse_idle_time() {
let cases = vec![(" ", 0), ("5", 5), ("1:30", 90), ("2d", 2880)];
for (input, expected_minutes) in cases {
let duration = FingerResponseUserSession::parse_idle_time(input).unwrap();
assert_eq!(
duration.num_minutes(),
expected_minutes,
"Failed on input: {}",
input
);
}
}
#[test]
fn test_finger_user_session_parsing() {
let line = "On since Mon Mar 1 10:00 (UTC) on pts/0, idle 5:00, from host.example.com";
let session = FingerResponseUserSession::try_from_finger_response_line(line).unwrap();
assert_eq!(session.tty, "pts/0");
assert_eq!(session.host, "host.example.com");
assert_eq!(session.login_time.weekday(), Weekday::Mon);
assert_eq!(session.login_time.hour(), 10);
assert_eq!(session.idle_time.num_minutes(), 300);
assert!(session.messages_on);
let line_off = "On since Mon Mar 1 10:00 (UTC) on pts/1, idle 10, from another.host.com (messages off)";
let session_off =
FingerResponseUserSession::try_from_finger_response_line(line_off).unwrap();
assert_eq!(session_off.tty, "pts/1");
assert_eq!(session_off.host, "another.host.com");
assert_eq!(session_off.login_time.weekday(), Weekday::Mon);
assert_eq!(session_off.login_time.hour(), 10);
assert_eq!(session_off.idle_time.num_minutes(), 10);
assert!(!session_off.messages_on);
}
#[test]
fn test_finger_user_entry_parsing_basic() {
let response_content = indoc::indoc! {"
Login: alice Name: Alice Wonderland
Directory: /home/alice Shell: /bin/bash
On since Mon Mar 1 10:00 (UTC) on pts/0, idle 5:00, from host.example.com
No Mail.
No Plan.
"}
.trim();
let response = RawFingerResponse::from(response_content.to_string());
let user_entry =
FingerResponseUserEntry::try_from_raw_finger_response(&response, "alice".to_string())
.unwrap();
assert_eq!(user_entry.username, "alice");
assert_eq!(user_entry.full_name, "Alice Wonderland");
assert_eq!(user_entry.home_dir, PathBuf::from("/home/alice"));
assert_eq!(user_entry.shell, PathBuf::from("/bin/bash"));
assert_eq!(user_entry.sessions.len(), 1);
assert_eq!(user_entry.sessions[0].tty, "pts/0");
assert_eq!(user_entry.sessions[0].host, "host.example.com");
}
#[test]
fn test_finger_user_entry_parsing_single_line_office_phone() {
let response_content = indoc::indoc! {"
Login: alice Name: Alice Wonderland
Directory: /home/alice Shell: /bin/bash
Office: 123 Main St, 012-345-6789
Home Phone: +0-123-456-7890
On since Mon Mar 1 10:00 (UTC) on pts/0, idle 5:00, from host.example.com
No Mail.
No Plan.
"}
.trim();
let response = RawFingerResponse::from(response_content.to_string());
let user_entry =
FingerResponseUserEntry::try_from_raw_finger_response(&response, "alice".to_string())
.unwrap();
assert_eq!(user_entry.office, Some("123 Main St".to_string()));
assert_eq!(user_entry.office_phone, Some("012-345-6789".to_string()));
assert_eq!(user_entry.home_phone, Some("+0-123-456-7890".to_string()));
}
#[test]
fn test_finger_user_entry_parsing_multiline_office_phone() {
let response_content = indoc::indoc! {"
Login: alice Name: Alice Wonderland
Directory: /home/alice Shell: /bin/bash
Office: 123 Main St
Office Phone: 012-345-6789
Home Phone: +0-123-456-7890
On since Mon Mar 1 10:00 (UTC) on pts/0, idle 5:00, from host.example.com
No Mail.
No Plan.
"}
.trim();
let response = RawFingerResponse::from(response_content.to_string());
let user_entry =
FingerResponseUserEntry::try_from_raw_finger_response(&response, "alice".to_string())
.unwrap();
assert_eq!(user_entry.office, Some("123 Main St".to_string()));
assert_eq!(user_entry.office_phone, Some("012-345-6789".to_string()));
assert_eq!(user_entry.home_phone, Some("+0-123-456-7890".to_string()));
}
#[test]
fn test_finger_user_entry_parsing_never_logged_in() {
let response_content = indoc::indoc! {"
Login: bob Name: Bob Builder
Directory: /home/bob Shell: /bin/zsh
Never logged in.
No Mail.
No Plan.
"}
.trim();
let response = RawFingerResponse::from(response_content.to_string());
let user_entry =
FingerResponseUserEntry::try_from_raw_finger_response(&response, "bob".to_string())
.unwrap();
assert!(user_entry.never_logged_in);
assert!(user_entry.sessions.is_empty());
}
#[repr(C)]
pub enum FingerWhereStatus {
LastLog,
LoggedIn,
}
-1
View File
@@ -1,4 +1,3 @@
pub mod config;
pub mod fingerd;
pub mod rwhod;
pub mod varlink_api;
-252
View File
@@ -1,252 +0,0 @@
use std::{
net::hostname,
os::unix::fs::{MetadataExt, PermissionsExt},
path::Path,
};
use chrono::{DateTime, Duration, Timelike, Utc};
use nix::sys::stat::stat;
use serde::{Deserialize, Serialize};
use users::all_users;
use uucore::utmpx::Utmpx;
use crate::proto::finger_protocol::{FingerResponseUserEntry, FingerResponseUserSession};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FingerRequestNetworking {
Any,
IPv4Only,
IPv6Only,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FingerRequestInfo {
ShortHost { restrict_gecos: bool },
ShortOffice { restrict_gecos: 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) {
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()
}
/// 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) -> anyhow::Result<Option<FingerResponseUserEntry>> {
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 mut utmpx_records = Utmpx::iter_all_records()
.filter(|entry| entry.user() == username)
.filter(|entry| entry.is_user_process())
.peekable();
// TODO: Don't use utmp entries for this, read from lastlog instead
let user_never_logged_in = utmpx_records.peek().is_none();
let now = Utc::now().with_nanosecond(0).unwrap_or(Utc::now());
let sessions: Vec<FingerResponseUserSession> = utmpx_records
.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").unwrap().unwrap();
assert_eq!(user_entry.username, "root");
}
// TODO: test serialization roundtrip
}
-19
View File
@@ -240,22 +240,3 @@ pub async fn rwhod_packet_sender_task(
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_determine_relevant_interfaces() {
let interfaces = determine_relevant_interfaces().unwrap();
for interface in interfaces {
println!("Interface: {} Address: {}", interface.name, interface.addr);
}
}
#[test]
fn test_generate_rwhod_status_update() {
let status_update = generate_rwhod_status_update().unwrap();
println!("{:?}", status_update);
}
}
+53 -150
View File
@@ -1,34 +1,27 @@
use std::os::fd::OwnedFd;
use anyhow::Context;
use serde::{Deserialize, Serialize};
use zlink::{ReplyError, service::MethodReply};
use crate::{
proto::{WhodStatusUpdate, WhodUserEntry, finger_protocol::FingerResponseUserEntry},
server::{
fingerd::{self, FingerRequestInfo, FingerRequestNetworking},
rwhod::RwhodStatusStore,
},
proto::{WhodStatusUpdate, WhodUserEntry, finger_protocol::FingerPerson},
server::rwhod::RwhodStatusStore,
};
// Types for 'no.ntnu.pvv.roowho2.rwhod'
#[zlink::proxy("no.ntnu.pvv.roowho2.rwhod")]
pub trait VarlinkRwhodClientProxy {
pub trait RwhodClientProxy {
async fn rwho(
&mut self,
all: bool,
) -> zlink::Result<Result<VarlinkRwhoResponse, VarlinkRwhodClientError>>;
) -> zlink::Result<Result<Vec<(String, WhodUserEntry)>, RwhodClientError>>;
async fn ruptime(
&mut self,
) -> zlink::Result<Result<VarlinkRuptimeResponse, VarlinkRwhodClientError>>;
async fn ruptime(&mut self) -> zlink::Result<Result<Vec<WhodStatusUpdate>, RwhodClientError>>;
}
#[derive(Debug, Deserialize)]
#[serde(tag = "method", content = "parameters")]
pub enum VarlinkRwhodClientRequest {
pub enum RwhodClientRequest {
#[serde(rename = "no.ntnu.pvv.roowho2.rwhod.Rwho")]
Rwho {
/// Retrieve all users, even those that have been idle for a long time.
@@ -39,60 +32,50 @@ pub enum VarlinkRwhodClientRequest {
Ruptime,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum VarlinkRwhodClientResponse {
Rwho(VarlinkRwhoResponse),
Ruptime(VarlinkRuptimeResponse),
pub enum RwhodClientResponse {
Rwho(RwhoResponse),
Ruptime(RuptimeResponse),
}
pub type VarlinkRwhoResponse = Vec<(String, WhodUserEntry)>;
pub type VarlinkRuptimeResponse = Vec<WhodStatusUpdate>;
pub type RwhoResponse = Vec<(String, WhodUserEntry)>;
pub type RuptimeResponse = Vec<WhodStatusUpdate>;
#[derive(Debug, Clone, PartialEq, ReplyError)]
#[zlink(interface = "no.ntnu.pvv.roowho2.rwhod")]
pub enum VarlinkRwhodClientError {
pub enum RwhodClientError {
InvalidRequest,
}
// Types for 'no.ntnu.pvv.roowho2.finger'
#[zlink::proxy("no.ntnu.pvv.roowho2.finger")]
pub trait VarlinkFingerClientProxy {
pub trait FingerClientProxy {
async fn finger(
&mut self,
user_queries: Option<Vec<String>>,
match_fullnames: bool,
request_info: FingerRequestInfo,
request_networking: FingerRequestNetworking,
disable_user_account_db: bool,
) -> zlink::Result<Result<VarlinkFingerResponse, VarlinkFingerClientError>>;
user_queries: Vec<String>,
) -> zlink::Result<Result<Vec<FingerPerson>, FingerClientError>>;
}
#[derive(Debug, Deserialize)]
#[serde(tag = "method", content = "parameters")]
pub enum VarlinkFingerClientRequest {
pub enum FingerClientRequest {
#[serde(rename = "no.ntnu.pvv.roowho2.finger.Finger")]
Finger {
user_queries: Option<Vec<String>>,
match_fullnames: bool,
request_info: FingerRequestInfo,
request_networking: FingerRequestNetworking,
disable_user_account_db: bool,
},
Finger { user_queries: Vec<String> },
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum VarlinkFingerClientResponse {
Finger(VarlinkFingerResponse),
pub enum FingerClientResponse {
Finger(FingerResponse),
}
pub type VarlinkFingerResponse = Vec<FingerResponseUserEntry>;
pub type FingerResponse = Vec<FingerPerson>;
#[derive(Debug, Clone, PartialEq, ReplyError)]
#[zlink(interface = "no.ntnu.pvv.roowho2.finger")]
pub enum VarlinkFingerClientError {
pub enum FingerClientError {
InvalidRequest,
}
@@ -101,42 +84,41 @@ pub enum VarlinkFingerClientError {
#[derive(Debug, Deserialize)]
#[serde(untagged)]
#[allow(unused)]
pub enum VarlinkMethod {
Rwhod(VarlinkRwhodClientRequest),
Finger(VarlinkFingerClientRequest),
enum Method {
Rwhod(RwhodClientRequest),
Finger(FingerClientRequest),
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
#[allow(unused)]
pub enum VarlinkReply {
Rwhod(VarlinkRwhodClientResponse),
Finger(VarlinkFingerClientResponse),
enum Reply {
Rwhod(RwhodClientResponse),
Finger(FingerClientResponse),
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(untagged)]
#[allow(unused)]
pub enum VarlinkReplyError {
Rwhod(VarlinkRwhodClientError),
Finger(VarlinkFingerClientError),
enum ReplyError {
Rwhod(RwhodClientError),
Finger(FingerClientError),
}
#[derive(Debug, Clone)]
pub struct VarlinkRoowhoo2ClientServer {
pub struct Roowhoo2ClientServer {
whod_status_store: RwhodStatusStore,
}
impl VarlinkRoowhoo2ClientServer {
impl Roowhoo2ClientServer {
pub fn new(whod_status_store: RwhodStatusStore) -> Self {
Self { whod_status_store }
}
}
impl VarlinkRoowhoo2ClientServer {
impl Roowhoo2ClientServer {
// TODO: handle 'all' parameter
async fn handle_rwho_request(&self, _all: bool) -> VarlinkRwhoResponse {
tracing::debug!(all = _all, "Handling Rwho request");
async fn handle_rwho_request(&self, _all: bool) -> RwhoResponse {
let store = self.whod_status_store.read().await;
let mut all_user_entries = Vec::with_capacity(store.len());
@@ -153,110 +135,31 @@ impl VarlinkRoowhoo2ClientServer {
all_user_entries
}
async fn handle_ruptime_request(&self) -> VarlinkRuptimeResponse {
tracing::debug!("Handling Ruptime request");
async fn handle_ruptime_request(&self) -> RuptimeResponse {
let store = self.whod_status_store.read().await;
store.values().cloned().collect()
}
async fn handle_finger_request(
&self,
user_queries: Option<Vec<String>>,
match_fullnames: bool,
request_info: FingerRequestInfo,
_request_networking: FingerRequestNetworking,
_disable_user_account_db: bool,
) -> VarlinkFingerResponse {
tracing::debug!(
user_queries = ?user_queries,
match_fullnames = match_fullnames,
request_info = ?request_info,
"Handling Finger request",
);
match user_queries {
Some(usernames) => usernames
.into_iter()
.flat_map::<Vec<_>, _>(|username| {
fingerd::search_for_user(&username, match_fullnames, &request_info)
.into_iter()
.map(|res| (username.clone(), res))
.collect()
})
.dedup_by(|a, b| match (&a.1, &b.1) {
(Ok(user_a), Ok(user_b)) => user_a.username == user_b.username,
_ => false,
})
.filter_map(|(username, user)| match user {
Ok(user_info) => Some(user_info),
Err(err) => {
tracing::error!(
"Error retrieving local user information for '{}': {}",
username,
err
);
None
}
})
.collect(),
None => {
// TODO: fetch logged in users using utmp entries
todo!()
}
}
}
}
impl zlink::Service<zlink::unix::Stream> for VarlinkRoowhoo2ClientServer {
type MethodCall<'de> = VarlinkMethod;
type ReplyParams<'se> = VarlinkReply;
impl zlink::Service for Roowhoo2ClientServer {
type MethodCall<'de> = RwhodClientRequest;
type ReplyParams<'se> = RwhodClientResponse;
type ReplyStreamParams = ();
type ReplyStream = futures_util::stream::Empty<(zlink::Reply<()>, Vec<OwnedFd>)>;
type ReplyError<'se> = VarlinkReplyError;
type ReplyStream = futures_util::stream::Empty<zlink::Reply<()>>;
type ReplyError<'se> = RwhodClientError;
async fn handle<'service>(
&'service mut self,
call: &'service zlink::Call<Self::MethodCall<'_>>,
_conn: &mut zlink::Connection<zlink::unix::Stream>,
_fds: Vec<std::os::fd::OwnedFd>,
) -> zlink::service::HandleResult<
Self::ReplyParams<'service>,
Self::ReplyStream,
Self::ReplyError<'service>,
> {
async fn handle<'ser, 'de: 'ser, Sock: zlink::connection::Socket>(
&'ser mut self,
call: zlink::Call<Self::MethodCall<'de>>,
_conn: &mut zlink::Connection<Sock>,
) -> MethodReply<Self::ReplyParams<'ser>, Self::ReplyStream, Self::ReplyError<'ser>> {
match call.method() {
VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Rwho { all }) => (
MethodReply::Single(Some(VarlinkReply::Rwhod(VarlinkRwhodClientResponse::Rwho(
self.handle_rwho_request(*all).await,
)))),
Default::default(),
),
VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Ruptime) => (
MethodReply::Single(Some(VarlinkReply::Rwhod(
VarlinkRwhodClientResponse::Ruptime(self.handle_ruptime_request().await),
))),
Default::default(),
),
VarlinkMethod::Finger(VarlinkFingerClientRequest::Finger {
user_queries,
match_fullnames,
request_info,
request_networking,
disable_user_account_db,
}) => (
MethodReply::Single(Some(VarlinkReply::Finger(
VarlinkFingerClientResponse::Finger(
self.handle_finger_request(
user_queries.clone(),
*match_fullnames,
request_info.clone(),
request_networking.clone(),
*disable_user_account_db,
)
.await,
),
))),
Default::default(),
),
RwhodClientRequest::Rwho { all } => MethodReply::Single(Some(
RwhodClientResponse::Rwho(self.handle_rwho_request(*all).await),
)),
RwhodClientRequest::Ruptime => MethodReply::Single(Some(RwhodClientResponse::Ruptime(
self.handle_ruptime_request().await,
))),
}
}
}
@@ -265,7 +168,7 @@ pub async fn varlink_client_server_task(
socket: zlink::unix::Listener,
whod_status_store: RwhodStatusStore,
) -> anyhow::Result<()> {
let service = VarlinkRoowhoo2ClientServer::new(whod_status_store);
let service = Roowhoo2ClientServer::new(whod_status_store);
let server = zlink::Server::new(socket, service);