diff src/conv.rs @ 73:ac6881304c78

Do conversations, along with way too much stuff. This implements conversations, along with all the memory management brouhaha that goes along with it. The conversation now lives directly on the handle rather than being a thing you have to get from it and then call manually. It Turns Out this makes things a lot easier! I guess we reorganized things again. For the last time. For real. I promise. This all passes ASAN, so it seems Pretty Good!
author Paul Fisher <paul@pfish.zone>
date Thu, 05 Jun 2025 03:41:38 -0400
parents 47eb242a4f88
children c7c596e6388f
line wrap: on
line diff
--- a/src/conv.rs	Wed Jun 04 03:53:36 2025 -0400
+++ b/src/conv.rs	Thu Jun 05 03:41:38 2025 -0400
@@ -4,8 +4,7 @@
 #![allow(dead_code)]
 
 use crate::constants::Result;
-use crate::pam_ffi::LibPamConversation;
-use crate::pam_ffi::Message;
+use crate::ErrorCode;
 use secure_string::SecureString;
 // TODO: In most cases, we should be passing around references to strings
 // or binary data. Right now we don't because that turns type inference and
@@ -15,6 +14,47 @@
 // associated types in the various Conversation traits to avoid copying
 // when unnecessary.
 
+/// 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.
+#[derive(Clone, Copy, Debug)]
+pub enum Message<'a> {
+    /// Requests information from the user; will be masked when typing.
+    ///
+    /// Response: [`MaskedText`](Response::MaskedText)
+    MaskedPrompt(&'a str),
+    /// Requests information from the user; will not be masked.
+    ///
+    /// Response: [`Text`](Response::Text)
+    Prompt(&'a str),
+    /// "Yes/No/Maybe conditionals" (a Linux-PAM extension).
+    ///
+    /// Response: [`Text`](Response::Text)
+    /// (Linux-PAM documentation doesn't define its contents.)
+    RadioPrompt(&'a str),
+    /// Raises an error message to the user.
+    ///
+    /// Response: [`NoResponse`](Response::NoResponse)
+    Error(&'a str),
+    /// Sends an informational message to the user.
+    ///
+    /// Response: [`NoResponse`](Response::NoResponse)
+    Info(&'a str),
+    /// Requests binary data from the client (a Linux-PAM extension).
+    ///
+    /// This is used for non-human or non-keyboard prompts (security key?).
+    /// NOT part of the X/Open PAM specification.
+    ///
+    /// Response: [`Binary`](Response::Binary)
+    BinaryPrompt {
+        /// Some binary data.
+        data: &'a [u8],
+        /// A "type" that you can use for signalling. Has no strict definition in PAM.
+        data_type: u8,
+    },
+}
+
 /// The responses that PAM will return from a request.
 #[derive(Debug, PartialEq, derive_more::From)]
 pub enum Response {
@@ -46,6 +86,15 @@
     Binary(BinaryData),
 }
 
+/// The function type for a conversation.
+///
+/// A macro to save typing `FnMut(&[Message]) -> Result<Vec<Response>>`.
+#[macro_export]
+macro_rules! conv_type {
+    () => {FnMut(&[Message]) -> Result<Vec<Response>>};
+    (impl) => { impl FnMut(&[Message]) -> Result<Vec<Response>> }
+}
+
 /// A channel for PAM modules to request information from the user.
 ///
 /// This trait is used by both applications and PAM modules:
@@ -59,7 +108,102 @@
     ///
     /// The returned Vec of messages always contains exactly as many entries
     /// as there were messages in the request; one corresponding to each.
-    fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>>;
+    fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>>;
+}
+
+fn conversation_func(func: conv_type!(impl)) -> impl Conversation {
+    Convo(func)
+}
+
+struct Convo<C: FnMut(&[Message]) -> Result<Vec<Response>>>(C);
+
+impl<C: FnMut(&[Message]) -> Result<Vec<Response>>> Conversation for Convo<C> {
+    fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
+        self.0(messages)
+    }
+}
+
+/// Provides methods to make it easier to send exactly one message.
+///
+/// This is primarily used by PAM modules, so that a module that only needs
+/// one piece of information at a time doesn't have a ton of boilerplate.
+/// You may also find it useful for testing PAM application libraries.
+///
+/// ```
+/// # use nonstick::{PamHandleModule, Conversation, Result};
+/// # fn _do_test(mut pam_handle: impl PamHandleModule) -> Result<()> {
+/// use nonstick::ConversationMux;
+///
+/// let token = pam_handle.masked_prompt("enter your one-time token")?;
+/// # Ok(())
+/// # }
+pub trait ConversationMux {
+    /// Prompts the user for something.
+    fn prompt(&mut self, request: &str) -> Result<String>;
+    /// Prompts the user for something, but hides what the user types.
+    fn masked_prompt(&mut self, request: &str) -> Result<SecureString>;
+    /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension).
+    ///
+    /// PAM documentation doesn't define the format of the response.
+    fn radio_prompt(&mut self, request: &str) -> Result<String>;
+    /// Alerts the user to an error.
+    fn error(&mut self, message: &str);
+    /// Sends an informational message to the user.
+    fn info(&mut self, message: &str);
+    /// Requests binary data from the user (a Linux-PAM extension).
+    fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>;
+}
+
+impl<C: Conversation> ConversationMux for C {
+    /// Prompts the user for something.
+    fn prompt(&mut self, request: &str) -> Result<String> {
+        let resp = self.converse(&[Message::Prompt(request)])?.pop();
+        match resp {
+            Some(Response::Text(s)) => Ok(s),
+            _ => Err(ErrorCode::ConversationError),
+        }
+    }
+
+    /// Prompts the user for something, but hides what the user types.
+    fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
+        let resp = self.converse(&[Message::MaskedPrompt(request)])?.pop();
+        match resp {
+            Some(Response::MaskedText(s)) => Ok(s),
+            _ => Err(ErrorCode::ConversationError),
+        }
+    }
+
+    /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension).
+    ///
+    /// PAM documentation doesn't define the format of the response.
+    fn radio_prompt(&mut self, request: &str) -> Result<String> {
+        let resp = self.converse(&[Message::RadioPrompt(request)])?.pop();
+        match resp {
+            Some(Response::Text(s)) => Ok(s),
+            _ => Err(ErrorCode::ConversationError),
+        }
+    }
+
+    /// Alerts the user to an error.
+    fn error(&mut self, message: &str) {
+        let _ = self.converse(&[Message::Error(message)]);
+    }
+
+    /// Sends an informational message to the user.
+    fn info(&mut self, message: &str) {
+        let _ = self.converse(&[Message::Info(message)]);
+    }
+
+    /// Requests binary data from the user (a Linux-PAM extension).
+    fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> {
+        let resp = self
+            .converse(&[Message::BinaryPrompt { data, data_type }])?
+            .pop();
+        match resp {
+            Some(Response::Binary(d)) => Ok(d),
+            _ => Err(ErrorCode::ConversationError),
+        }
+    }
 }
 
 /// Trait that an application can implement if they want to handle messages
