This commit is contained in:
Kenny Levinsen
2020-01-21 01:01:57 +01:00
parent 608311ca9a
commit 2ef9e9ec17
13 changed files with 863 additions and 665 deletions

9
Cargo.lock generated
View File

@@ -182,14 +182,6 @@ dependencies = [
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "greetctl"
version = "0.1.0"
dependencies = [
"greet_proto 0.2.0",
"rpassword 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "greetd"
version = "0.1.1"
@@ -474,6 +466,7 @@ dependencies = [
"mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)",
"pin-project-lite 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"signal-hook-registry 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-macros 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]

View File

@@ -5,4 +5,4 @@ panic = "abort"
panic = "abort"
[workspace]
members = ["greet_proto", "greetd", "greetctl", "agreety"]
members = ["greet_proto", "greetd", "agreety"]

View File

@@ -8,7 +8,7 @@ use ini::Ini;
use nix::sys::utsname::uname;
use rpassword::prompt_password_stderr;
use greet_proto::{Header, Request, Response};
use greet_proto::{Header, Question, QuestionStyle, Request, Response};
fn prompt_stderr(prompt: &str) -> Result<String, Box<dyn std::error::Error>> {
let stdin = io::stdin();
@@ -50,16 +50,14 @@ fn get_issue() -> Result<String, Box<dyn std::error::Error>> {
}
fn login(node: &str, cmd: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let username = prompt_stderr(&format!("{} login: ", node)).unwrap();
let password = prompt_password_stderr("Password: ").unwrap();
let username = prompt_stderr(&format!("{} login: ", node))?;
let command = match cmd {
Some(cmd) => cmd.to_string(),
None => prompt_stderr("Command: ").unwrap(),
None => prompt_stderr("Command: ")?,
};
let request = Request::Login {
let request = Request::Initiate {
username,
password,
env: vec![
format!("XDG_SESSION_DESKTOP={}", &command),
format!("XDG_CURRENT_DESKTOP={}", &command),
@@ -86,11 +84,83 @@ fn login(node: &str, cmd: Option<&str>) -> Result<(), Box<dyn std::error::Error>
let resp = Response::from_slice(&resp_buf)?;
match resp {
Response::Success => Ok(()),
Response::Failure(err) => {
Err(std::io::Error::new(io::ErrorKind::Other, format!("login error: {:?}", err)).into())
Response::Success => (),
Response::Failure(err) => return Err(format!("login error: {:?}", err).into()),
_ => return Err("unexpected response".into()),
}
loop {
let request = Request::GetQuestion;
let req = request.to_bytes()?;
let header = Header::new(req.len() as u32);
stream.write_all(&header.to_bytes()?)?;
stream.write_all(&req)?;
// Read response
let mut header_buf = vec![0; Header::len()];
stream.read_exact(&mut header_buf)?;
let header = Header::from_slice(&header_buf)?;
let mut resp_buf = vec![0; header.len as usize];
stream.read_exact(&mut resp_buf)?;
let resp = Response::from_slice(&resp_buf)?;
let mut starting = false;
let request = match resp {
Response::Question { next_question } => match next_question {
Some(Question { msg, style }) => {
let answer = match style {
QuestionStyle::Visible => prompt_stderr(&msg)?,
QuestionStyle::Secret => prompt_password_stderr(&msg)?,
QuestionStyle::Info => {
eprintln!("info: {}", msg);
"".to_string()
}
QuestionStyle::Error => {
eprintln!("error: {}", msg);
"".to_string()
}
};
Request::Answer {
answer: Some(answer),
}
}
None => {
starting = true;
Request::Start
}
},
Response::Failure(err) => return Err(format!("login error: {:?}", err).into()),
_ => return Err("unexpected response".into()),
};
let req = request.to_bytes()?;
let header = Header::new(req.len() as u32);
stream.write_all(&header.to_bytes()?)?;
stream.write_all(&req)?;
// Read response
let mut header_buf = vec![0; Header::len()];
stream.read_exact(&mut header_buf)?;
let header = Header::from_slice(&header_buf)?;
let mut resp_buf = vec![0; header.len as usize];
stream.read_exact(&mut resp_buf)?;
let resp = Response::from_slice(&resp_buf)?;
match resp {
Response::Success => (),
Response::Failure(err) => return Err(format!("login error: {:?}", err).into()),
_ => return Err("unexpected response".into()),
}
if starting {
break;
}
}
Ok(())
}
fn main() {
@@ -133,9 +203,7 @@ fn main() {
Ok(()) => {
break;
}
Err(_) => {
eprintln!("");
}
Err(e) => eprintln!("error: {}", e),
}
}
}

View File

@@ -65,12 +65,17 @@ pub enum ShutdownAction {
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum Request {
Login {
Initiate {
username: String,
password: String,
cmd: Vec<String>,
env: Vec<String>,
},
GetQuestion,
Answer {
answer: Option<String>,
},
Cancel,
Start,
Shutdown {
action: ShutdownAction,
},
@@ -90,7 +95,19 @@ impl Request {
#[serde(tag = "error_type")]
#[serde(rename_all = "snake_case")]
pub enum Failure {
LoginError {
InitiateError {
description: String,
},
GetQuestionError {
description: String,
},
AnswerError {
description: String,
},
StartError {
description: String,
},
CancelError {
description: String,
},
ShutdownError {
@@ -99,11 +116,28 @@ pub enum Failure {
},
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum QuestionStyle {
Visible,
Secret,
Info,
Error,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct Question {
pub msg: String,
pub style: QuestionStyle,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum Response {
Success,
Question { next_question: Option<Question> },
Failure(Failure),
}

View File

@@ -1,9 +0,0 @@
[package]
name = "greetctl"
version = "0.1.0"
authors = ["Kenny Levinsen <kl@kl.wtf>"]
edition = "2018"
[dependencies]
greet_proto = { path = "../greet_proto" }
rpassword = "4.0"

View File

@@ -1,64 +0,0 @@
use std::env;
use std::io::{self, BufRead, Read, Write};
use std::os::unix::net::UnixStream;
use greet_proto::{Header, Request, Response};
use rpassword::prompt_password_stderr;
fn prompt_stderr(prompt: &str) -> Result<String, Box<dyn std::error::Error>> {
let stdin = io::stdin();
let mut stdin_iter = stdin.lock().lines();
eprint!("{}", prompt);
Ok(stdin_iter.next().unwrap()?)
}
fn login() -> Result<(), Box<dyn std::error::Error>> {
let username = prompt_stderr("Username: ").unwrap();
let password = prompt_password_stderr("Password: ").unwrap();
let command = prompt_stderr("Command: ").unwrap();
let request = Request::Login {
username,
password,
cmd: vec![command],
env: vec![],
};
// Write request
let req = request.to_bytes()?;
let header = Header::new(req.len() as u32);
let mut stream = UnixStream::connect(env::var("GREETD_SOCK")?)?;
stream.write_all(&header.to_bytes()?)?;
stream.write_all(&req)?;
// Read response
let mut header_buf = vec![0; Header::len()];
stream.read_exact(&mut header_buf)?;
let header = Header::from_slice(&header_buf)?;
let mut resp_buf = vec![0; header.len as usize];
stream.read_exact(&mut resp_buf)?;
let resp = Response::from_slice(&resp_buf)?;
match resp {
Response::Success => Ok(()),
Response::Failure(err) => {
Err(std::io::Error::new(io::ErrorKind::Other, format!("login error: {:?}", err)).into())
}
}
}
fn main() {
loop {
match login() {
Ok(()) => {
eprintln!("authentication successful");
break;
}
Err(err) => eprintln!("error: {:?}", err),
}
}
}

View File

@@ -15,5 +15,5 @@ serde_json = "1.0"
greet_proto = { path = "../greet_proto" }
libc = "0.2"
futures = "0.3"
tokio = { version = "0.2", features = ["net", "stream", "sync", "macros", "signal", "rt-util", "io-util"] }
tokio = { version = "0.2", features = ["net", "stream", "sync", "macros", "signal", "rt-util", "io-util", "time"] }
tokio-util = { version = "0.2", features = ["codec"] }

View File

@@ -5,17 +5,19 @@ use std::time::Duration;
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
use nix::unistd::{alarm, execv, fork, ForkResult};
use tokio::{sync::RwLock, time::delay_for};
use crate::scrambler::Scrambler;
use crate::session::{Session, SessionChild};
use greet_proto::ShutdownAction;
use crate::session::{QuestionStyle as SessQuestionStyle, Session, SessionChild, SessionState};
use greet_proto::{Question, QuestionStyle, ShutdownAction};
pub struct ContextInner {
current_session: Option<SessionChild>,
pending_session: Option<Session>,
}
/// Context keeps track of running sessions and start new ones.
pub struct Context<'a> {
session: Option<SessionChild>,
greeter: Option<SessionChild>,
pending_session: Option<Session<'a>>,
pub struct Context {
inner: RwLock<ContextInner>,
greeter_bin: String,
greeter_user: String,
vt: usize,
@@ -38,91 +40,146 @@ fn run(cmd: &str) -> Result<(), Box<dyn Error>> {
Ok(())
}
impl<'a> Context<'a> {
pub fn new(greeter_bin: String, greeter_user: String, vt: usize) -> Context<'a> {
impl Context {
pub fn new(greeter_bin: String, greeter_user: String, vt: usize) -> Context {
Context {
session: None,
greeter: None,
pending_session: None,
inner: RwLock::new(ContextInner {
current_session: None,
pending_session: None,
}),
greeter_bin,
greeter_user,
vt,
}
}
/// Start a greeter session.
pub async fn greet(&mut self) -> Result<(), Box<dyn Error>> {
if self.greeter.is_some() {
eprintln!("greeter session already active");
return Err(io::Error::new(io::ErrorKind::Other, "greeter already active").into());
}
async fn create_greeter(&self) -> Result<SessionChild, Box<dyn Error>> {
let mut pending_session = Session::new(
"greeter",
"user",
&self.greeter_user,
"",
vec![self.greeter_bin.to_string()],
vec![],
self.vt,
)?;
let greeter = match pending_session.start().await {
Ok(s) => s,
Err(e) => return Err(format!("session start failed: {}", e).into()),
};
self.greeter = Some(greeter);
)
.await?;
match pending_session.get_state().await {
Ok(SessionState::Ready) => (),
Ok(SessionState::Question(_, _)) => {
return Err("session start failed: unexpected question".into())
}
Err(err) => return Err(format!("session start failed: {}", err).into()),
}
match pending_session.start().await {
Ok(s) => Ok(s),
Err(e) => Err(format!("session start failed: {}", e).into()),
}
}
/// Start a greeter session.
pub async fn greet(&self) -> Result<(), Box<dyn Error>> {
{
let inner = self.inner.read().await;
if inner.current_session.is_some() {
eprintln!("session already active");
return Err(io::Error::new(io::ErrorKind::Other, "session already active").into());
}
}
self.inner.write().await.current_session = Some(self.create_greeter().await?);
Ok(())
}
/// Start a login session.
pub fn login(
&mut self,
pub async fn initiate(
&self,
username: String,
mut password: String,
cmd: Vec<String>,
provided_env: Vec<String>,
) -> Result<(), Box<dyn Error>> {
if self.greeter.is_none() {
eprintln!("login request not valid when greeter is not active");
return Err(io::Error::new(io::ErrorKind::Other, "greeter not active").into());
}
if self.session.is_some() {
eprintln!("login session already active");
return Err(io::Error::new(io::ErrorKind::Other, "session already active").into());
{
let inner = self.inner.read().await;
if inner.current_session.is_none() {
eprintln!("login request requires active session");
return Err(io::Error::new(io::ErrorKind::Other, "session not active").into());
}
}
let pending_session = Session::new(
"login",
"user",
&username,
&password,
cmd,
provided_env,
self.vt,
)?;
password.scramble();
self.pending_session = Some(pending_session);
// We give the greeter 5 seconds to prove itself well-behaved before
// we lose patience and shoot it in the back repeatedly. This is all
// handled by our alarm handler.
alarm::set(5);
let pending_session =
Session::new("login", "user", &username, cmd, provided_env, self.vt).await?;
self.inner.write().await.pending_session = Some(pending_session);
Ok(())
}
pub fn shutdown(&mut self, action: ShutdownAction) -> Result<(), Box<dyn Error>> {
if self.greeter.is_none() || self.session.is_some() {
eprintln!("shutdown request not valid when greeter is not active");
return Err(io::Error::new(io::ErrorKind::Other, "greeter not active").into());
pub async fn cancel(&self) -> Result<(), Box<dyn Error>> {
let pending_session = self.inner.write().await.pending_session.take();
if let Some(mut s) = pending_session {
s.post_answer(None).await?
}
Ok(())
}
pub async fn get_question(&self) -> Result<Option<Question>, Box<dyn Error>> {
let mut inner = self.inner.write().await;
match &mut inner.pending_session {
Some(s) => match s.get_state().await? {
SessionState::Ready => Ok(None),
SessionState::Question(style, string) => Ok(Some(Question {
msg: string,
style: match style {
SessQuestionStyle::Visible => QuestionStyle::Visible,
SessQuestionStyle::Secret => QuestionStyle::Secret,
SessQuestionStyle::Info => QuestionStyle::Info,
SessQuestionStyle::Error => QuestionStyle::Error,
},
})),
},
None => Err("no session active".into()),
}
}
pub async fn post_answer(&self, answer: Option<String>) -> Result<(), Box<dyn Error>> {
let mut inner = self.inner.write().await;
match &mut inner.pending_session {
Some(s) => s.post_answer(answer).await,
None => Err("no session active".into()),
}
}
pub async fn start(&self) -> Result<(), Box<dyn Error>> {
let mut inner = self.inner.write().await;
match &mut inner.pending_session {
Some(s) => {
match s.get_state().await? {
SessionState::Ready => {
// We give the greeter 5 seconds to prove itself well-behaved before
// we lose patience and shoot it in the back repeatedly. This is all
// handled by our alarm handler.
alarm::set(5);
Ok(())
}
_ => Err("session is not ready".into()),
}
}
None => Err("no session active".into()),
}
}
pub async fn shutdown(&self, action: ShutdownAction) -> Result<(), Box<dyn Error>> {
{
let inner = self.inner.read().await;
if inner.current_session.is_none() {
eprintln!("shutdown request not valid when greeter is not active");
return Err(io::Error::new(io::ErrorKind::Other, "greeter not active").into());
}
}
let cmd = match action {
ShutdownAction::Poweroff => "poweroff",
ShutdownAction::Reboot => "reboot",
ShutdownAction::Exit => {
self.terminate()?;
self.terminate().await?;
unreachable!("previous call must always fail");
}
};
@@ -131,10 +188,11 @@ impl<'a> Context<'a> {
}
/// Notify the Context of an alarm.
pub async fn alarm(&mut self) -> Result<(), Box<dyn Error>> {
pub async fn alarm(&self) -> Result<(), Box<dyn Error>> {
// Keep trying to terminate the greeter until it gives up.
if let Some(mut p) = self.pending_session.take() {
if let Some(g) = self.greeter.take() {
let mut inner = self.inner.write().await;
if let Some(mut p) = inner.pending_session.take() {
if let Some(g) = inner.current_session.take() {
if p.elapsed() > Duration::from_secs(10) {
// We're out of patience.
g.kill();
@@ -142,18 +200,18 @@ impl<'a> Context<'a> {
// Let's try to give it a gentle nudge.
g.term();
}
self.greeter = Some(g);
self.pending_session = Some(p);
inner.current_session = Some(g);
inner.pending_session = Some(p);
alarm::set(1);
return Ok(());
}
drop(inner);
let s = match p.start().await {
Ok(s) => s,
Err(e) => return Err(format!("session start failed: {}", e).into()),
};
self.session = Some(s);
let mut inner = self.inner.write().await;
inner.current_session = Some(s);
}
Ok(())
@@ -161,7 +219,7 @@ impl<'a> Context<'a> {
/// Notify the Context that it needs to check its children for termination.
/// This should be called on SIGCHLD.
pub async fn check_children(&mut self) -> Result<(), Box<dyn Error>> {
pub async fn check_children(&self) -> Result<(), Box<dyn Error>> {
loop {
match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
// No pending exits.
@@ -169,55 +227,45 @@ impl<'a> Context<'a> {
// We got an exit, see if it's something we need to clean up.
Ok(WaitStatus::Exited(pid, ..)) | Ok(WaitStatus::Signaled(pid, ..)) => {
match &self.session {
let mut inner = self.inner.write().await;
let was_greeter;
let elapsed;
match &inner.current_session {
Some(session) if session.owns_pid(pid) => {
// Session task is dead, so kill the session and
// restart the greeter. However, ensure that we are
// not spamming th egreeter by waiting at least one
// one second since login was requested before
// restarting it.
if session.elapsed() < Duration::from_secs(1) {
std::thread::sleep(std::time::Duration::from_millis(1000));
}
self.session = None;
eprintln!("session exited");
if let Err(e) = self.greet().await {
return Err(
format!("session start failed for greeter: {}", e).into()
);
}
was_greeter = session.get_service() == "greeter";
elapsed = session.elapsed();
inner.current_session = None;
}
_ => (),
};
match &self.greeter {
Some(greeter) if greeter.owns_pid(pid) => {
self.greeter = None;
match self.pending_session.take() {
Some(mut pending_session) => {
eprintln!("starting pending session");
// Our greeter finally bit the dust so we can
// start our pending session.
let s = match pending_session.start().await {
Ok(s) => s,
Err(e) => {
return Err(
format!("session start failed: {}", e).into()
);
}
};
_ => continue,
}
self.session = Some(s);
match inner.pending_session.take() {
Some(mut pending_session) => {
eprintln!("starting pending session");
// Our greeter finally bit the dust so we can
// start our pending session.
drop(inner);
let s = match pending_session.start().await {
Ok(s) => s,
Err(e) => {
return Err(format!("session start failed: {}", e).into());
}
None => {
if self.session.is_none() {
// Greeter died on us, let's just die with it.
return Err("greeter died with no pending session".into());
}
}
}
};
let mut inner = self.inner.write().await;
inner.current_session = Some(s);
}
_ => (),
};
None if !was_greeter => {
if elapsed < Duration::from_secs(1) {
delay_for(Duration::from_secs(1)).await;
}
inner.current_session = Some(self.create_greeter().await?);
}
None => {
// Greeter died on us, let's just die with it.
return Err("greeter died with no pending session".into());
}
}
}
// Useless status.
@@ -234,12 +282,13 @@ impl<'a> Context<'a> {
/// Notify the Context that we want to terminate. This should be called on
/// SIGTERM.
pub fn terminate(&mut self) -> Result<(), Box<dyn Error>> {
if let Some(session) = self.session.take() {
session.shoo();
pub async fn terminate(&self) -> Result<(), Box<dyn Error>> {
let mut inner = self.inner.write().await;
if let Some(sess) = &inner.current_session {
sess.shoo();
}
if let Some(greeter) = self.greeter.take() {
greeter.shoo();
if let Some(sess) = &mut inner.pending_session {
let _ = sess.post_answer(None).await;
}
Err("terminating".into())
}

View File

@@ -5,7 +5,7 @@ mod scrambler;
mod session;
mod terminal;
use std::{cell::RefCell, error::Error, io, rc::Rc};
use std::{error::Error, io, rc::Rc};
use nix::{
sys::mman::{mlockall, MlockAllFlags},
@@ -31,7 +31,7 @@ fn reset_vt(vt: usize) -> Result<(), Box<dyn Error>> {
Ok(())
}
async fn client(ctx: Rc<RefCell<Context<'_>>>, mut s: UnixStream) -> Result<(), Box<dyn Error>> {
async fn client(ctx: Rc<Context>, mut s: UnixStream) -> Result<(), Box<dyn Error>> {
loop {
let mut header_bytes = [0; Header::len()];
@@ -50,18 +50,39 @@ async fn client(ctx: Rc<RefCell<Context<'_>>>, mut s: UnixStream) -> Result<(),
body_bytes.scramble();
let resp = match req {
Request::Login {
username,
password,
cmd,
env,
} => match ctx.borrow_mut().login(username, password, cmd, env) {
Request::Initiate { username, cmd, env } => {
match ctx.initiate(username, cmd, env).await {
Ok(_) => Response::Success,
Err(e) => Response::Failure(Failure::InitiateError {
description: format!("{}", e),
}),
}
}
Request::Start => match ctx.start().await {
Ok(_) => Response::Success,
Err(e) => Response::Failure(Failure::LoginError {
Err(e) => Response::Failure(Failure::StartError {
description: format!("{}", e),
}),
},
Request::Shutdown { action } => match ctx.borrow_mut().shutdown(action) {
Request::GetQuestion => match ctx.get_question().await {
Ok(v) => Response::Question { next_question: v },
Err(e) => Response::Failure(Failure::GetQuestionError {
description: format!("{}", e),
}),
},
Request::Cancel => match ctx.cancel().await {
Ok(_) => Response::Success,
Err(e) => Response::Failure(Failure::CancelError {
description: format!("{}", e),
}),
},
Request::Answer { answer } => match ctx.post_answer(answer).await {
Ok(_) => Response::Success,
Err(e) => Response::Failure(Failure::AnswerError {
description: format!("{}", e),
}),
},
Request::Shutdown { action } => match ctx.shutdown(action).await {
Ok(_) => Response::Success,
Err(e) => Response::Failure(Failure::ShutdownError {
action,
@@ -79,6 +100,7 @@ async fn client(ctx: Rc<RefCell<Context<'_>>>, mut s: UnixStream) -> Result<(),
.map_err(|e| format!("unable to serialize header: {}", e))?;
s.write_all(&header_bytes).await?;
s.write_all(&resp_bytes).await?;
}
}
@@ -109,12 +131,8 @@ async fn main() {
};
drop(term);
let ctx = Rc::new(RefCell::new(Context::new(
config.greeter,
config.greeter_user,
vt,
)));
if let Err(e) = ctx.borrow_mut().greet().await {
let ctx = Rc::new(Context::new(config.greeter, config.greeter_user, vt));
if let Err(e) = ctx.greet().await {
eprintln!("unable to start greeter: {}", e);
reset_vt(vt).expect("unable to reset vt");
std::process::exit(1);
@@ -129,11 +147,7 @@ async fn main() {
let mut alarm = signal(SignalKind::alarm()).expect("unable to listen for SIGALRM");
loop {
alarm.recv().await;
alarm_ctx
.borrow_mut()
.alarm()
.await
.expect("unable to read alarm");
alarm_ctx.alarm().await.expect("unable to read alarm");
}
});
@@ -143,7 +157,6 @@ async fn main() {
loop {
child.recv().await;
child_ctx
.borrow_mut()
.check_children()
.await
.expect("unable to check children");
@@ -156,10 +169,7 @@ async fn main() {
signal(SignalKind::terminate()).expect("unable to listen for SIGTERM");
loop {
term.recv().await;
term_ctx
.borrow_mut()
.terminate()
.expect("unable to terminate");
term_ctx.terminate().await.expect("unable to terminate");
}
});

View File

@@ -1,5 +1,3 @@
use std::ffi::{CStr, CString};
/// A trait representing the PAM authentification conversation
///
/// PAM authentification is done as a conversation mechanism, in which PAM
@@ -15,46 +13,14 @@ pub trait Converse {
///
/// This would typically be the username. The exact question is provided as the
/// `msg` argument if you wish to display it to your user.
fn prompt_echo<'a>(&'a mut self, msg: &CStr) -> ::std::result::Result<&'a CStr, ()>;
fn prompt_echo(&self, msg: &str) -> ::std::result::Result<String, ()>;
/// PAM requests a value that should be typed blindly by the user
///
/// This would typically be the password. The exact question is provided as the
/// `msg` argument if you wish to display it to your user.
fn prompt_blind<'a>(&'a mut self, msg: &CStr) -> ::std::result::Result<&'a CStr, ()>;
fn prompt_blind(&self, msg: &str) -> ::std::result::Result<String, ()>;
/// This is an informational message from PAM
fn info(&mut self, msg: &CStr);
fn info(&self, msg: &str) -> Result<(), ()>;
/// This is an error message from PAM
fn error(&mut self, msg: &CStr);
}
/// A minimalistic conversation handler, that uses given login and password
///
/// This conversation handler is not really interactive, but simply returns to
/// PAM the value that have been set using the `set_credentials` method.
pub struct PasswordConv {
login: CString,
passwd: CString,
}
impl PasswordConv {
/// Create a new `PasswordConv` handler
pub fn new(login: &str, password: &str) -> PasswordConv {
PasswordConv {
login: CString::new(login).unwrap(),
passwd: CString::new(password).unwrap(),
}
}
}
impl Converse for PasswordConv {
fn prompt_echo<'a>(&'a mut self, _msg: &CStr) -> Result<&'a CStr, ()> {
Ok(&self.login)
}
fn prompt_blind<'a>(&'a mut self, _msg: &CStr) -> Result<&'a CStr, ()> {
Ok(&self.passwd)
}
fn info(&mut self, _msg: &CStr) {}
fn error(&mut self, msg: &CStr) {
eprintln!("[PAM ERROR] {}", msg.to_string_lossy());
}
fn error(&self, msg: &str) -> Result<(), ()>;
}

View File

@@ -1,13 +1,14 @@
use std::ffi::CStr;
use std::ffi::{CStr, CString};
use std::mem;
use std::pin::Pin;
use libc::{c_int, c_void, calloc, free, size_t, strdup};
use pam_sys::{PamConversation, PamMessage, PamMessageStyle, PamResponse, PamReturnCode};
use super::converse::Converse;
pub struct PamConvHandlerWrapper {
pub handler: Box<dyn Converse>,
pub struct PamConvHandlerWrapper<'a> {
pub handler: Pin<Box<dyn Converse + 'a>>,
}
pub fn make_conversation(conv: &mut PamConvHandlerWrapper) -> PamConversation {
@@ -31,7 +32,7 @@ pub extern "C" fn converse(
return PamReturnCode::BUF_ERR as c_int;
}
let wrapper = unsafe { &mut *(appdata_ptr as *mut PamConvHandlerWrapper) };
let wrapper = unsafe { &*(appdata_ptr as *const PamConvHandlerWrapper) };
let mut result: PamReturnCode = PamReturnCode::SUCCESS;
for i in 0..num_msg as isize {
@@ -39,24 +40,43 @@ pub extern "C" fn converse(
let m: &mut PamMessage = unsafe { &mut **(msg.offset(i)) };
let r: &mut PamResponse = unsafe { &mut *(resp.offset(i)) };
let msg = unsafe { CStr::from_ptr(m.msg) };
let msg = match msg.to_str() {
Ok(m) => m,
Err(_) => {
result = PamReturnCode::CONV_ERR;
break;
}
};
// match on msg_style
match PamMessageStyle::from(m.msg_style) {
PamMessageStyle::PROMPT_ECHO_ON => {
if let Ok(handler_response) = wrapper.handler.prompt_echo(msg) {
r.resp = unsafe { strdup(handler_response.as_ptr()) };
let cstr =
CString::new(handler_response).expect("unable to allocate response string");
r.resp = unsafe { strdup(cstr.as_ptr()) };
} else {
result = PamReturnCode::CONV_ERR;
}
}
PamMessageStyle::PROMPT_ECHO_OFF => {
if let Ok(handler_response) = wrapper.handler.prompt_blind(msg) {
r.resp = unsafe { strdup(handler_response.as_ptr()) };
let cstr =
CString::new(handler_response).expect("unable to allocate response string");
r.resp = unsafe { strdup(cstr.as_ptr()) };
} else {
result = PamReturnCode::CONV_ERR;
}
}
PamMessageStyle::ERROR_MSG => wrapper.handler.error(msg),
PamMessageStyle::TEXT_INFO => wrapper.handler.info(msg),
PamMessageStyle::ERROR_MSG => {
if wrapper.handler.error(msg).is_err() {
result = PamReturnCode::CONV_ERR;
}
}
PamMessageStyle::TEXT_INFO => {
if wrapper.handler.info(msg).is_err() {
result = PamReturnCode::CONV_ERR;
}
}
}
if result != PamReturnCode::SUCCESS {
break;

View File

@@ -1,6 +1,7 @@
use std::error::Error;
use std::ffi::{CStr, CString};
use std::io;
use std::pin::Pin;
use std::ptr;
use libc::c_void;
@@ -12,20 +13,25 @@ use super::ffi::{make_conversation, PamConvHandlerWrapper};
pub struct PamSession<'a> {
handle: &'a mut PamHandle,
pub converse: Box<PamConvHandlerWrapper>,
#[allow(unused)]
lifetime_extender: Pin<Box<PamConvHandlerWrapper<'a>>>,
last_code: PamReturnCode,
}
impl<'a> PamSession<'a> {
pub fn start(service: &str, pam_conv: Box<dyn Converse>) -> Result<PamSession, Box<dyn Error>> {
let mut pch = Box::new(PamConvHandlerWrapper { handler: pam_conv });
pub fn start(
service: &str,
user: &'a str,
pam_conv: Pin<Box<dyn Converse + 'a>>,
) -> Result<PamSession<'a>, Box<dyn Error>> {
let mut pch = Box::pin(PamConvHandlerWrapper { handler: pam_conv });
let conv = make_conversation(&mut *pch);
let mut pam_handle: *mut PamHandle = ptr::null_mut();
match pam_sys::start(service, None, &conv, &mut pam_handle) {
match pam_sys::start(service, Some(user), &conv, &mut pam_handle) {
PamReturnCode::SUCCESS => Ok(PamSession {
handle: unsafe { &mut *pam_handle },
converse: pch,
lifetime_extender: pch,
last_code: PamReturnCode::SUCCESS,
}),
_ => Err(io::Error::new(io::ErrorKind::Other, "unable to start pam session").into()),

View File

@@ -5,51 +5,55 @@ use std::{
ffi::CString,
fs,
fs::File,
io::{BufRead, BufReader, Write},
os::unix::io::{AsRawFd, FromRawFd, RawFd},
io::{BufRead, BufReader},
path::PathBuf,
time::{Duration, Instant},
};
use nix::{
fcntl::{fcntl, FcntlArg},
sys::{
signal::{SigSet, Signal},
wait::{waitpid, WaitPidFlag, WaitStatus},
},
unistd::{close, execve, fork, initgroups, setgid, setsid, setuid, ForkResult, Gid, Pid, Uid},
unistd::{execve, fork, initgroups, setgid, setsid, setuid, ForkResult, Gid, Pid, Uid},
};
use pam_sys::{PamFlag, PamItemType};
use serde::{Deserialize, Serialize};
use users::{os::unix::UserExt, User};
use users::os::unix::UserExt;
use crate::{pam::converse::PasswordConv, pam::session::PamSession, terminal};
use crate::{pam::converse::Converse, pam::session::PamSession, terminal};
#[derive(Debug, Serialize, Deserialize)]
enum PamQuestionStyle {
PromptEchoOff,
PromptEchoOn,
ErrorMsg,
TextInfo,
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum QuestionStyle {
Visible,
Secret,
Info,
Error,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
enum ParentToSessionChild {
InitiateLogin { username: String },
PamResponse { resp: String, code: i32 },
InitiateLogin {
service: String,
class: String,
user: String,
vt: usize,
env: Vec<String>,
cmd: Vec<String>,
},
PamResponse {
resp: String,
},
Cancel,
Start,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
enum SessionChildToParent {
PamMessage { style: QuestionStyle, msg: String },
PamAuthSuccess,
Error { error: String },
FinalChildPid(u64),
PamMessage {
style: PamQuestionStyle,
msg: String,
},
LoginFinalized {
success: bool,
error_msg: String,
},
}
/// Returns a set containing the signals we want to block in the main process.
@@ -63,15 +67,9 @@ pub fn blocked_sigset() -> SigSet {
mask
}
fn dup_fd_cloexec(fd: RawFd) -> Result<RawFd, Box<dyn Error>> {
match fcntl(fd, FcntlArg::F_DUPFD_CLOEXEC(0)) {
Ok(fd) => Ok(fd),
Err(e) => Err(e.into()),
}
}
/// SessionChild tracks the processes spawned by a session
pub struct SessionChild {
service: String,
opened: Instant,
task: Pid,
sub_task: Pid,
@@ -94,6 +92,10 @@ impl SessionChild {
let _ = nix::sys::signal::kill(self.task, Signal::SIGKILL);
}
pub fn get_service(&self) -> &str {
&self.service
}
/// Terminate a session. Sends SIGTERM in a loop, then sends SIGKILL in a loop.
pub fn shoo(&self) {
let task = self.sub_task;
@@ -135,15 +137,12 @@ impl SessionChild {
}
/// A device to initiate a logged in PAM session.
pub struct Session<'a> {
pub struct Session {
service: String,
opened: Instant,
pam: PamSession<'a>,
user: User,
class: String,
vt: usize,
env: Vec<String>,
cmd: Vec<String>,
task: Pid,
sock: tokio::net::UnixDatagram,
last_msg: Option<SessionChildToParent>,
}
fn split_env<'a>(s: &'a str) -> Option<(&'a str, &str)> {
@@ -156,7 +155,373 @@ fn split_env<'a>(s: &'a str) -> Option<(&'a str, &str)> {
}
}
impl<'a> Session<'a> {
pub struct SessionConv<'a> {
sock: &'a std::os::unix::net::UnixDatagram,
}
impl<'a> SessionConv<'a> {
fn question(&self, msg: &str, style: QuestionStyle) -> Result<String, ()> {
let msg = SessionChildToParent::PamMessage {
style,
msg: msg.to_string(),
};
let data = serde_json::to_vec(&msg).map_err(|e| eprintln!("pam_conv: {}", e))?;
self.sock
.send(&data)
.map_err(|e| eprintln!("pam_conv: {}", e))?;
let mut data = [0; 1024];
let len = self
.sock
.recv(&mut data[..])
.map_err(|e| eprintln!("pam_conv: {}", e))?;
let msg = serde_json::from_slice(&data[..len]).map_err(|e| eprintln!("pam_conv: {}", e))?;
match msg {
ParentToSessionChild::PamResponse { resp, .. } => Ok(resp),
ParentToSessionChild::Cancel => Err(()),
_ => Err(()),
}
}
/// Create a new `PasswordConv` handler
pub fn new(sock: &'a std::os::unix::net::UnixDatagram) -> SessionConv {
SessionConv { sock }
}
}
impl<'a> Converse for SessionConv<'a> {
fn prompt_echo(&self, msg: &str) -> Result<String, ()> {
self.question(msg, QuestionStyle::Visible)
}
fn prompt_blind(&self, msg: &str) -> Result<String, ()> {
self.question(msg, QuestionStyle::Secret)
}
fn info(&self, msg: &str) -> Result<(), ()> {
self.question(msg, QuestionStyle::Info).map(|_| ())
}
fn error(&self, msg: &str) -> Result<(), ()> {
self.question(msg, QuestionStyle::Error).map(|_| ())
}
}
/// Process environment.d folders to generate the configured environment for
/// the session.
fn generate_user_environment(pam: &mut PamSession, home: String) -> Result<(), Box<dyn Error>> {
let dirs = [
"/usr/lib/environments.d/",
"/usr/local/lib/environments.d/",
"/run/environments.d/",
"/etc/environments.d/",
&format!("{}/.config/environment.d", home),
];
let mut env: HashMap<String, String> = HashMap::new();
for dir in dirs.iter() {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => continue,
};
let mut filepaths: Vec<PathBuf> =
entries.filter_map(Result::ok).map(|e| e.path()).collect();
filepaths.sort();
for filepath in filepaths.into_iter() {
let reader = BufReader::new(match File::open(&filepath) {
Ok(f) => f,
Err(_) => continue,
});
for line in reader.lines().filter_map(Result::ok) {
let (key, value) = match split_env(&line) {
Some(v) => v,
None => continue,
};
if key.starts_with('#') {
continue;
}
if !value.contains('$') {
env.insert(key.to_string(), value.to_string());
continue;
}
let reference = &value[1..];
let value = match env.get(reference) {
Some(v) => v.to_string(),
None => match pam.getenv(reference) {
Some(pam_val) => match split_env(pam_val) {
Some((_, new_value)) => new_value.to_string(),
None => "".to_string(),
},
None => "".to_string(),
},
};
env.insert(key.to_string(), value);
}
}
}
let env = env
.into_iter()
.map(|(key, value)| format!("{}={}", key, value));
for e in env {
pam.putenv(&e)
.map_err(|e| format!("unable to set PAM environment: {}", e))?;
}
Ok(())
}
/// The entry point for the session worker process. The session worker is
/// responsible for the entirety of the session setup and execution. It is
/// started by Session::start.
fn session_worker(sock: &std::os::unix::net::UnixDatagram) -> Result<(), Box<dyn Error>> {
let mut data = [0; 1024];
let len = sock
.recv(&mut data[..])
.map_err(|e| format!("unable to recieve message: {}", e))?;
let msg = serde_json::from_slice(&data[..len])
.map_err(|e| format!("unable to deserialize message: {}", e))?;
let (service, class, user, vt, env, cmd) = match msg {
ParentToSessionChild::InitiateLogin {
service,
class,
user,
vt,
env,
cmd,
} => (service, class, user, vt, env, cmd),
ParentToSessionChild::Cancel => return Err("cancelled".into()),
_ => return Err("unexpected message".into()),
};
let conv = Box::pin(SessionConv::new(sock));
let mut pam = PamSession::start(&service, &user, conv)
.map_err(|e| format!("unable to initiate PAM session: {}", e))?;
pam.authenticate(PamFlag::NONE)
.map_err(|e| format!("unable to authenticate account: {}", e))?;
pam.acct_mgmt(PamFlag::NONE)
.map_err(|e| format!("unable to validate account: {}", e))?;
let msg = SessionChildToParent::PamAuthSuccess;
let out =
serde_json::to_vec(&msg).map_err(|e| format!("unable to serialize message: {}", e))?;
sock.send(&out)
.map_err(|e| format!("unable to send message: {}", e))?;
let len = sock
.recv(&mut data[..])
.map_err(|e| format!("unable to recieve message: {}", e))?;
let msg = serde_json::from_slice(&data[..len])
.map_err(|e| format!("unable to deserialize message: {}", e))?;
match msg {
ParentToSessionChild::Start => (),
ParentToSessionChild::Cancel => return Err("cancelled".into()),
_ => return Err("unexpected message".into()),
}
let pam_username = pam.get_user()?;
let user = users::get_user_by_name(&pam_username).ok_or("unable to get user info")?;
// Clear the signal masking that was inherited from the parent.
blocked_sigset()
.thread_unblock()
.map_err(|e| format!("unable to unblock signals: {}", e))?;
// Make this process a session leader.
setsid().map_err(|e| format!("unable to become session leader: {}", e))?;
// Opening our target terminal. This will automatically make it our
// controlling terminal. An attempt was made to use TIOCSCTTY to do
// this explicitly, but it neither worked nor was worth the additional
// code.
let mut target_term = terminal::Terminal::open(vt)?;
// Clear TTY so that it will be empty when we switch to it.
target_term.term_clear()?;
// Set the target VT mode to text for compatibility. Other login
// managers set this to graphics, but that disallows start of textual
// applications, which greetd aims to support.
target_term.kd_setmode(terminal::KdMode::Text)?;
// A bit more work if a VT switch is required.
if vt != target_term.vt_get_current()? {
// Perform a switch to the target VT, simultaneously resetting it to
// VT_AUTO.
eprintln!("session worker: switching to VT {}", vt);
target_term.vt_setactivate(vt)?;
eprintln!("session worker: switch to VT {} complete", vt);
}
// Hook up std(in|out|err). This allows us to run console applications.
// Also, hooking up stdin is required, as applications otherwise fail to
// start, both for graphical and console-based applications. I do not
// know why this is the case.
target_term.term_connect_pipes()?;
// We no longer need these, so close them to avoid inheritance.
drop(target_term);
// Prepare some values from the user struct we gathered earlier.
let username = user.name().to_str().unwrap_or("");
let home = user.home_dir().to_str().unwrap_or("");
let shell = user.shell().to_str().unwrap_or("");
let uid = Uid::from_raw(user.uid());
let gid = Gid::from_raw(user.primary_group_id());
// PAM has to be provided a bunch of environment variables before
// open_session. We pass any environment variables from our greeter
// through here as well. This allows them to affect PAM (more
// specifically, pam_systemd.so), as well as make it easier to gather
// and set all environment variables later.
let prepared_env = [
"XDG_SEAT=seat0".to_string(),
format!("XDG_SESSION_CLASS={}", class),
format!("XDG_VTNR={}", vt),
format!("USER={}", username),
format!("LOGNAME={}", username),
format!("HOME={}", home),
format!("SHELL={}", shell),
];
for e in prepared_env.iter().chain(env.iter()) {
pam.putenv(e)
.map_err(|e| format!("unable to set PAM environment: {}", e))?;
}
// Tell PAM what TTY we're targetting, which is used by logind.
pam.set_item(PamItemType::TTY, &format!("/dev/tty{}", vt))
.map_err(|e| format!("unable to set PAM TTY item: {}", e))?;
// Not the credentials you think.
pam.setcred(PamFlag::ESTABLISH_CRED)
.map_err(|e| format!("unable to establish PAM credentials: {}", e))?;
// Session time!
pam.open_session(PamFlag::NONE)
.map_err(|e| format!("unable to open PAM session: {}", e))?;
// Prepare some strings in C format that we'll need.
let cusername = CString::new(username)?;
let command = format!("[ -f /etc/profile ] && source /etc/profile; [ -f $HOME/.profile ] && source $HOME/.profile; exec {}", cmd.join(" "));
// Change working directory
let pwd = match env::set_current_dir(home) {
Ok(_) => home,
Err(_) => {
env::set_current_dir("/")
.map_err(|e| format!("unable to set working directory: {}", e))?;
"/"
}
};
// Check to see if a few necessary variables are there and patch things
// up as needed.
let mut fixup_env = vec![
format!("PWD={}", pwd),
format!("GREETD_SOCK={}", env::var("GREETD_SOCK").unwrap()),
];
if !pam.hasenv("TERM") {
fixup_env.push("TERM=linux".to_string());
}
if !pam.hasenv("XDG_RUNTIME_DIR") {
fixup_env.push(format!("XDG_RUNTIME_DIR=/run/user/{}", uid));
}
for e in fixup_env.into_iter() {
pam.putenv(&e)
.map_err(|e| format!("unable to set PAM environment: {}", e))?;
}
// We're almost done with our environment. Let's go through
// environment.d configuration to fix up the last bits.
let home = home.to_string();
generate_user_environment(&mut pam, home)?;
// Extract PAM environment for use with execve below.
let pamenvlist = pam
.getenvlist()
.map_err(|e| format!("unable to get PAM environment: {}", e))?;
let envvec = pamenvlist.to_vec();
// PAM is weird and gets upset if you exec from the process that opened
// the session, registering it automatically as a log-out. Thus, we must
// exec in a new child.
let child = match fork().map_err(|e| format!("unable to fork: {}", e))? {
ForkResult::Parent { child, .. } => child,
ForkResult::Child => {
// It is important that we do *not* return from here by
// accidentally using '?'. The process *must* exit from within
// this match arm.
// Drop privileges to target user
initgroups(&cusername, gid).expect("unable to init groups");
setgid(gid).expect("unable to set GID");
setuid(uid).expect("unable to set UID");
// Run
let cpath = CString::new("/bin/sh").unwrap();
execve(
&cpath,
&[
&cpath,
&CString::new("-c").unwrap(),
&CString::new(command).unwrap(),
],
&envvec,
)
.expect("unable to exec");
unreachable!("after exec");
}
};
// Signal the inner PID to the parent process.
let msg = SessionChildToParent::FinalChildPid(child.as_raw() as u64);
let data = serde_json::to_vec(&msg)?;
sock.send(&data)
.map_err(|e| format!("unable to send message: {}", e))?;
sock.shutdown(std::net::Shutdown::Both)
.map_err(|e| format!("unable to close pipe: {}", e))?;
// Wait for process to terminate, handling EINTR as necessary.
loop {
match waitpid(child, None) {
Err(nix::Error::Sys(nix::errno::Errno::EINTR)) => continue,
Err(e) => eprintln!("session: waitpid on inner child failed: {}", e),
Ok(_) => break,
}
}
// Close the session. This step requires root privileges to run, as it
// will result in various forms of login teardown (including unmounting
// home folders, telling logind that the session ended, etc.). This is
// why we cannot drop privileges in this process, but must do it in the
// inner-most child.
pam.close_session(PamFlag::NONE)
.map_err(|e| format!("unable to close PAM session: {}", e))?;
pam.setcred(PamFlag::DELETE_CRED)
.map_err(|e| format!("unable to clear PAM credentials: {}", e))?;
pam.end()
.map_err(|e| format!("unable to clear PAM credentials: {}", e))?;
Ok(())
}
pub enum SessionState {
Question(QuestionStyle, String),
Ready,
}
impl Session {
///
/// Create a Session object with details required to start the specified
/// session. Surceeds if the service accepts the credentials.
@@ -165,318 +530,14 @@ impl<'a> Session<'a> {
/// authentication attempt. If successful, this same PAM handle will later
/// be used to tart the session.
///
pub fn new(
service: &'a str,
pub async fn new(
service: &str,
class: &str,
username: &str,
password: &str,
user: &str,
cmd: Vec<String>,
provided_env: Vec<String>,
env: Vec<String>,
vt: usize,
) -> Result<Session<'a>, Box<dyn Error>> {
let pass_conv = Box::new(PasswordConv::new(username, password));
let mut pam_session = PamSession::start(service, pass_conv)?;
pam_session.authenticate(PamFlag::NONE)?;
pam_session.acct_mgmt(PamFlag::NONE)?;
let pam_username = pam_session.get_user()?;
let user = users::get_user_by_name(&pam_username).ok_or("unable to get user info")?;
// Return our description of this session.
Ok(Session {
opened: Instant::now(),
pam: pam_session,
class: class.to_string(),
env: provided_env,
user,
vt,
cmd,
})
}
/// Process environment.d folders to generate the configured environment for
/// the session.
fn generate_user_environment(&mut self, home: String) -> Result<(), Box<dyn Error>> {
let dirs = [
"/usr/lib/environments.d/",
"/usr/local/lib/environments.d/",
"/run/environments.d/",
"/etc/environments.d/",
&format!("{}/.config/environment.d", home),
];
let mut env: HashMap<String, String> = HashMap::new();
for dir in dirs.iter() {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => continue,
};
let mut filepaths: Vec<PathBuf> =
entries.filter_map(Result::ok).map(|e| e.path()).collect();
filepaths.sort();
for filepath in filepaths.into_iter() {
let reader = BufReader::new(match File::open(&filepath) {
Ok(f) => f,
Err(_) => continue,
});
for line in reader.lines().filter_map(Result::ok) {
let (key, value) = match split_env(&line) {
Some(v) => v,
None => continue,
};
if key.starts_with('#') {
continue;
}
if !value.contains('$') {
env.insert(key.to_string(), value.to_string());
continue;
}
let reference = &value[1..];
let value = match env.get(reference) {
Some(v) => v.to_string(),
None => match self.pam.getenv(reference) {
Some(pam_val) => match split_env(pam_val) {
Some((_, new_value)) => new_value.to_string(),
None => "".to_string(),
},
None => "".to_string(),
},
};
env.insert(key.to_string(), value);
}
}
}
let env = env
.into_iter()
.map(|(key, value)| format!("{}={}", key, value));
for e in env {
self.pam
.putenv(&e)
.map_err(|e| format!("unable to set PAM environment: {}", e))?;
}
Ok(())
}
/// The entry point for the session worker process. The session worker is
/// responsible for the entirety of the session setup and execution. It is
/// started by Session::start.
fn session_worker(
&mut self,
childfd: std::os::unix::net::UnixDatagram,
) -> Result<(), Box<dyn Error>> {
// Clear the signal masking that was inherited from the parent.
blocked_sigset()
.thread_unblock()
.map_err(|e| format!("unable to unblock signals: {}", e))?;
// Make this process a session leader.
setsid().map_err(|e| format!("unable to become session leader: {}", e))?;
// Opening our target terminal. This will automatically make it our
// controlling terminal. An attempt was made to use TIOCSCTTY to do
// this explicitly, but it neither worked nor was worth the additional
// code.
let mut target_term = terminal::Terminal::open(self.vt)?;
// Clear TTY so that it will be empty when we switch to it.
target_term.term_clear()?;
// Set the target VT mode to text for compatibility. Other login
// managers set this to graphics, but that disallows start of textual
// applications, which greetd aims to support.
target_term.kd_setmode(terminal::KdMode::Text)?;
// A bit more work if a VT switch is required.
if self.vt != target_term.vt_get_current()? {
// Perform a switch to the target VT, simultaneously resetting it to
// VT_AUTO.
eprintln!("session worker: switching to VT {}", self.vt);
target_term.vt_setactivate(self.vt)?;
eprintln!("session worker: switch to VT {} complete", self.vt);
}
// Hook up std(in|out|err). This allows us to run console applications.
// Also, hooking up stdin is required, as applications otherwise fail to
// start, both for graphical and console-based applications. I do not
// know why this is the case.
target_term.term_connect_pipes()?;
// We no longer need these, so close them to avoid inheritance.
drop(target_term);
// Prepare some values from the user struct we gathered earlier.
let username = self.user.name().to_str().unwrap_or("");
let home = self.user.home_dir().to_str().unwrap_or("");
let shell = self.user.shell().to_str().unwrap_or("");
let uid = Uid::from_raw(self.user.uid());
let gid = Gid::from_raw(self.user.primary_group_id());
// PAM has to be provided a bunch of environment variables before
// open_session. We pass any environment variables from our greeter
// through here as well. This allows them to affect PAM (more
// specifically, pam_systemd.so), as well as make it easier to gather
// and set all environment variables later.
let prepared_env = [
"XDG_SEAT=seat0".to_string(),
format!("XDG_SESSION_CLASS={}", self.class),
format!("XDG_VTNR={}", self.vt),
format!("USER={}", username),
format!("LOGNAME={}", username),
format!("HOME={}", home),
format!("SHELL={}", shell),
];
for e in prepared_env.iter().chain(self.env.iter()) {
self.pam
.putenv(e)
.map_err(|e| format!("unable to set PAM environment: {}", e))?;
}
// Tell PAM what TTY we're targetting, which is used by logind.
self.pam
.set_item(PamItemType::TTY, &format!("/dev/tty{}", self.vt))
.map_err(|e| format!("unable to set PAM TTY item: {}", e))?;
// Not the credentials you think.
self.pam
.setcred(PamFlag::ESTABLISH_CRED)
.map_err(|e| format!("unable to establish PAM credentials: {}", e))?;
// Session time!
self.pam
.open_session(PamFlag::NONE)
.map_err(|e| format!("unable to open PAM session: {}", e))?;
// Prepare some strings in C format that we'll need.
let cusername = CString::new(username)?;
let command = format!("[ -f /etc/profile ] && source /etc/profile; [ -f $HOME/.profile ] && source $HOME/.profile; exec {}", self.cmd.join(" "));
// Change working directory
let pwd = match env::set_current_dir(home) {
Ok(_) => home,
Err(_) => {
env::set_current_dir("/")
.map_err(|e| format!("unable to set working directory: {}", e))?;
"/"
}
};
// Check to see if a few necessary variables are there and patch things
// up as needed.
let mut fixup_env = vec![
format!("PWD={}", pwd),
format!("GREETD_SOCK={}", env::var("GREETD_SOCK").unwrap()),
];
if !self.pam.hasenv("TERM") {
fixup_env.push("TERM=linux".to_string());
}
if !self.pam.hasenv("XDG_RUNTIME_DIR") {
fixup_env.push(format!("XDG_RUNTIME_DIR=/run/user/{}", uid));
}
for e in fixup_env.into_iter() {
self.pam
.putenv(&e)
.map_err(|e| format!("unable to set PAM environment: {}", e))?;
}
// We're almost done with our environment. Let's go through
// environment.d configuration to fix up the last bits.
let home = home.to_string();
self.generate_user_environment(home)?;
// Extract PAM environment for use with execve below.
let pamenvlist = self
.pam
.getenvlist()
.map_err(|e| format!("unable to get PAM environment: {}", e))?;
let envvec = pamenvlist.to_vec();
// PAM is weird and gets upset if you exec from the process that opened
// the session, registering it automatically as a log-out. Thus, we must
// exec in a new child.
let child = match fork().map_err(|e| format!("unable to fork: {}", e))? {
ForkResult::Parent { child, .. } => child,
ForkResult::Child => {
// It is important that we do *not* return from here by
// accidentally using '?'. The process *must* exit from within
// this match arm.
// Drop privileges to target user
initgroups(&cusername, gid).expect("unable to init groups");
setgid(gid).expect("unable to set GID");
setuid(uid).expect("unable to set UID");
// Run
close(childfd.as_raw_fd()).expect("unable to close pipe");
let cpath = CString::new("/bin/sh").unwrap();
execve(
&cpath,
&[
&cpath,
&CString::new("-c").unwrap(),
&CString::new(command).unwrap(),
],
&envvec,
)
.expect("unable to exec");
unreachable!("after exec");
}
};
// Signal the inner PID to the parent process.
let msg = SessionChildToParent::FinalChildPid(child.as_raw() as u64);
let data = serde_json::to_vec(&msg)?;
eprintln!("sending: {:?}\n", data);
childfd
.send(&data)
.map_err(|e| format!("unable to send message: {}", e))?;
// close(childfd.as_raw_fd())
// .map_err(|e| format!("unable to close pipe: {}", e))?;
// Wait for process to terminate, handling EINTR as necessary.
loop {
match waitpid(child, None) {
Err(nix::Error::Sys(nix::errno::Errno::EINTR)) => continue,
Err(e) => eprintln!("session: waitpid on inner child failed: {}", e),
Ok(_) => break,
}
}
// Close the session. This step requires root privileges to run, as it
// will result in various forms of login teardown (including unmounting
// home folders, telling logind that the session ended, etc.). This is
// why we cannot drop privileges in this process, but must do it in the
// inner-most child.
self.pam
.close_session(PamFlag::NONE)
.map_err(|e| format!("unable to close PAM session: {}", e))?;
self.pam
.setcred(PamFlag::DELETE_CRED)
.map_err(|e| format!("unable to clear PAM credentials: {}", e))?;
self.pam
.end()
.map_err(|e| format!("unable to clear PAM credentials: {}", e))?;
Ok(())
}
///
/// Start the session described within the Session.
///
pub async fn start(&mut self) -> Result<SessionChild, Box<dyn Error>> {
) -> Result<Session, Box<dyn Error>> {
// Pipe used to communicate the true PID of the final child.
let (parentfd, childfd) = std::os::unix::net::UnixDatagram::pair()
.map_err(|e| format!("could not create pipe: {}", e))?;
@@ -484,52 +545,116 @@ impl<'a> Session<'a> {
// PAM requires for unfathmoable reasons that we run this in a
// subprocess. Things seem to fail otherwise.
let child = match fork()? {
ForkResult::Parent { child, .. } => {
close(childfd.as_raw_fd())?;
child
}
ForkResult::Parent { child, .. } => child,
ForkResult::Child => {
// It is important that we do *not* return from here by
// accidentally using '?'. The process *must* exit from within
// this match arm.
// Close our side of the pipe.
close(parentfd.as_raw_fd()).expect("unable to close parent pipe");
// Keep our old stderr around for logging, but CLOEXEC so that
// we are not poluting the new child.
let mut stderr = unsafe { File::from_raw_fd(dup_fd_cloexec(2 as RawFd).unwrap()) };
// Run the child entrypoint.
if let Err(e) = self.session_worker(childfd) {
writeln!(stderr, "session worker: {}", e).expect("could not write log output");
if let Err(e) = session_worker(&childfd) {
let msg = SessionChildToParent::Error {
error: format!("{}", e),
};
let data = serde_json::to_vec(&msg).expect("unable to serialize message");
childfd.send(&data).expect("unable to send message");
std::process::exit(1);
}
std::process::exit(0);
}
};
let mut parentfd = tokio::net::UnixDatagram::from_std(parentfd)?;
let mut sock = tokio::net::UnixDatagram::from_std(parentfd)?;
// We have no use for the PAM handle in the host process anymore
self.pam.setcred(PamFlag::DELETE_CRED)?;
self.pam.end()?;
let msg = ParentToSessionChild::InitiateLogin {
service: service.to_string(),
class: class.to_string(),
user: user.to_string(),
vt,
env,
cmd,
};
let data = serde_json::to_vec(&msg)?;
sock.send(&data)
.await
.map_err(|e| format!("unable to send worker process request: {}", e))?;
Ok(Session {
service: service.to_string(),
opened: Instant::now(),
task: child,
sock,
last_msg: None,
})
}
pub async fn get_state(&mut self) -> Result<SessionState, Box<dyn Error>> {
let msg = match self.last_msg.take() {
Some(msg) => msg,
None => {
let mut data = [0u8; 1024];
let len = self.sock.recv(&mut data[..]).await?;
serde_json::from_slice(&data[..len])
.map_err(|e| format!("unable to read worker process response: {}", e))?
}
};
self.last_msg = Some(msg.clone());
match msg {
SessionChildToParent::PamMessage { style, msg } => {
Ok(SessionState::Question(style, msg))
}
SessionChildToParent::PamAuthSuccess => Ok(SessionState::Ready),
SessionChildToParent::Error { error } => Err(error.into()),
_ => Err("unexpected message from session worker".into()),
}
}
pub async fn post_answer(&mut self, answer: Option<String>) -> Result<(), Box<dyn Error>> {
self.last_msg = None;
let msg = match answer {
Some(resp) => ParentToSessionChild::PamResponse { resp },
None => ParentToSessionChild::Cancel,
};
let data = serde_json::to_vec(&msg)?;
self.sock.send(&data).await?;
Ok(())
}
///
/// Start the session described within the Session.
///
pub async fn start(&mut self) -> Result<SessionChild, Box<dyn Error>> {
let msg = ParentToSessionChild::Start;
let data = serde_json::to_vec(&msg)
.map_err(|e| format!("unable to serialize worker process request: {}", e))?;
self.sock
.send(&data)
.await
.map_err(|e| format!("unable to send worker process request: {}", e))?;
let mut data = [0u8; 1024];
let len = self
.sock
.recv(&mut data[..])
.await
.map_err(|e| format!("unable to receive worker process response: {}", e))?;
let msg = serde_json::from_slice(&data[..len])
.map_err(|e| format!("unable to deserialize worker process response: {}", e))?;
self.sock.shutdown(std::net::Shutdown::Both)?;
// Read the true child PID.
let mut data = Vec::new();
parentfd.recv(&mut data).await?;
eprintln!("got: {:?}\n", data);
// parentfd.shutdown(std::net::Shutdown::Write)?;
let msg = serde_json::from_slice(&data)
.map_err(|e| format!("unable to read worker process response: {}", e))?;
let sub_task = match msg {
SessionChildToParent::Error { error } => return Err(error.into()),
SessionChildToParent::FinalChildPid(raw_pid) => Pid::from_raw(raw_pid as i32),
_ => panic!("unexpected message"),
};
Ok(SessionChild {
service: self.service.to_string(),
opened: Instant::now(),
task: child,
task: self.task,
sub_task,
})
}