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.
This commit is contained in:
parent
b538e148d1
commit
7901a2a0b0
|
@ -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,
|
||||
}
|
||||
}
|
331
src/main.rs
331
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<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 {
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
51
src/pty.rs
51
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<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
|
||||
|
|
|
@ -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),
|
||||
))
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue