From 7901a2a0b0de551ba346b1ed7a436e121ae64a58 Mon Sep 17 00:00:00 2001 From: Michael Pfaff Date: Thu, 8 Jun 2023 00:28:31 -0400 Subject: [PATCH] Work - Automatic terminfo installation - Some very nasty code for this support. To be cleaned up at a later point. - Seemingly correct privilege dropping/impersonation for spawning user shells - Run the user's shell from /etc/passwd (but uses the libc api instead of accessing the file directly) - Seems to have fixed the bug when using the fish shell that was mentioned in a previous commit - A parser for the `id` command, used for determining the UID, GID, and supplementary groups of a given user by name (could get the UID and GID from the same API used to get the shell, but would miss out on supplementary groups). - Temporarily disabled `Stream::Exec` until it can be brought up to speed with improvements made to `Stream::Shell` - Added a workaround for an oddity in PAM authentication - Further testing has suggested that the "workaround" might have been a fluke/misunderstanding of the problem. Further testing is needed. --- src/io_util.rs | 16 +++ src/main.rs | 331 +++++++++++++++++++++++++++++++++++++++++------ src/passwd.rs | 218 +++++++++++++++++++++++++++++++ src/pty.rs | 51 +++++--- src/terminfo.rs | 115 ++++++++++++++++ src/user_info.rs | 128 ++++++++++++++++++ 6 files changed, 801 insertions(+), 58 deletions(-) create mode 100644 src/io_util.rs create mode 100644 src/passwd.rs create mode 100644 src/terminfo.rs create mode 100644 src/user_info.rs diff --git a/src/io_util.rs b/src/io_util.rs new file mode 100644 index 0000000..c9053d5 --- /dev/null +++ b/src/io_util.rs @@ -0,0 +1,16 @@ +use std::io::{ErrorKind, Result}; + +use nix::unistd::{Gid, Uid}; + +pub fn create_dir_owned(path: &str, uid: Uid, gid: Gid) -> Result<()> { + std::fs::create_dir(path)?; + nix::unistd::chown(path, Some(uid), Some(gid))?; + Ok(()) +} + +pub fn ignore_already_exists(r: Result<()>) -> Result<()> { + match r { + Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), + _ => r, + } +} diff --git a/src/main.rs b/src/main.rs index 88376df..1ccd5c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,36 +6,67 @@ extern crate serde; #[macro_use] extern crate tracing; +mod io_util; +mod passwd; mod pty; +mod terminfo; +mod user_info; use std::ffi::{CStr, CString}; +use std::fmt; use std::future::Future; use std::mem::ManuallyDrop; use std::net::SocketAddr; use std::os::fd::FromRawFd; use std::os::unix::process::CommandExt; use std::process::Stdio; +use std::ptr::NonNull; use std::str::FromStr; use std::sync::Arc; use std::task::Poll; use anyhow::{Context, Result}; use base64::Engine as _; +use nix::unistd::{Gid, Uid}; use pam_client::ConversationHandler; use quinn::{ReadExactError, RecvStream, SendStream}; use rustls::client::ServerCertVerifier; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::Command; use tracing::Instrument; +use user_info::UserInfo; use webpki::SubjectNameRef; +use crate::user_info::get_user_info; + #[derive(Debug, Clone, Serialize, Deserialize)] +struct Term { + name: String, + info: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] enum Stream { Exec, - Shell, + Shell { + env_term: Option, + command: Option<(CString, Vec)>, + }, // TODO: port forwarding } +impl fmt::Debug for Stream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Exec => f.write_str("Exec"), + Self::Shell { + env_term: _, + command, + } => f.debug_struct("Shell").field("command", command).finish(), + } + } +} + #[tokio::main] async fn main() -> Result<()> { tracing::subscriber::set_global_default( @@ -49,15 +80,19 @@ async fn main() -> Result<()> { let mut args = std::env::args(); _ = args.next(); - let ctrl_c = tokio::signal::ctrl_c(); let fut = run_cmd(args); + if std::env::var("NO_CTRLC").is_ok() { + fut.await + } else { + let ctrl_c = tokio::signal::ctrl_c(); - tokio::select! { - _ = ctrl_c => { - info!("Aborting"); - Ok(()) + tokio::select! { + _ = ctrl_c => { + info!("Aborting"); + Ok(()) + } + r = fut => r, } - r = fut => r, } } @@ -73,7 +108,6 @@ async fn run_cmd(mut args: std::env::Args) -> Result<()> { const ALPN_QUINOA: &str = "quinoa"; struct ServerConfig { - shell: String, listen: SocketAddr, } @@ -84,18 +118,11 @@ struct ClientConfig { async fn run_server() -> Result<()> { let cfg = { - let opt_shell = std::env::var("SHELL").context("SHELL not defined")?; let opt_listen = std::env::var("BIND_ADDR") .unwrap_or_else(|_| "127.0.0.1:8022".to_owned()) .parse()?; - &*Box::leak( - ServerConfig { - shell: opt_shell, - listen: opt_listen, - } - .into(), - ) + &*Box::leak(ServerConfig { listen: opt_listen }.into()) }; let subject_alt_names = vec!["localhost".to_string()]; @@ -164,7 +191,7 @@ fn transport_config() -> quinn::TransportConfig { #[derive(Debug, Clone, Copy)] struct FinishedEarly; -impl std::fmt::Display for FinishedEarly { +impl fmt::Display for FinishedEarly { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { ReadExactError::FinishedEarly.fmt(f) } @@ -423,7 +450,7 @@ impl Hash { const B64: base64::engine::general_purpose::GeneralPurpose = base64::engine::general_purpose::STANDARD_NO_PAD; -impl std::fmt::Display for Hash { +impl fmt::Display for Hash { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use fast_hex::Encode; match self { @@ -598,7 +625,22 @@ async fn run_client(mut args: std::env::Args) -> Result<()> { let (mut send, mut recv) = conn.open_bi().await?; - write_msg(&mut send, &Stream::Shell).await?; + let env_term = std::env::var("TERM"); + let env_terminfo = std::env::var("TERMINFO"); + + write_msg( + &mut send, + &Stream::Shell { + env_term: if let (Ok(name), Ok(path)) = (env_term, env_terminfo) { + let info = terminfo::read_terminfo(&path, &name).await?; + Some(Term { name, info }) + } else { + None + }, + command: None, + }, + ) + .await?; info!("connected"); @@ -834,10 +876,11 @@ async fn authenticate_conn(cfg: &'static ServerConfig, conn: &quinn::Connection) } } + let username = hello.username.clone(); let hdl = tokio::task::spawn_blocking(move || { let mut ctx = pam_client::Context::new( "sshd", - Some(&hello.username), + Some(&username), Conversation { send: q_send, recv: a_recv, @@ -845,13 +888,24 @@ async fn authenticate_conn(cfg: &'static ServerConfig, conn: &quinn::Connection) )?; info!("created context"); - ctx.authenticate(pam_client::Flag::NONE)?; + ctx.authenticate(pam_client::Flag::NONE) + .context("authenticate")?; info!("authenticated user"); - ctx.acct_mgmt(pam_client::Flag::NONE)?; - info!("validated user"); + // very odd behaviour: + // if user is currently logged in, we must call acct_mgmt first. + // if user is not currently logged in, we must call acc_mgmt first and ignore the error. + let r = ctx.acct_mgmt(pam_client::Flag::NONE).context("acct_mgmt"); + if cfg!(not(any(target_os = "macos", target_os = "ios"))) { + r?; + info!("validated user"); + } else { + info!("not validating user due to macOS oddity"); + } - let sess = ctx.open_session(pam_client::Flag::NONE)?; + let sess = ctx + .open_session(pam_client::Flag::NONE) + .context("open_session")?; info!("opened session"); let sess = sess.leak(); @@ -946,18 +1000,45 @@ async fn authenticate_conn(cfg: &'static ServerConfig, conn: &quinn::Connection) let (mut ctx, sess) = hdl.await??; let sess = ctx.unleak_session(sess); - info!("logged in: {}", sess.envlist()); + let env = sess.envlist(); + info!("logged in: {}", env); write_msg(&mut send, &Question::LoggedIn).await?; send.finish().await?; recv.stop(0u8.into())?; - handle_conn(cfg, conn).await + let user_info = get_user_info(&hello.username).await?; + + let span = info_span!("logged_in", username = user_info.user.name,); + + // TODO: move out of authenticate_conn, return necessary info + handle_conn(cfg, conn, user_info, env) + .instrument(span) + .await } -async fn handle_conn(cfg: &'static ServerConfig, conn: &quinn::Connection) -> Result<()> { +async fn handle_conn( + cfg: &'static ServerConfig, + conn: &quinn::Connection, + user_info: UserInfo, + env: pam_client::env_list::EnvList, +) -> Result<()> { info!("established"); + let env = env + .into_iter() + .filter_map(|pair| { + let element = pair.as_cstr().to_bytes_with_nul(); + let sep = element + .iter() + .position(|b| *b == b'=') + .unwrap_or(element.len()); + let k = CString::new(&element[..sep]).ok()?; + let v = CString::new(&element[sep + 1..]).ok()?; + Some((k, v)) + }) + .collect::>(); + loop { let stream = conn.accept_bi().await; let (send, mut recv) = match stream { @@ -976,11 +1057,16 @@ async fn handle_conn(cfg: &'static ServerConfig, conn: &quinn::Connection) -> Re "stream", r#type = ?stream ); + let user_info = user_info.clone(); + let env = env.clone(); tokio::task::spawn( async move { let r = match stream { - Stream::Exec => handle_stream_exec(cfg, send, recv).await, - Stream::Shell => handle_stream_shell(cfg, send, recv).await, + Stream::Exec => handle_stream_exec(cfg, send, recv, &user_info).await, + Stream::Shell { env_term, command } => { + handle_stream_shell(cfg, send, recv, &user_info, env, env_term, command) + .await + } }; if let Err(e) = r { error!("Error in stream handler: {e}"); @@ -995,11 +1081,10 @@ async fn handle_stream_exec( cfg: &ServerConfig, mut send: SendStream, mut recv: RecvStream, + user_info: &UserInfo, ) -> Result<()> { - let mut cmd = std::process::Command::new(&cfg.shell); - if cfg.shell == "bash" || cfg.shell.ends_with("/bash") { - cmd.arg("-i"); - } + (|| todo!())(); + let mut cmd = std::process::Command::new(""); cmd.stdout(Stdio::piped()) .stderr(Stdio::piped()) .stdin(Stdio::piped()); @@ -1067,17 +1152,177 @@ async fn handle_stream_shell( cfg: &ServerConfig, mut send: SendStream, mut recv: RecvStream, + user_info: &UserInfo, + env: Vec<(CString, CString)>, + env_term: Option, + command: Option<(CString, Vec)>, ) -> Result<()> { - let args = if cfg.shell == "bash" || cfg.shell.ends_with("/bash") { - vec![CStr::from_bytes_with_nul(b"-i\0")?] + let mut user_passwd_buf = Vec::new(); + let user_passwd = passwd::Passwd::from_uid(user_info.user.id, &mut user_passwd_buf)?; + let user_home = user_passwd.as_ref().map(|p| p.dir); + let shell = command + .as_ref() + .map(|(command, _)| command.as_ref()) + .unwrap_or_else(|| { + user_passwd + .as_ref() + .map(|p| p.shell) + .unwrap_or(CStr::from_bytes_with_nul(b"/bin/sh\0").unwrap()) + }); + + let shell_name = CStr::from_bytes_with_nul( + shell + .to_bytes_with_nul() + .iter() + .rposition(|b| *b == b'/') + .map(|i| &shell.to_bytes_with_nul()[i + 1..]) + .unwrap_or(shell.to_bytes_with_nul()), + )?; + let args = command + .as_ref() + .map(|(_, args)| args.iter().map(|s| s.as_ref()).collect::>()) + .unwrap_or_else(|| vec![&shell_name]); + let opt_shell = shell; + + let c_user_name = CString::new(user_info.user.name.as_str())?; + let user_home = user_home.as_ref().and_then(|s| s.to_str().ok()); + const TERMINFO_PATH: &str = "/.local/share/quinoa/terminfo"; + let terminfo_path = if let Some(user_home) = user_home { + let mut terminfo_path = String::with_capacity(user_home.len() + TERMINFO_PATH.len()); + terminfo_path.push_str(user_home); + for comp in TERMINFO_PATH.split_inclusive('/') { + terminfo_path.push_str(comp); + if comp != "/" { + io_util::ignore_already_exists(io_util::create_dir_owned( + &terminfo_path, + user_info.user.id, + user_info.group.id, + ))?; + } + } + Some(terminfo_path) } else { - vec![] + None }; - let mut opt_shell_with_nul = Vec::with_capacity(cfg.shell.len() + 1); - opt_shell_with_nul.extend(cfg.shell.as_bytes()); - opt_shell_with_nul.push(0); - let opt_shell = CStr::from_bytes_with_nul(&opt_shell_with_nul)?; - let mut sh = pty::create_pty(opt_shell, &args)?; + if let (Some(env_term), Some(terminfo_path)) = (&env_term, &terminfo_path) { + terminfo::install_terminfo( + terminfo_path, + &env_term.name, + &env_term.info, + (user_info.user.id, user_info.group.id), + ) + .await?; + } + let c_env_term = env_term.map(|t| CString::new(t.name)).transpose()?; + let c_env_terminfo = terminfo_path.map(CString::new).transpose()?; + + let pre_exec = unsafe { + pty::PreExec::new(move || { + fn unsetenv(name: &CStr) -> Result<(), nix::Error> { + if unsafe { libc::unsetenv(name.as_ptr()) } != 0 { + Err(nix::Error::last().into()) + } else { + Ok(()) + } + } + + fn setenv(name: &CStr, value: &CStr, overwrite: bool) -> Result<(), nix::Error> { + if unsafe { libc::setenv(name.as_ptr(), value.as_ptr(), overwrite as i32) } != 0 { + Err(nix::Error::last().into()) + } else { + Ok(()) + } + } + + fn getenv(name: &CStr) -> Option> { + unsafe { + let ptr = libc::getenv(name.as_ptr()); + NonNull::new(CStr::from_ptr(ptr) as *const _ as *mut _) + } + } + + extern "C" { + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub fn _NSGetEnviron() -> *mut *const *const std::os::raw::c_char; + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + static mut environ: *const *const c_char; + } + + let mut keep = Vec::new(); + const PRESERVE_ENV: &[&[u8]] = &[b"PATH\0", b"LANG\0"]; + unsafe { + #[cfg(any(target_os = "macos", target_os = "ios"))] + let mut environ = *_NSGetEnviron(); + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + let mut environ = environ; + if !environ.is_null() { + while !(*environ).is_null() { + let key_value = CStr::from_ptr(*environ).to_bytes_with_nul(); + environ = environ.add(1); + let Some(i) = key_value.iter().position(|b| *b == b'=') else { + continue + }; + let key = &key_value[..i]; + let value = &key_value[i + 1..]; + if PRESERVE_ENV.iter().any(|s| &s[..s.len() - 1] == key) { + keep.push((key, value)); + } + } + } + } + nix::env::clearenv()?; + for (k, v) in keep + .into_iter() + .chain(env.iter().map(|(k, v)| (k.as_bytes(), v.as_bytes()))) + { + setenv(&CString::new(k)?, CStr::from_bytes_with_nul(v)?, true)?; + } + setenv( + CStr::from_bytes_with_nul(b"USER\0").unwrap(), + &c_user_name, + true, + )?; + if let Some(c_env_terminfo) = &c_env_terminfo { + // otherwise system terminfo will be used + setenv( + CStr::from_bytes_with_nul(b"TERMINFO\0").unwrap(), + &c_env_terminfo, + true, + )?; + } + if let Some(c_env_term) = &c_env_term { + setenv( + CStr::from_bytes_with_nul(b"TERM\0").unwrap(), + c_env_term, + true, + )?; + } + + #[cfg(not(any( + target_os = "macos", + target_os = "ios", + target_os = "redox", + target_os = "haiku" + )))] + nix::unistd::setgroups(&user_info.groups.into_iter().map(|g| g.id).collect()) + .context("setting supplementary groups")?; + + nix::unistd::setgid(user_info.group.id).context("setting primary group")?; + nix::unistd::setuid(user_info.user.id).context("setting user")?; + + if nix::unistd::seteuid(Uid::from_raw(0)).is_ok() { + return Err(anyhow!("We got the privileges back via seteuid. Very bad!")); + } + if nix::unistd::setuid(Uid::from_raw(0)).is_ok() { + return Err(anyhow!("We got the privileges back via setuid. Very bad!")); + } + + std::env::set_current_dir("/")?; + + Ok(()) + }) + }; + let mut sh = pty::create_pty(opt_shell, &args, pre_exec)?; sh.set_nodelay()?; let mut pty = sh.pty; //let mut pty = tokio::io::unix::AsyncFd::with_interest(pty, tokio::io::Interest::READABLE)?; @@ -1214,14 +1459,14 @@ async fn handle_stream_shell( //_ = waker_recv.try_recv(); if let Err(e) = r { if e.raw_os_error() == Some(35) { - //info!("not ready: {}", e); + //debug!("not ready: {}", e); //tokio::task::yield_now().await; //tokio::time::sleep(std::time::Duration::from_millis(1)).await; } else { return Err(e.into()); } } else if buf.len() == 0 { - info!("not ready: empty"); + debug!("not ready: empty"); //tokio::task::yield_now().await; //tokio::time::sleep(std::time::Duration::from_millis(1)).await; } else { diff --git a/src/passwd.rs b/src/passwd.rs new file mode 100644 index 0000000..0354ce7 --- /dev/null +++ b/src/passwd.rs @@ -0,0 +1,218 @@ +//! Get user information stored in the password file `/etc/passwd`. +//! +//! This crate provides a safe wrapper for libc functions such as [`getpwnam_r(3)`] and [`getpwuid_r(3)`]. +//! +//! # Usage +//! +//! Add this to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! etc-passwd = "0.2.0" +//! ``` +//! +//! # Examples +//! +//! Get a current user information: +//! +//! ``` +//! use passwd::Passwd; +//! +//! # fn main() -> Result<(), Box> { +//! if let Some(passwd) = Passwd::current_user()? { +//! println!("current user name is: {}", passwd.name.to_str()?); +//! println!("your user id is: {}", passwd.uid); +//! println!("your group id is: {}", passwd.gid); +//! println!("your full name is: {}", passwd.gecos.to_str()?); +//! println!("your home directory is: {}", passwd.dir.to_str()?); +//! println!("your login shell is: {}", passwd.shell.to_str()?); +//! } else { +//! println!("oops! current user is not found... who are you?"); +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! [`getpwnam_r(3)`]: https://man7.org/linux/man-pages/man3/getpwnam_r.3.html +//! [`getpwuid_r(3)`]: https://man7.org/linux/man-pages/man3/getpwuid_r.3.html + +use std::ffi::CStr; +use std::io::{Error, Result}; +use std::mem::MaybeUninit; + +use nix::unistd::{Gid, Uid}; + +/// Representation of a user information stored in the password file `/etc/passwd`. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Passwd<'a> { + /// A username. + pub name: &'a CStr, + /// A user password. + pub passwd: &'a CStr, + /// A user ID. + pub uid: Uid, + /// A group ID. + pub gid: Gid, + /// A user full name or a comment. + pub gecos: &'a CStr, + /// A home directory. + pub dir: &'a CStr, + /// A shell program. + pub shell: &'a CStr, +} + +impl<'a> Passwd<'a> { + /// Looks up the username in the password file and returns a `Passwd` with user information, if the user is found. + pub fn from_name(name: impl AsRef, buf: &'a mut Vec) -> Result> { + let name = name.as_ref(); + getpw_r(name.as_ptr(), buf, libc::getpwnam_r) + } + + /// Looks up the user ID and returns a `Passwd` with user information, if the user is found. + pub fn from_uid(uid: Uid, buf: &'a mut Vec) -> Result> { + getpw_r(uid.as_raw(), buf, libc::getpwuid_r) + } + + /// Looks up current user's information in the password file and return a `Passwd` with user information, if the user is found. + /// + /// This is a shortcut for `Passwd::from_uid(libc::getuid())`. + pub fn current_user(buf: &'a mut Vec) -> Result> { + Self::from_uid(unsafe { nix::unistd::getuid() }, buf) + } + + unsafe fn from_c_struct(passwd: libc::passwd) -> Self { + let libc::passwd { + pw_name, + pw_passwd, + pw_uid, + pw_gid, + pw_gecos, + pw_dir, + pw_shell, + .. + } = passwd; + Self { + name: CStr::from_ptr(pw_name), + passwd: CStr::from_ptr(pw_passwd), + uid: Uid::from_raw(pw_uid), + gid: Gid::from_raw(pw_gid), + gecos: CStr::from_ptr(pw_gecos), + dir: CStr::from_ptr(pw_dir), + shell: CStr::from_ptr(pw_shell), + } + } +} + +fn getpw_r<'a, T>( + key: T, + buf: &'a mut Vec, + f: unsafe extern "C" fn( + key: T, + pwd: *mut libc::passwd, + buf: *mut libc::c_char, + buflen: libc::size_t, + result: *mut *mut libc::passwd, + ) -> libc::c_int, +) -> Result>> +where + T: Copy, +{ + let mut passwd = MaybeUninit::uninit(); + let amt = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) }; + let mut amt = libc::c_long::max(amt, 512) as usize; + buf.clear(); + buf.reserve(amt); + + loop { + buf.reserve(amt); + let mut result = std::ptr::null_mut(); + let code = unsafe { + f( + key, + passwd.as_mut_ptr(), + buf.as_mut_ptr() as _, + buf.capacity(), + &mut result, + ) + }; + + return if !result.is_null() { + Ok(Some(unsafe { Passwd::from_c_struct(passwd.assume_init()) })) + } else if code == 0 { + Ok(None) + } else { + let e = Error::last_os_error(); + let errno = e.raw_os_error().unwrap(); + match errno { + // A signal was caught + libc::EINTR => continue, + + // Insufficient buffer space + libc::ERANGE => { + amt *= 2; + continue; + } + + // The given name or uid was not found. + // see https://man7.org/linux/man-pages/man3/getpwnam_r.3.html + 0 | libc::ENOENT | libc::ESRCH | libc::EBADF | libc::EPERM => Ok(None), + + // Other errors + _ => Err(e), + } + }; + } +} + +#[cfg(test)] +mod test { + use std::ffi::CString; + + use super::*; + + type Result = std::result::Result>; + + #[test] + fn root() -> Result<()> { + let mut buf_a = Vec::new(); + let mut buf_b = Vec::new(); + let by_name = + Passwd::from_name(CStr::from_bytes_with_nul(b"root\0")?, &mut buf_a)?.unwrap(); + let by_uid = Passwd::from_uid(0.into(), &mut buf_b)?.unwrap(); + + assert_eq!(by_name.uid, Uid::from_raw(0)); + assert_eq!(by_name.gid, Gid::from_raw(0)); + assert_eq!(by_name.name.to_str()?, "root"); + assert_eq!(by_name.dir.to_str()?, "/root"); + + assert_eq!(by_uid, by_name); + + Ok(()) + } + + #[test] + fn current_user() -> Result<()> { + let uid = unsafe { nix::unistd::getuid() }; + let mut buf_a = Vec::new(); + let mut buf_b = Vec::new(); + let by_cu = Passwd::current_user(&mut buf_a)?.unwrap(); + let by_name = Passwd::from_name(&by_cu.name, &mut buf_b)?.unwrap(); + + assert_eq!(by_cu.uid, uid); + // Assume $HOME is not modified + assert_eq!(by_cu.dir.to_str()?, std::env::var("HOME")?); + + assert_eq!(by_cu, by_name); + + Ok(()) + } + + #[test] + fn user_not_exist() -> Result<()> { + let mut buf_a = Vec::new(); + let mut buf_b = Vec::new(); + assert!(Passwd::from_uid(u32::MAX.into(), &mut buf_a)?.is_none()); + assert!(Passwd::from_name(CString::new("")?, &mut buf_b)?.is_none()); + Ok(()) + } +} diff --git a/src/pty.rs b/src/pty.rs index 3a87412..0bdd62e 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -1,4 +1,4 @@ -use nix::fcntl::{fcntl, open, FcntlArg, OFlag}; +use nix::fcntl::{fcntl, FcntlArg, OFlag}; use nix::pty::{grantpt, posix_openpt, ptsname, unlockpt, Winsize}; use nix::sys::stat::Mode; use nix::unistd::{ForkResult, Pid}; @@ -53,7 +53,19 @@ impl Drop for Proc { } } -pub fn create_pty + std::fmt::Debug>(path: &CStr, argv: &[S]) -> nix::Result { +pub struct PreExec(T); + +impl PreExec { + pub unsafe fn new(t: T) -> Self { + Self(t) + } +} + +pub fn create_pty(path: &CStr, argv: &[S], pre_exec: PreExec) -> nix::Result +where + S: AsRef + std::fmt::Debug, + F: FnOnce() -> anyhow::Result<()>, +{ /* Create a new master */ let master_fd = posix_openpt(OFlag::O_RDWR)?; @@ -66,16 +78,13 @@ pub fn create_pty + std::fmt::Debug>(path: &CStr, argv: &[S]) -> let slave_name = unsafe { ptsname(&master_fd) }?; /* Try to open the slave */ - let _slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, Mode::empty())?; + let _slave_fd = nix::fcntl::open(Path::new(&slave_name), OFlag::O_RDWR, Mode::empty())?; trace!("master opened the slave_fd!"); /* Launch our child process. The main application loop can inspect and then pass the stdin data to it. */ let child_pid = match unsafe { nix::unistd::fork() } { - Ok(ForkResult::Child) => { - init_child(path, argv, slave_name).unwrap(); - unreachable!() - } + Ok(ForkResult::Child) => match init_child(path, argv, pre_exec, &slave_name).unwrap() {}, Ok(ForkResult::Parent { child }) => child, Err(e) => panic!("{}", e), }; @@ -96,15 +105,28 @@ pub fn create_pty + std::fmt::Debug>(path: &CStr, argv: &[S]) -> }) } -fn init_child + std::fmt::Debug>( +fn init_child( path: &CStr, argv: &[S], - slave_name: String, -) -> anyhow::Result { - /* Open slave end for pseudoterminal */ - let slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, Mode::empty())?; - trace!("child opened the slave_fd!"); + pre_exec: PreExec, + slave_name: &str, +) -> anyhow::Result +where + S: AsRef + std::fmt::Debug, + F: FnOnce() -> anyhow::Result<()>, +{ debug!("we are going to execute: {:?} {:?}", path, argv); + init_child_common(slave_name)?; + + (pre_exec.0)()?; + trace!("running exec"); + match nix::unistd::execv(path, argv)? {} +} + +fn init_child_common(slave_name: &str) -> anyhow::Result<()> { + /* Open slave end for pseudoterminal */ + let slave_fd = nix::fcntl::open(Path::new(slave_name), OFlag::O_RDWR, Mode::empty())?; + trace!("child opened the slave_fd!"); // assign stdin, stdout, stderr to the tty nix::unistd::dup2(slave_fd, STDIN_FILENO)?; @@ -114,8 +136,7 @@ fn init_child + std::fmt::Debug>( nix::unistd::setsid().unwrap(); unsafe { set_controlling_terminal(slave_fd) }.unwrap(); - trace!("running exec"); - Ok(nix::unistd::execv(path, argv)?) + Ok(()) } // copied from... somewhere in std diff --git a/src/terminfo.rs b/src/terminfo.rs new file mode 100644 index 0000000..c705c2a --- /dev/null +++ b/src/terminfo.rs @@ -0,0 +1,115 @@ +//! Support for automatically installing terminfo files. + +use std::io::{Result, Write}; +use std::os::fd::AsRawFd; +use std::path::Path; + +use nix::unistd::{Gid, Uid}; + +#[derive(Clone, Copy)] +struct TerminfoPath<'a> { + path: &'a str, + term: &'a str, + first: char, +} + +impl<'a> TerminfoPath<'a> { + pub fn new(path: &'a str, term: &'a str) -> Option { + let first = *term.as_bytes().iter().next()?; + let first = char::from_u32(first.into())?; + Some(Self { path, term, first }) + } + + pub fn try_each(self, mut f: F) -> Result> + where + F: FnMut(&str) -> Result>, + { + let mut buf = String::new(); + if cfg!(target_os = "macos") { + self.write_hex(&mut buf); + } else { + self.write_ltr(&mut buf); + } + f(&buf) + /*self.write_ltr(&mut buf); + if let Some(t) = f(&buf)? { + return Ok(Some(t)); + } + self.write_hex(&mut buf); + f(&buf)*/ + } + + fn write_ltr(self, buf: &mut String) { + buf.clear(); + buf.reserve_exact(self.path.len() + 1 + 1 + 1 + self.term.len()); + buf.push_str(self.path); + buf.push_str("/"); + buf.push(self.first); + buf.push_str("/"); + buf.push_str(self.term); + } + + fn write_hex(self, buf: &mut String) { + use std::fmt::Write; + buf.clear(); + buf.reserve_exact(self.path.len() + 1 + 1 + 1 + self.term.len()); + buf.push_str(self.path); + buf.push_str("/"); + _ = write!(buf, "{:x}", self.first as u8); + buf.push_str("/"); + buf.push_str(self.term); + } +} + +pub async fn install_terminfo( + path: &str, + term: &str, + terminfo: &[u8], + owner: (Uid, Gid), +) -> Result<()> { + if let Some(helper) = TerminfoPath::new(path, term) { + tokio::task::block_in_place(move || { + helper + .try_each(move |path| { + if !Path::new(&path).exists() { + info!("Installing terminfo file for {:?} at {:?}", term, path); + let dir = &path[..path.len() - term.len() - 1]; + crate::io_util::ignore_already_exists(crate::io_util::create_dir_owned( + dir, owner.0, owner.1, + ))?; + let mut f = std::fs::File::create(&path)?; + nix::unistd::fchown(f.as_raw_fd(), Some(owner.0), Some(owner.1))?; + f.write_all(terminfo)?; + Ok(None) + } else { + Ok(Some(())) + } + }) + .map(|_| ()) + }) + } else { + Ok(()) + } +} + +pub async fn read_terminfo(path: &str, term: &str) -> Result> { + if let Some(helper) = TerminfoPath::new(path, term) { + let o = tokio::task::block_in_place(move || { + helper.try_each(move |path| { + if Path::new(&path).exists() { + std::fs::read(&path).map(Some) + } else { + Ok(None) + } + }) + }) + .transpose(); + if let Some(r) = o { + return r; + } + } + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("terminfo file not found in {:?} for {:?}", path, term), + )) +} diff --git a/src/user_info.rs b/src/user_info.rs new file mode 100644 index 0000000..e56ded7 --- /dev/null +++ b/src/user_info.rs @@ -0,0 +1,128 @@ +use std::process::Stdio; + +use anyhow::{Context, Result}; +use nix::unistd::{Gid, Uid}; + +#[derive(Debug, Clone)] +pub struct User { + pub name: String, + pub id: Uid, +} + +#[derive(Debug, Clone)] +pub struct Group { + pub name: String, + pub id: Gid, +} + +#[derive(Debug, Clone)] +pub struct UserInfo { + pub user: User, + pub group: Group, + pub groups: Vec, +} + +fn parse_pair(s: &str) -> Result<(u32, &str, &str)> { + let (id, s) = s.split_once('(').with_context(|| { + format!( + "Invalid output because it is missing the opening parenthesis: {:?}", + s + ) + })?; + let (name, s) = s.split_once(')').with_context(|| { + format!( + "Invalid output because it is missing the closing parenthesis: {:?}", + s + ) + })?; + let id = id + .parse() + .with_context(|| format!("Invalid output because the id is invalid: {:?}", id))?; + Ok((id, name, s)) +} + +pub async fn get_user_info(username: &str) -> Result { + let output = tokio::process::Command::new("id") + .arg("--") + .arg(username) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .output() + .await?; + let mut stdout = String::from_utf8(output.stdout)?; + if stdout.ends_with('\n') { + stdout.pop(); + } + + let mut user = None; + let mut group = None; + let mut groups = Vec::new(); + + for item in stdout.split(' ') { + let (k, v) = item + .split_once('=') + .context("Invalid output because the pair is missing the key-value delimiter")?; + match k { + "uid" if user.is_none() => { + let (id, name, s) = parse_pair(v)?; + if !s.is_empty() { + return Err(anyhow!( + "Invalid output because there are unexpected trailing bytes after pair" + )); + } + user = Some(User { + id: Uid::from_raw(id), + name: name.to_owned(), + }); + } + "gid" if group.is_none() => { + let (id, name, s) = parse_pair(v)?; + if !s.is_empty() { + return Err(anyhow!( + "Invalid output because there are unexpected trailing bytes after pair" + )); + } + group = Some(Group { + id: Gid::from_raw(id), + name: name.to_owned(), + }); + } + "groups" if groups.is_empty() => { + let mut v = v; + if v.is_empty() { + continue; + } + loop { + let (id, name, s) = parse_pair(v)?; + v = s; + groups.push(Group { + id: Gid::from_raw(id), + name: name.to_owned(), + }); + if v.is_empty() { + break; + } else if v.as_bytes()[0] != b',' { + return Err(anyhow!( + "Invalid output because there is a missing comma between pairs: {:?}", + v + )); + } else { + v = &v[1..]; + } + } + } + _ => { + return Err(anyhow!( + "Invalid output because the key was either unrecognized or duplicated: {:?}", + k + )) + } + } + } + + Ok(UserInfo { + user: user.context("Invalid output because the user entry was missing")?, + group: group.context("Invalid output because the group entry was missing")?, + groups, + }) +}