summaryrefslogtreecommitdiff
path: root/whrd
diff options
context:
space:
mode:
authorRaindropsSys <raindrops@equestria.dev>2024-05-19 18:39:18 +0200
committerRaindropsSys <raindrops@equestria.dev>2024-05-19 18:39:18 +0200
commit520b89511a12de95af9b6c7a65f0fbcf47b92d09 (patch)
treedf331a2b13e44149f4d9ef924ac9d109e5aafdf7 /whrd
parentdfa0895f994972c6218f62acd3762445b321fcb0 (diff)
downloadwhere-rs-520b89511a12de95af9b6c7a65f0fbcf47b92d09.tar.gz
where-rs-520b89511a12de95af9b6c7a65f0fbcf47b92d09.tar.bz2
where-rs-520b89511a12de95af9b6c7a65f0fbcf47b92d09.zip
Updated 10 files, added 2 files, deleted where-shared/Cargo.toml and renamed 3 files
Diffstat (limited to 'whrd')
-rw-r--r--whrd/Cargo.toml13
-rw-r--r--whrd/src/error.rs91
-rw-r--r--whrd/src/lib.rs208
-rw-r--r--whrd/src/parse.rs43
4 files changed, 355 insertions, 0 deletions
diff --git a/whrd/Cargo.toml b/whrd/Cargo.toml
new file mode 100644
index 0000000..adb0635
--- /dev/null
+++ b/whrd/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "whrd"
+version = "1.0.0-rc.1"
+edition = "2021"
+description = "A Rust library to work with the WHRD/UDP protocol, a protocol to access a list of logged in users on multiple systems at once."
+authors = ["Raindrops", "ryze132"]
+
+[lib]
+name = "whrd"
+crate-type = ["dylib", "lib"]
+
+[dependencies]
+coreutils_core = "0.1.1"
diff --git a/whrd/src/error.rs b/whrd/src/error.rs
new file mode 100644
index 0000000..931aefe
--- /dev/null
+++ b/whrd/src/error.rs
@@ -0,0 +1,91 @@
+use std::fmt::Display;
+use std::string::FromUtf8Error;
+use std::{fmt, io};
+use std::net::AddrParseError;
+use std::time::Duration;
+use crate::{MAX_ENTRY_LENGTH, MAX_PAYLOAD_LENGTH};
+
+pub enum WhereError {
+ EncodeDecodeError(EncodeDecodeError),
+ IOError(io::Error),
+ TimedOut(String, String, usize, Duration),
+ CannotParseAddress(AddrParseError)
+}
+
+pub enum EncodeDecodeError {
+ InvalidEntryLength(usize),
+ InvalidPayloadLength(usize),
+ BadMagic([u8; 4]),
+ IncorrectEntryCount,
+ StringSizeLimitExceeded(u32, usize),
+ StringDecodeError(FromUtf8Error),
+ NonbinaryBoolean,
+ EmptyRemote,
+ IOErrorWhileTranscoding(io::Error)
+}
+
+pub type WhereResult<T> = Result<T, WhereError>;
+pub type EncodeDecodeResult<T> = Result<T, EncodeDecodeError>;
+
+impl From<io::Error> for WhereError {
+ fn from(value: io::Error) -> Self {
+ Self::IOError(value)
+ }
+}
+
+impl From<EncodeDecodeError> for WhereError {
+ fn from(value: EncodeDecodeError) -> Self {
+ Self::EncodeDecodeError(value)
+ }
+}
+
+impl From<FromUtf8Error> for WhereError {
+ fn from(value: FromUtf8Error) -> Self {
+ Self::EncodeDecodeError(EncodeDecodeError::StringDecodeError(value))
+ }
+}
+
+impl From<io::Error> for EncodeDecodeError {
+ fn from(value: io::Error) -> Self {
+ Self::IOErrorWhileTranscoding(value)
+ }
+}
+
+impl From<FromUtf8Error> for EncodeDecodeError {
+ fn from (value: FromUtf8Error) -> Self {
+ Self::StringDecodeError(value)
+ }
+}
+
+impl From<AddrParseError> for WhereError {
+ fn from (value: AddrParseError) -> Self {
+ Self::CannotParseAddress(value)
+ }
+}
+
+impl Display for EncodeDecodeError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InvalidEntryLength(s) => write!(f, "Invalid entry length: {s} but maximum is {MAX_ENTRY_LENGTH}"),
+ Self::InvalidPayloadLength(s) => write!(f, "Invalid full payload length: {s} but maximum is {MAX_PAYLOAD_LENGTH}"),
+ Self::BadMagic(m) => write!(f, "Invalid packet magic ({}), possible corruption or invalid server", String::from_utf8_lossy(m)),
+ Self::IncorrectEntryCount => write!(f, "Invalid amount of entries decoded"),
+ Self::StringDecodeError(e) => write!(f, "String decoding error: {e}"),
+ Self::StringSizeLimitExceeded(curr, max) => write!(f, "Exceeded length limit for payload string ({curr} > {max})"),
+ Self::NonbinaryBoolean => write!(f, "Boolean value is not 0 or 1"),
+ Self::EmptyRemote => write!(f, "Remote tag set but no remote host is present"),
+ Self::IOErrorWhileTranscoding(e) => write!(f, "Input/output error while encoding/decoding: {e}"),
+ }
+ }
+}
+
+impl Display for WhereError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::EncodeDecodeError(e) => write!(f, "Encode/decode error: {e}"),
+ Self::IOError(e) => write!(f, "Input/output error: {e}"),
+ Self::TimedOut(server, address, max_retry, timeout) => write!(f, "Timed out waiting for data from {server} ({address}) after {max_retry} attempts every {} ms", timeout.as_millis()),
+ Self::CannotParseAddress(e) => write!(f, "Unable to parse server address: {e}")
+ }
+ }
+}
diff --git a/whrd/src/lib.rs b/whrd/src/lib.rs
new file mode 100644
index 0000000..8ff483d
--- /dev/null
+++ b/whrd/src/lib.rs
@@ -0,0 +1,208 @@
+use std::io::Cursor;
+use coreutils_core::os::utmpx::*;
+
+use crate::error::{WhereResult, EncodeDecodeResult, EncodeDecodeError};
+
+mod parse;
+pub mod error;
+
+pub const WHERED_MAGIC: [u8; 4] = *b"WHRD";
+pub const MAX_USER_TTY_LENGTH: usize = 32;
+pub const MAX_REMOTE_LENGTH: usize = 64;
+pub const MAX_ENTRY_LENGTH: usize = MAX_REMOTE_LENGTH + MAX_USER_TTY_LENGTH * 2 + 25;
+pub const MAX_PAYLOAD_LENGTH: usize = 65501;
+pub const MAX_PAYLOAD_ENTRIES: usize = MAX_PAYLOAD_LENGTH / MAX_ENTRY_LENGTH;
+
+type Payload = [u8; MAX_PAYLOAD_LENGTH];
+type PayloadCursor = Cursor<Payload>;
+
+#[derive(Debug)]
+pub struct Session {
+ pub host: Option<String>,
+ pub pid: i32,
+ pub login_time: i64,
+ pub user: String,
+ pub tty: String,
+ pub remote: Option<String>,
+ pub active: bool,
+}
+
+#[derive(Debug)]
+pub struct SessionCollection {
+ inner: Vec<Session>
+}
+
+impl SessionCollection {
+ pub fn fetch() -> Self {
+ let inner: Vec<Session> = UtmpxSet::system()
+ .into_iter()
+ .filter(|utmpx| utmpx.entry_type() == UtmpxKind::UserProcess || utmpx.entry_type() == UtmpxKind::DeadProcess)
+ .map(Session::from)
+ .collect();
+
+ Self {
+ inner
+ }
+ }
+
+ pub fn get_empty() -> Self {
+ Self {
+ inner: vec![]
+ }
+ }
+
+ pub fn into_vec(self) -> Vec<Session> {
+ self.inner
+ }
+
+ pub fn to_udp_payload(self) -> EncodeDecodeResult<Vec<u8>> {
+ println!("Encoding payload with {} entries", self.inner.len());
+
+ let mut bytes: Vec<u8> = vec![];
+ bytes.extend(&WHERED_MAGIC);
+
+ let entry_count = (self.inner.len() as u16).to_be_bytes();
+ bytes.extend(&entry_count);
+
+ for item in self.inner {
+ let entry = item.to_udp_payload();
+
+ if entry.len() > MAX_ENTRY_LENGTH {
+ return Err(EncodeDecodeError::InvalidEntryLength(entry.len()));
+ }
+
+ bytes.extend(entry);
+ }
+
+ if bytes.len() > MAX_PAYLOAD_LENGTH {
+ Err(EncodeDecodeError::InvalidPayloadLength(bytes.len()))
+ } else {
+ Ok(bytes)
+ }
+ }
+
+ pub fn from_udp_payload(buffer: Payload, host: &str) -> WhereResult<Self> {
+ let mut cursor = Cursor::new(buffer);
+ let mut inner = vec![];
+
+ // Check magic
+ parse::read_field(&mut cursor, |buf| {
+ if buf != WHERED_MAGIC {
+ Err(EncodeDecodeError::BadMagic(buf))?
+ } else {
+ Ok(())
+ }
+ })?;
+
+ let entry_count = parse::read_field(&mut cursor, |buf| Ok(u16::from_be_bytes(buf)))?;
+
+ for _ in 0..entry_count {
+ inner.push(Session::from_udp_payload(&mut cursor, host)?);
+ }
+
+ Ok(Self {
+ inner
+ })
+ }
+}
+
+impl Session {
+ pub fn from_udp_payload(cursor: &mut PayloadCursor, host: &str) -> WhereResult<Self> {
+ let pid = parse::read_field(cursor, |buf| Ok(i32::from_be_bytes(buf)))?;
+ let login_time = parse::read_field(cursor, |buf| Ok(i64::from_be_bytes(buf)))?;
+ let user = parse::read_string_field(cursor, MAX_USER_TTY_LENGTH as u32)?;
+ let tty = parse::read_string_field(cursor, MAX_USER_TTY_LENGTH as u32)?;
+
+ let remote = {
+ let has_remote_tag = parse::read_bool_field(cursor)?;
+ if has_remote_tag {
+ Some(parse::read_string_field(cursor, MAX_REMOTE_LENGTH as u32)?)
+ } else {
+ None
+ }
+ };
+
+ let active = parse::read_bool_field(cursor)?;
+
+ let host = Some(host.to_string());
+
+ Ok(Self {
+ host,
+ pid,
+ login_time,
+ user,
+ tty,
+ remote,
+ active,
+ })
+ }
+
+ pub fn to_udp_payload(self) -> Vec<u8> {
+ let mut bytes: Vec<u8> = vec![];
+
+ let pid = self.pid.to_be_bytes();
+ let login_time = self.login_time.to_be_bytes();
+ let user_length = (self.user.len() as u32).to_be_bytes();
+ let user = self.user.as_bytes();
+ let tty_length = (self.tty.len() as u32).to_be_bytes();
+ let tty = self.tty.as_bytes();
+ let active = self.active as u8;
+
+ bytes.extend(&pid);
+ bytes.extend(&login_time);
+ bytes.extend(&user_length);
+ bytes.extend(user);
+ bytes.extend(&tty_length);
+ bytes.extend(tty);
+
+ match self.remote {
+ None => bytes.push(0u8),
+ Some(host) => {
+ let host_bytes = host.into_bytes();
+ let host_length = (host_bytes.len() as u32).to_be_bytes();
+
+ bytes.push(1u8);
+ bytes.extend(&host_length);
+ bytes.extend(&host_bytes);
+ }
+ }
+
+ bytes.push(active);
+
+ bytes
+ }
+}
+
+impl From<Utmpx> for Session {
+ fn from(utmpx: Utmpx) -> Self {
+ // BStr doesn't have a known size at compile time, so we can't use it instead of String
+ let mut host = utmpx.host().to_string();
+ host.truncate(MAX_REMOTE_LENGTH);
+
+ let mut user = utmpx.user().to_string();
+ user.truncate(MAX_USER_TTY_LENGTH);
+
+ let pid = utmpx.process_id();
+ // In the case of a user session, this will always be a TTY
+ let mut tty = utmpx.device_name().to_string();
+ tty.truncate(MAX_USER_TTY_LENGTH);
+
+ let remote = if host.is_empty() {
+ None
+ } else {
+ Some(host)
+ };
+ let active = utmpx.entry_type() == UtmpxKind::UserProcess;
+ let login_time = utmpx.timeval().tv_sec;
+
+ Self {
+ host: None,
+ user,
+ pid,
+ tty,
+ remote,
+ active,
+ login_time
+ }
+ }
+}
diff --git a/whrd/src/parse.rs b/whrd/src/parse.rs
new file mode 100644
index 0000000..83542d8
--- /dev/null
+++ b/whrd/src/parse.rs
@@ -0,0 +1,43 @@
+use std::io::Read;
+
+use crate::error::{EncodeDecodeError, WhereError, WhereResult};
+use crate::PayloadCursor;
+
+pub fn read_field<const N: usize, F, T>(cursor: &mut PayloadCursor, convert_func: F) -> WhereResult<T>
+where
+ F: Fn([u8; N]) -> WhereResult<T>
+{
+ let mut buffer = [0u8; N];
+ cursor.read_exact(&mut buffer)?;
+
+ let value = convert_func(buffer)?;
+ Ok(value)
+}
+
+pub fn read_field_dynamic<F, T>(cursor: &mut PayloadCursor, size: usize, convert_func: F) -> WhereResult<T>
+where
+ F: Fn(Vec<u8>) -> WhereResult<T>
+{
+ let mut buffer = vec![0u8; size];
+ cursor.read_exact(&mut buffer)?;
+
+ let value = convert_func(buffer)?;
+ Ok(value)
+}
+
+pub fn read_bool_field(cursor: &mut PayloadCursor) -> WhereResult<bool> {
+ let value = read_field::<1, _, _>(cursor, |buf| Ok(buf[0] == 1))?;
+ Ok(value)
+}
+
+pub fn read_string_field(cursor: &mut PayloadCursor, max_length: u32) -> WhereResult<String> {
+ let string_length = read_field(cursor, |buf| Ok(u32::from_be_bytes(buf)))?;
+
+ if string_length > max_length {
+ return Err(WhereError::EncodeDecodeError(EncodeDecodeError::StringSizeLimitExceeded(string_length, max_length as usize)));
+ }
+
+ let string = read_field_dynamic(cursor, string_length as usize, |buf| Ok(String::from_utf8(buf)?))?;
+
+ Ok(string)
+}