@@ -81,14 +225,11 @@
     fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>;
 }
 
-impl Conversation for LibPamConversation {
-    fn send(&mut self, _: &[Message]) -> Result<Vec<Response>> {
-        todo!()
-    }
-}
-
-impl<D: DemuxedConversation> Conversation for D {
-    fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
+impl<DM> Conversation for DM
+where
+    DM: DemuxedConversation,
+{
+    fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
         messages
             .iter()
             .map(|msg| match *msg {
@@ -195,7 +336,7 @@
                 Response::NoResponse,
             ],
             tester
-                .send(&[
+                .converse(&[
                     Message::Prompt("what"),
                     Message::MaskedPrompt("reveal"),
                     Message::Error("whoopsie"),
@@ -208,7 +349,7 @@
 
         assert_eq!(
             ErrorCode::PermissionDenied,
-            tester.send(&[Message::Prompt("give_err")]).unwrap_err(),
+            tester.converse(&[Message::Prompt("give_err")]).unwrap_err(),
         );
 
         // Test the Linux-PAM extensions separately.
@@ -219,7 +360,7 @@
                 Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)),
             ],
             tester
-                .send(&[
+                .converse(&[
                     Message::RadioPrompt("channel?"),
                     Message::BinaryPrompt {
                         data: &[10, 9, 8],
@@ -229,4 +370,80 @@
                 .unwrap()
         );
     }
+
+    #[test]
+    fn test_mux() {
+        use super::ConversationMux;
+        struct MuxTester;
+
+        impl Conversation for MuxTester {
+            fn converse(&mut self, messages: &[Message]) -> crate::Result<Vec<Response>> {
+                if let [msg] = messages {
+                    match msg {
+                        Message::Info(info) => {
+                            assert_eq!("let me tell you", *info);
+                            Ok(vec![Response::NoResponse])
+                        }
+                        Message::Error(error) => {
+                            assert_eq!("oh no", *error);
+                            Ok(vec![Response::NoResponse])
+                        }
+                        Message::Prompt("should_error") => Err(ErrorCode::BufferError),
+                        Message::Prompt(ask) => {
+                            assert_eq!("question", *ask);
+                            Ok(vec![Response::Text("answer".to_owned())])
+                        }
+                        Message::MaskedPrompt("return_wrong_type") => {
+                            Ok(vec![Response::NoResponse])
+                        }
+                        Message::MaskedPrompt(ask) => {
+                            assert_eq!("password!", *ask);
+                            Ok(vec![Response::MaskedText(SecureString::from(
+                                "open sesame",
+                            ))])
+                        }
+                        Message::BinaryPrompt { data, data_type } => {
+                            assert_eq!(&[1, 2, 3], data);
+                            assert_eq!(69, *data_type);
+                            Ok(vec![Response::Binary(super::BinaryData::new(
+                                vec![3, 2, 1],
+                                42,
+                            ))])
+                        }
+                        Message::RadioPrompt(ask) => {
+                            assert_eq!("radio?", *ask);
+                            Ok(vec![Response::Text("yes".to_owned())])
+                        }
+                    }
+                } else {
+                    panic!("messages is the wrong size ({len})", len = messages.len())
+                }
+            }
+        }
+
+        let mut tester = MuxTester;
+
+        assert_eq!("answer", tester.prompt("question").unwrap());
+        assert_eq!(
+            SecureString::from("open sesame"),
+            tester.masked_prompt("password!").unwrap()
+        );
+        tester.error("oh no");
+        tester.info("let me tell you");
+        {
+            assert_eq!("yes", tester.radio_prompt("radio?").unwrap());
+            assert_eq!(
+                super::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()
+        )
+    }
 }