diff options
author | RaindropsSys <raindrops@equestria.dev> | 2024-05-19 18:39:18 +0200 |
---|---|---|
committer | RaindropsSys <raindrops@equestria.dev> | 2024-05-19 18:39:18 +0200 |
commit | 520b89511a12de95af9b6c7a65f0fbcf47b92d09 (patch) | |
tree | df331a2b13e44149f4d9ef924ac9d109e5aafdf7 /whrd | |
parent | dfa0895f994972c6218f62acd3762445b321fcb0 (diff) | |
download | where-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.toml | 13 | ||||
-rw-r--r-- | whrd/src/error.rs | 91 | ||||
-rw-r--r-- | whrd/src/lib.rs | 208 | ||||
-rw-r--r-- | whrd/src/parse.rs | 43 |
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) +} |