- 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.
This commit is contained in:
Michael Pfaff 2023-06-08 00:28:31 -04:00
parent b538e148d1
commit 7901a2a0b0
Signed by: michael
GPG Key ID: CF402C4A012AA9D4
6 changed files with 801 additions and 58 deletions

16
src/io_util.rs Normal file
View File

@ -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,
}
}

View File

@ -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<u8>,
}
#[derive(Clone, Serialize, Deserialize)]
enum Stream {
Exec,
Shell,
Shell {
env_term: Option<Term>,
command: Option<(CString, Vec<CString>)>,
},
// 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::<Vec<_>>();
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<Term>,
command: Option<(CString, Vec<CString>)>,
) -> 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::<Vec<_>>())
.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<NonNull<CStr>> {
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 {

218
src/passwd.rs Normal file
View File

@ -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<dyn std::error::Error>> {
//! 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<CStr>, buf: &'a mut Vec<u8>) -> Result<Option<Self>> {
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<u8>) -> Result<Option<Self>> {
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<u8>) -> Result<Option<Self>> {
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<u8>,
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<Option<Passwd<'a>>>
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<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[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(())
}
}

View File

@ -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<S: AsRef<CStr> + std::fmt::Debug>(path: &CStr, argv: &[S]) -> nix::Result<Child> {
pub struct PreExec<T>(T);
impl<T> PreExec<T> {
pub unsafe fn new(t: T) -> Self {
Self(t)
}
}
pub fn create_pty<S, F>(path: &CStr, argv: &[S], pre_exec: PreExec<F>) -> nix::Result<Child>
where
S: AsRef<CStr> + 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<S: AsRef<CStr> + 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<S: AsRef<CStr> + std::fmt::Debug>(path: &CStr, argv: &[S]) ->
})
}
fn init_child<S: AsRef<CStr> + std::fmt::Debug>(
fn init_child<S, F>(
path: &CStr,
argv: &[S],
slave_name: String,
) -> anyhow::Result<std::convert::Infallible> {
/* 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<F>,
slave_name: &str,
) -> anyhow::Result<std::convert::Infallible>
where
S: AsRef<CStr> + 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<S: AsRef<CStr> + 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

115
src/terminfo.rs Normal file
View File

@ -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<Self> {
let first = *term.as_bytes().iter().next()?;
let first = char::from_u32(first.into())?;
Some(Self { path, term, first })
}
pub fn try_each<T, F>(self, mut f: F) -> Result<Option<T>>
where
F: FnMut(&str) -> Result<Option<T>>,
{
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<Vec<u8>> {
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),
))
}

128
src/user_info.rs Normal file
View File

@ -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<Group>,
}
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<UserInfo> {
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,
})
}