Mercurial > crates > nonstick
view src/conv.rs @ 98:b87100c5eed4
Start on environment variables, and make pointers nicer.
This starts work on the PAM environment handling, and in so doing,
introduces the CHeapBox and CHeapString structs. These are analogous
to Box and CString, but they're located on the C heap rather than
being Rust-managed memory.
This is because environment variables deal with even more pointers
and it turns out we can lose a lot of manual freeing using homemade
smart pointers.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 24 Jun 2025 04:25:25 -0400 |
parents | f3e260f9ddcb |
children |
line wrap: on
line source
//! The PAM conversation and associated Stuff. // Temporarily allowed until we get the actual conversation functions hooked up. #![allow(dead_code)] use crate::constants::{ErrorCode, Result}; use std::cell::Cell; use std::fmt; use std::fmt::Debug; use std::result::Result as StdResult; /// The types of message and request that can be sent to a user. /// /// The data within each enum value is the prompt (or other information) /// that will be presented to the user. #[non_exhaustive] pub enum Message<'a> { Prompt(&'a QAndA<'a>), MaskedPrompt(&'a MaskedQAndA<'a>), Error(&'a ErrorMsg<'a>), Info(&'a InfoMsg<'a>), RadioPrompt(&'a RadioQAndA<'a>), BinaryPrompt(&'a BinaryQAndA<'a>), } impl Message<'_> { /// Sets an error answer on this question, without having to inspect it. /// /// Use this as a default match case: /// /// ``` /// use nonstick::conv::{Message, QAndA}; /// use nonstick::ErrorCode; /// /// fn cant_respond(message: Message) { /// match message { /// Message::Info(i) => { /// eprintln!("fyi, {}", i.question()); /// i.set_answer(Ok(())) /// } /// Message::Error(e) => { /// eprintln!("ERROR: {}", e.question()); /// e.set_answer(Ok(())) /// } /// // We can't answer any questions. /// other => other.set_error(ErrorCode::ConversationError), /// } /// } pub fn set_error(&self, err: ErrorCode) { match *self { Message::Prompt(m) => m.set_answer(Err(err)), Message::MaskedPrompt(m) => m.set_answer(Err(err)), Message::Error(m) => m.set_answer(Err(err)), Message::Info(m) => m.set_answer(Err(err)), Message::RadioPrompt(m) => m.set_answer(Err(err)), Message::BinaryPrompt(m) => m.set_answer(Err(err)), } } } macro_rules! q_and_a { ($(#[$m:meta])* $name:ident<'a, Q=$qt:ty, A=$at:ty>, $val:path) => { $(#[$m])* pub struct $name<'a> { q: $qt, a: Cell<Result<$at>>, } $(#[$m])* impl<'a> $name<'a> { #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")] pub fn new(question: $qt) -> Self { Self { q: question, a: Cell::new(Err(ErrorCode::ConversationError)), } } /// Converts this Q&A into a [`Message`] for the [`Conversation`]. pub fn message(&self) -> Message<'_> { $val(self) } /// The contents of the question being asked. /// /// For instance, this might say `"Username:"` to prompt the user /// for their name, or the text of an error message. pub fn question(&self) -> $qt { self.q } /// Sets the answer to the question. /// /// The [`Conversation`] implementation calls this to set the answer. /// The conversation should *always call this function*, /// even for Q&A messages that don't have "an answer" /// (like error or info messages). pub fn set_answer(&self, answer: Result<$at>) { self.a.set(answer) } /// Gets the answer to the question. pub fn answer(self) -> Result<$at> { self.a.into_inner() } } // shout out to stackoverflow user ballpointben for this lazy impl: // https://stackoverflow.com/a/78871280/39808 $(#[$m])* impl fmt::Debug for $name<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { #[derive(Debug)] struct $name<'a> { q: $qt } fmt::Debug::fmt(&$name { q: self.q }, f) } } }; } q_and_a!( /// A Q&A that asks the user for text and does not show it while typing. /// /// In other words, a password entry prompt. MaskedQAndA<'a, Q=&'a str, A=String>, Message::MaskedPrompt ); q_and_a!( /// A standard Q&A prompt that asks the user for text. /// /// This is the normal "ask a person a question" prompt. /// When the user types, their input will be shown to them. /// It can be used for things like usernames. QAndA<'a, Q=&'a str, A=String>, Message::Prompt ); q_and_a!( /// A Q&A for "radio button"–style data. (Linux-PAM extension) /// /// This message type is theoretically useful for "yes/no/maybe" /// questions, but nowhere in the documentation is it specified /// what the format of the answer will be, or how this should be shown. RadioQAndA<'a, Q=&'a str, A=String>, Message::RadioPrompt ); q_and_a!( /// Asks for binary data. (Linux-PAM extension) /// /// This sends a binary message to the client application. /// It can be used to communicate with non-human logins, /// or to enable things like security keys. /// /// The `data_type` tag is a value that is simply passed through /// to the application. PAM does not define any meaning for it. BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>, Message::BinaryPrompt ); /// Owned binary data. #[derive(Debug, Default, PartialEq)] pub struct BinaryData { /// The data. pub data: Vec<u8>, /// A tag describing the type of the data, to use how you please. pub data_type: u8, } impl BinaryData { /// Creates a `BinaryData` with the given contents and type. pub fn new(data: impl Into<Vec<u8>>, data_type: u8) -> Self { Self { data: data.into(), data_type, } } } impl<IV: Into<Vec<u8>>> From<(IV, u8)> for BinaryData { /// Makes a new BinaryData from borrowed data. fn from((data, data_type): (IV, u8)) -> Self { Self { data: data.into(), data_type, } } } impl From<BinaryData> for (Vec<u8>, u8) { /// Easy destructuring. fn from(value: BinaryData) -> Self { (value.data, value.data_type) } } impl<'a> From<&'a BinaryData> for (&'a [u8], u8) { fn from(value: &'a BinaryData) -> Self { (&value.data, value.data_type) } } q_and_a!( /// A message containing information to be passed to the user. /// /// While this does not have an answer, [`Conversation`] implementations /// should still call [`set_answer`][`QAndA::set_answer`] to verify that /// the message has been displayed (or actively discarded). InfoMsg<'a, Q = &'a str, A = ()>, Message::Info ); q_and_a!( /// An error message to be passed to the user. /// /// While this does not have an answer, [`Conversation`] implementations /// should still call [`set_answer`][`QAndA::set_answer`] to verify that /// the message has been displayed (or actively discarded). ErrorMsg<'a, Q = &'a str, A = ()>, Message::Error ); /// A channel for PAM modules to request information from the user. /// /// This trait is used by both applications and PAM modules: /// /// - Applications implement Conversation and provide a user interface /// to allow the user to respond to PAM questions. /// - Modules call a Conversation implementation to request information /// or send information to the user. pub trait Conversation { /// Sends messages to the user. /// /// The returned Vec of messages always contains exactly as many entries /// as there were messages in the request; one corresponding to each. /// /// TODO: write detailed documentation about how to use this. fn communicate(&self, messages: &[Message]); } /// Turns a simple function into a [`Conversation`]. /// /// This can be used to wrap a free-floating function for use as a /// Conversation: /// /// ``` /// use nonstick::conv::{conversation_func, Conversation, Message}; /// mod some_library { /// # use nonstick::Conversation; /// pub fn get_auth_data(conv: &mut impl Conversation) { /// /* ... */ /// } /// } /// /// fn my_terminal_prompt(messages: &[Message]) { /// // ... /// # unimplemented!() /// } /// /// fn main() { /// some_library::get_auth_data(&mut conversation_func(my_terminal_prompt)); /// } /// ``` pub fn conversation_func(func: impl Fn(&[Message])) -> impl Conversation { FunctionConvo(func) } struct FunctionConvo<C: Fn(&[Message])>(C); impl<C: Fn(&[Message])> Conversation for FunctionConvo<C> { fn communicate(&self, messages: &[Message]) { self.0(messages) } } /// A Conversation struct UsernamePasswordConvo { username: String, password: String, } /// A conversation trait for asking or answering one question at a time. /// /// An implementation of this is provided for any [`Conversation`], /// or a PAM application can implement this trait and handle messages /// one at a time. /// /// For example, to use a `Conversation` as a `ConversationAdapter`: /// /// ``` /// # use nonstick::{Conversation, Result}; /// // Bring this trait into scope to get `masked_prompt`, among others. /// use nonstick::ConversationAdapter; /// /// fn ask_for_token(convo: &impl Conversation) -> Result<String> { /// convo.masked_prompt("enter your one-time token") /// } /// ``` /// /// or to use a `ConversationAdapter` as a `Conversation`: /// /// ``` /// use nonstick::{Conversation, ConversationAdapter}; /// # use nonstick::{BinaryData, Result}; /// mod some_library { /// # use nonstick::Conversation; /// pub fn get_auth_data(conv: &impl Conversation) { /* ... */ /// } /// } /// /// struct MySimpleConvo {/* ... */} /// # impl MySimpleConvo { fn new() -> Self { Self{} } } /// /// impl ConversationAdapter for MySimpleConvo { /// // ... /// # fn prompt(&self, request: &str) -> Result<String> { /// # unimplemented!() /// # } /// # /// # fn masked_prompt(&self, request: &str) -> Result<String> { /// # unimplemented!() /// # } /// # /// # fn error_msg(&self, message: &str) { /// # unimplemented!() /// # } /// # /// # fn info_msg(&self, message: &str) { /// # unimplemented!() /// # } /// # /// # fn radio_prompt(&self, request: &str) -> Result<String> { /// # unimplemented!() /// # } /// # /// # fn binary_prompt(&self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> { /// # unimplemented!() /// # } /// } /// /// fn main() { /// let mut simple = MySimpleConvo::new(); /// some_library::get_auth_data(&mut simple.into_conversation()) /// } /// ``` pub trait ConversationAdapter { /// Lets you use this simple conversation as a full [Conversation]. /// /// The wrapper takes each message received in [`Conversation::communicate`] /// and passes them one-by-one to the appropriate method, /// then collects responses to return. fn into_conversation(self) -> Demux<Self> where Self: Sized, { Demux(self) } /// Prompts the user for something. fn prompt(&self, request: &str) -> Result<String>; /// Prompts the user for something, but hides what the user types. fn masked_prompt(&self, request: &str) -> Result<String>; /// Alerts the user to an error. fn error_msg(&self, message: &str); /// Sends an informational message to the user. fn info_msg(&self, message: &str); /// \[Linux extension] Prompts the user for a yes/no/maybe conditional. /// /// PAM documentation doesn't define the format of the response. /// /// When called on an implementation that doesn't support radio prompts, /// this will return [`ErrorCode::ConversationError`]. /// If implemented on an implementation that doesn't support radio prompts, /// this will never be called. fn radio_prompt(&self, request: &str) -> Result<String> { let _ = request; Err(ErrorCode::ConversationError) } /// \[Linux extension] Requests binary data from the user. /// /// When called on an implementation that doesn't support radio prompts, /// this will return [`ErrorCode::ConversationError`]. /// If implemented on an implementation that doesn't support radio prompts, /// this will never be called. fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { let _ = data_and_type; Err(ErrorCode::ConversationError) } } impl<CA: ConversationAdapter> From<CA> for Demux<CA> { fn from(value: CA) -> Self { Demux(value) } } macro_rules! conv_fn { ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { $(#[$m])* fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> { let prompt = <$msg>::new($($param),*); self.communicate(&[prompt.message()]); prompt.answer() } }; ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { $(#[$m])* fn $fn_name(&self, $($param: $pt),*) { self.communicate(&[<$msg>::new($($param),*).message()]); } }; } impl<C: Conversation> ConversationAdapter for C { conv_fn!(prompt(message: &str) -> String { QAndA }); conv_fn!(masked_prompt(message: &str) -> String { MaskedQAndA } ); conv_fn!(error_msg(message: &str) { ErrorMsg }); conv_fn!(info_msg(message: &str) { InfoMsg }); conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA }); conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA }); } /// A [`Conversation`] which asks the questions one at a time. /// /// This is automatically created by [`ConversationAdapter::into_conversation`]. pub struct Demux<CA: ConversationAdapter>(CA); impl<CA: ConversationAdapter> Demux<CA> { /// Gets the original Conversation out of this wrapper. fn into_inner(self) -> CA { self.0 } } impl<CA: ConversationAdapter> Conversation for Demux<CA> { fn communicate(&self, messages: &[Message]) { for msg in messages { match msg { Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())), Message::MaskedPrompt(prompt) => { prompt.set_answer(self.0.masked_prompt(prompt.question())) } Message::RadioPrompt(prompt) => { prompt.set_answer(self.0.radio_prompt(prompt.question())) } Message::Info(prompt) => { self.0.info_msg(prompt.question()); prompt.set_answer(Ok(())) } Message::Error(prompt) => { self.0.error_msg(prompt.question()); prompt.set_answer(Ok(())) } Message::BinaryPrompt(prompt) => { let q = prompt.question(); prompt.set_answer(self.0.binary_prompt(q)) } } } } } #[cfg(test)] mod tests { use super::*; use crate::constants::ErrorCode; #[test] fn test_demux() { #[derive(Default)] struct DemuxTester { error_ran: Cell<bool>, info_ran: Cell<bool>, } impl ConversationAdapter for DemuxTester { fn prompt(&self, request: &str) -> Result<String> { match request { "what" => Ok("whatwhat".to_owned()), "give_err" => Err(ErrorCode::PermissionDenied), _ => panic!("unexpected prompt!"), } } fn masked_prompt(&self, request: &str) -> Result<String> { assert_eq!("reveal", request); Ok("my secrets".to_owned()) } fn error_msg(&self, message: &str) { self.error_ran.set(true); assert_eq!("whoopsie", message); } fn info_msg(&self, message: &str) { self.info_ran.set(true); assert_eq!("did you know", message); } fn radio_prompt(&self, request: &str) -> Result<String> { assert_eq!("channel?", request); Ok("zero".to_owned()) } fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { assert_eq!((&[10, 9, 8][..], 66), data_and_type); Ok(BinaryData::new(vec![5, 5, 5], 5)) } } let tester = DemuxTester::default(); let what = QAndA::new("what"); let pass = MaskedQAndA::new("reveal"); let err = ErrorMsg::new("whoopsie"); let info = InfoMsg::new("did you know"); let has_err = QAndA::new("give_err"); let conv = tester.into_conversation(); // Basic tests. conv.communicate(&[ what.message(), pass.message(), err.message(), info.message(), has_err.message(), ]); assert_eq!("whatwhat", what.answer().unwrap()); assert_eq!("my secrets", pass.answer().unwrap()); assert_eq!(Ok(()), err.answer()); assert_eq!(Ok(()), info.answer()); assert_eq!(ErrorCode::PermissionDenied, has_err.answer().unwrap_err()); let tester = conv.into_inner(); assert!(tester.error_ran.get()); assert!(tester.info_ran.get()); // Test the Linux extensions separately. { let conv = tester.into_conversation(); let radio = RadioQAndA::new("channel?"); let bin = BinaryQAndA::new((&[10, 9, 8], 66)); conv.communicate(&[radio.message(), bin.message()]); assert_eq!("zero", radio.answer().unwrap()); assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap()); } } fn test_mux() { struct MuxTester; impl Conversation for MuxTester { fn communicate(&self, messages: &[Message]) { if let [msg] = messages { match *msg { Message::Info(info) => { assert_eq!("let me tell you", info.question()); info.set_answer(Ok(())) } Message::Error(error) => { assert_eq!("oh no", error.question()); error.set_answer(Ok(())) } Message::Prompt(prompt) => prompt.set_answer(match prompt.question() { "should_err" => Err(ErrorCode::PermissionDenied), "question" => Ok("answer".to_owned()), other => panic!("unexpected question {other:?}"), }), Message::MaskedPrompt(ask) => { assert_eq!("password!", ask.question()); ask.set_answer(Ok("open sesame".into())) } Message::BinaryPrompt(prompt) => { assert_eq!((&[1, 2, 3][..], 69), prompt.question()); prompt.set_answer(Ok(BinaryData::from((&[3, 2, 1], 42)))) } Message::RadioPrompt(ask) => { assert_eq!("radio?", ask.question()); ask.set_answer(Ok("yes".to_owned())) } } } else { panic!( "there should only be one message, not {len}", len = messages.len() ) } } } let tester = MuxTester; assert_eq!("answer", tester.prompt("question").unwrap()); assert_eq!("open sesame", tester.masked_prompt("password!").unwrap()); tester.error_msg("oh no"); tester.info_msg("let me tell you"); // Linux-PAM extensions. Always implemented, but separate for clarity. { assert_eq!("yes", tester.radio_prompt("radio?").unwrap()); assert_eq!( BinaryData::new(vec![3, 2, 1], 42), tester.binary_prompt((&[1, 2, 3], 69)).unwrap(), ) } assert_eq!( ErrorCode::BufferError, tester.prompt("should_error").unwrap_err(), ); assert_eq!( ErrorCode::ConversationError, tester.masked_prompt("return_wrong_type").unwrap_err() ) } }