diff --git a/Cargo.lock b/Cargo.lock index 4a5bffc9..5db53a16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,7 +1288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.60.2", @@ -1426,9 +1426,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -1485,6 +1485,7 @@ dependencies = [ "uu_setpgid", "uu_setsid", "uu_uuidgen", + "uu_wall", "uucore 0.2.2", "uuid", "uutests", @@ -1701,6 +1702,17 @@ dependencies = [ "windows", ] +[[package]] +name = "uu_wall" +version = "0.0.1" +dependencies = [ + "chrono", + "clap", + "libc", + "unicode-width", + "uucore 0.2.2", +] + [[package]] name = "uucore" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 56d2898f..d74582c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ feat_common_core = [ "setpgid", "setsid", "uuidgen", + "wall", ] [workspace.dependencies] @@ -86,10 +87,10 @@ clap = { workspace = true } clap_complete = { workspace = true } clap_mangen = { workspace = true } dns-lookup = { workspace = true } -parse_datetime = {workspace = true} +parse_datetime = { workspace = true } phf = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } +serde_json = { workspace = true } textwrap = { workspace = true } uucore = { workspace = true } @@ -113,13 +114,16 @@ nologin = { optional = true, version = "0.0.1", package = "uu_nologin", path = " renice = { optional = true, version = "0.0.1", package = "uu_renice", path = "src/uu/renice" } rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" } setpgid = { optional = true, version = "0.0.1", package = "uu_setpgid", path = "src/uu/setpgid" } -setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path ="src/uu/setsid" } -uuidgen = { optional = true, version = "0.0.1", package = "uu_uuidgen", path ="src/uu/uuidgen" } +setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path = "src/uu/setsid" } +uuidgen = { optional = true, version = "0.0.1", package = "uu_uuidgen", path = "src/uu/uuidgen" } +wall = { optional = true, version = "0.0.1", package = "uu_wall", path = "src/uu/wall" } [dev-dependencies] ctor = "0.6.0" # dmesg test require fixed-boot-time feature turned on. -dmesg = { version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg", features = ["fixed-boot-time"] } +dmesg = { version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg", features = [ + "fixed-boot-time", +] } libc = { workspace = true } pretty_assertions = "1" rand = { workspace = true } diff --git a/src/uu/wall/Cargo.toml b/src/uu/wall/Cargo.toml new file mode 100644 index 00000000..ccf6a029 --- /dev/null +++ b/src/uu/wall/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "uu_wall" +version = "0.0.1" +edition = "2021" + +[lib] +path = "src/wall.rs" + +[[bin]] +name = "wall" +path = "src/main.rs" + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } +libc = { workspace = true } +chrono = { workspace = true } +unicode-width = "0.2.2" diff --git a/src/uu/wall/src/main.rs b/src/uu/wall/src/main.rs new file mode 100644 index 00000000..304f2b55 --- /dev/null +++ b/src/uu/wall/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_wall); diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs new file mode 100644 index 00000000..19fedd55 --- /dev/null +++ b/src/uu/wall/src/wall.rs @@ -0,0 +1,338 @@ +#![warn(clippy::all, clippy::pedantic)] + +use std::{ + ffi::{CStr, CString}, + fmt::Write as fw, + fs::{File, OpenOptions}, + io::{stdin, BufRead, BufReader, Read, Write}, + path::Path, + str::FromStr, + sync::{mpsc, Arc}, + time::{Duration, SystemTime}, +}; + +use chrono::{DateTime, Local}; +use clap::{crate_version, Arg, ArgAction, Command}; +use libc::{c_char, gid_t}; +use unicode_width::UnicodeWidthChar; +use uucore::{ + error::{UResult, USimpleError}, + format_usage, help_about, help_usage, +}; + +const TERM_WIDTH: usize = 79; +const BLANK: &str = unsafe { str::from_utf8_unchecked(&[b' '; TERM_WIDTH]) }; + +const ABOUT: &str = help_about!("wall.md"); +const USAGE: &str = help_usage!("wall.md"); + +#[must_use] +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg( + Arg::new("input") + .value_name(" | ") + .help("file to read or literal message") + .num_args(1..) + .index(1), + ) + .arg( + Arg::new("group") + .short('g') + .long("group") + .help("only send mesage to group"), + ) + .arg( + Arg::new("nobanner") + .short('n') + .long("nobanner") + .action(ArgAction::SetTrue) + .help("do not print banner, works only for root"), + ) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .value_parser(clap::value_parser!(u64)) + .help("write timeout in seconds"), + ) +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = uu_app().try_get_matches_from_mut(args)?; + + let timeout = args.get_one::("timeout"); + if timeout == Some(&0) { + return Err(USimpleError::new(1, "invalid timeout argument: 0")); + } + + let flag = args.get_flag("nobanner"); + let print_banner = if flag && unsafe { libc::geteuid() } != 0 { + eprintln!("wall: --nobanner is available only for root"); + true + } else { + !flag + }; + + let group = args + .get_one::("group") + .map(get_group_gid) + .transpose()?; + + match args.get_many::("input") { + Some(v) => { + let vals: Vec<&str> = v.map(String::as_str).collect(); + + let fname = vals + .first() + .expect("clap guarantees at least 1 value for input"); + + let p = Path::new(fname); + if vals.len() == 1 && p.exists() { + // When we are not root, but suid or sgid, refuse to read files + // (e.g. device files) that the user may not have access to. + // After all, our invoker can easily do "wall < file" + // instead of "wall file". + unsafe { + let uid = libc::getuid(); + if uid > 0 && (uid != libc::geteuid() || libc::getgid() != libc::getegid()) { + return Err(USimpleError::new( + 1, + format!("will not read {fname} - use stdin"), + )); + } + } + + let f = File::open(p) + .map_err(|_| USimpleError::new(1, format!("cannot open {fname}")))?; + + wall(f, group, timeout, print_banner); + } else { + let mut s = vals.as_slice().join(" "); + s.push('\n'); + wall(s.as_bytes(), group, timeout, print_banner); + } + } + None => wall(stdin(), group, timeout, print_banner), + } + + Ok(()) +} + +fn wall(input: R, group: Option, timeout: Option<&u64>, print_banner: bool) { + let msg = makemsg(input, print_banner); + let mut seen_ttys = Vec::with_capacity(16); + loop { + let entry = unsafe { + let utmpptr = libc::getutxent(); + if utmpptr.is_null() { + break; + } + &*utmpptr + }; + + if entry.ut_user[0] == 0 || entry.ut_type != libc::USER_PROCESS { + continue; + } + + let first = entry.ut_line[0].cast_unsigned(); + if first == 0 || first == b':' { + continue; + } + + if let Some(gid) = group { + if !is_gr_member(&entry.ut_user, gid) { + continue; + } + } + + let tty = unsafe { + let len = entry + .ut_line + .iter() + .position(|&c| c == 0) + .unwrap_or(entry.ut_line.len()); + + let bytes = std::slice::from_raw_parts(entry.ut_line.as_ptr().cast(), len); + str::from_utf8_unchecked(bytes).to_owned() + }; + + if !seen_ttys.contains(&tty) { + if let Err(e) = ttymsg(&tty, msg.clone(), timeout) { + eprintln!("warn ({tty:?}): {e}"); + } + seen_ttys.push(tty); + } + } + unsafe { libc::endutxent() }; +} + +fn makemsg(input: R, print_banner: bool) -> Arc { + let mut buf = String::with_capacity(256); + if print_banner { + let hostname = unsafe { + let mut buf = [0; 256]; + let ret = libc::gethostname(buf.as_mut_ptr(), buf.len()); + if ret == 0 { + CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned() + } else { + "unknown".to_string() + } + }; + + let whom = unsafe { + let ruid = libc::getuid(); + let pw = libc::getpwuid(ruid); + if !pw.is_null() && !(*pw).pw_name.is_null() { + CStr::from_ptr((*pw).pw_name).to_string_lossy().into_owned() + } else { + eprintln!("cannot get passwd uid"); + "".to_string() + } + }; + + let whereat = unsafe { + let tty_ptr = libc::ttyname(libc::STDOUT_FILENO); + if tty_ptr.is_null() { + "somewhere".to_string() + } else { + let s = CStr::from_ptr(tty_ptr).to_string_lossy(); + s.strip_prefix("/dev/").unwrap_or(&s).to_string() + } + }; + + let date = DateTime::::from(SystemTime::now()).format("%a %b %e %T %Y"); + let banner = format!("Broadcast message from {whom}@{hostname} ({whereat}) ({date}):"); + + buf += BLANK; + buf += "\r\n"; + buf += &banner; + buf.extend(std::iter::repeat_n( + ' ', + TERM_WIDTH.saturating_sub(banner.len()), + )); + buf += "\x07\x07\r\n"; + } + + buf += BLANK; + buf += "\r\n"; + + let mut reader = BufReader::new(input).lines(); + while let Some(Ok(line)) = reader.next() { + buf += &fputs_careful(&line); + } + + buf += BLANK; + buf += "\r\n"; + + Arc::new(buf) +} + +fn fputs_careful(line: &str) -> String { + let mut buf = String::with_capacity(line.len() + TERM_WIDTH); + let mut col = 0; + + for ch in line.chars() { + match ch { + '\x07' => buf.push(ch), + '\t' => { + buf.push(ch); + col += 7 - (col % 8); + } + _ if ch.is_ascii_control() => { + buf.push('^'); + buf.push((ch as u8 ^ 0x40) as char); + col += 2; + } + _ if (0x80_u8..=0x9F).contains(&(ch as u8)) => { + let _ = write!(buf, "\\x{:02X}", ch as u8); + col += 4; + } + _ if ch.is_control() => { + let _ = write!(buf, "\\u{:04X}", ch as u32); + col += 6; + } + _ => { + buf.push(ch); + col += ch.width_cjk().unwrap_or_default(); + } + } + + if col >= TERM_WIDTH { + buf += "\r\n"; + col = 0; + } + } + + buf.extend(std::iter::repeat_n(' ', TERM_WIDTH.saturating_sub(col))); + buf + "\r\n" +} + +fn is_gr_member(user: &[c_char], gid: gid_t) -> bool { + #![allow(clippy::cast_sign_loss)] + + let pw = unsafe { libc::getpwnam(user.as_ptr()) }; + if pw.is_null() { + return false; + } + + let group = unsafe { (*pw).pw_gid }; + if gid == group { + return true; + } + + let mut ngroups = 16; + let mut groups = vec![0; ngroups as usize]; + while unsafe { libc::getgrouplist(user.as_ptr(), group, groups.as_mut_ptr(), &raw mut ngroups) } + == -1 + { + groups.resize(ngroups as usize, 0); + } + groups.contains(&gid) +} + +fn get_group_gid(group: &String) -> UResult { + let cname = + CString::from_str(group).map_err(|_| USimpleError::new(1, "invalid group argument"))?; + + let gr = unsafe { libc::getgrnam(cname.as_ptr()) }; + if !gr.is_null() { + return Ok(unsafe { (*gr).gr_gid }); + } + + let gid = group + .parse::() + .map_err(|_| USimpleError::new(1, "invalid group argument"))?; + + if unsafe { libc::getgrgid(gid) }.is_null() { + return Err(USimpleError::new(1, format!("{group}: unknown gid"))); + } + Ok(gid) +} + +fn ttymsg(tty: &str, msg: Arc, timeout: Option<&u64>) -> Result<(), &'static str> { + let (tx, rx) = mpsc::channel(); + let mut device = String::with_capacity(5 + tty.len()); + device.push_str("/dev/"); + device.push_str(tty); + + std::thread::spawn(move || { + let r = match OpenOptions::new().write(true).open(&device) { + Ok(mut f) => f.write_all(msg.as_bytes()).map_err(|_| "write failed"), + Err(_) => Err("open failed"), + }; + let _ = tx.send(r); + }); + + if let Some(&t) = timeout { + rx.recv_timeout(Duration::from_secs(t)) + .map_err(|_| "write timeout")? + } else { + rx.recv().map_err(|_| "channel closed")? + } +} diff --git a/src/uu/wall/wall.md b/src/uu/wall/wall.md new file mode 100644 index 00000000..37481667 --- /dev/null +++ b/src/uu/wall/wall.md @@ -0,0 +1,7 @@ +# wall + +``` +wall [options] [ | ] +``` + +Write a mesage to all users.