diff src/conv.rs @ 69:8f3ae0c7ab92

Rework conversation data types and make safe wrappers. This removes the old `Conversation` type and reworks the FFI types used for PAM conversations. This creates safe `TestResponse` and `BinaryResponse` structures in `conv`, providing a safe way to pass response messages to PAM Conversations. The internals of these types are allocated on the C heap, as required by PAM. We also remove the Conversation struct, which was specific to the real PAM implementation so that we can introduce a better abstraction. Also splits a new `PamApplicationHandle` trait from `PamHandle`, for the parts of a PAM handle that are specific to the application side of a PAM transaction.
author Paul Fisher <paul@pfish.zone>
date Sun, 01 Jun 2025 01:15:04 -0400
parents bbe84835d6db
children 9f8381a1c09c
line wrap: on
line diff
--- a/src/conv.rs	Tue May 27 16:40:49 2025 -0400
+++ b/src/conv.rs	Sun Jun 01 01:15:04 2025 -0400
@@ -1,123 +1,81 @@
-//! The [Conversation] struct, for interacting with the user.
-//!
-//! This module is experimental and will probably be rewritten in the future
-//! to improve the interface for both PAM modules and clients.
+//! The PAM conversation and associated Stuff.
 
-use crate::constants::MessageStyle;
-use crate::constants::Result;
-use crate::constants::{ErrorCode, InvalidEnum};
-use crate::items::Item;
-use libc::{c_char, c_int};
-use num_derive::FromPrimitive;
-use std::ffi::{CStr, CString};
-use std::ptr;
+use crate::pam_ffi::{BinaryResponseInner, NulError, TextResponseInner};
+use std::num::TryFromIntError;
+use std::ops::Deref;
+use std::result::Result as StdResult;
 
-/// Styles of message that are shown to the user.
-#[derive(Debug, PartialEq, FromPrimitive)]
-#[non_exhaustive] // non-exhaustive because C might give us back anything!
-pub enum MessageStyle {
-    /// Requests information from the user; will be masked when typing.
-    PromptEchoOff = 1,
-    /// Requests information from the user; will not be masked.
-    PromptEchoOn = 2,
-    /// An error message.
-    ErrorMsg = 3,
-    /// An informational message.
-    TextInfo = 4,
-    /// Yes/No/Maybe conditionals. Linux-PAM specific.
-    RadioType = 5,
-    /// For server–client non-human interaction.
-    /// NOT part of the X/Open PAM specification.
-    BinaryPrompt = 7,
-}
+/// An owned text response to a PAM conversation.
+///
+/// It points to a value on the C heap.
+#[repr(C)]
+struct TextResponse(*mut TextResponseInner);
 
-impl TryFrom<c_int> for MessageStyle {
-    type Error = InvalidEnum<Self>;
-    fn try_from(value: c_int) -> std::result::Result<Self, Self::Error> {
-        Self::from_i32(value).ok_or(value.into())
+impl TextResponse {
+    /// Creates a text response.
+    pub fn new(text: impl AsRef<str>) -> StdResult<Self, NulError> {
+        TextResponseInner::alloc(text).map(Self)
     }
 }
 
-impl From<MessageStyle> for c_int {
-    fn from(val: MessageStyle) -> Self {
-        val as Self
+impl Deref for TextResponse {
+    type Target = TextResponseInner;
+    fn deref(&self) -> &Self::Target {
+        // SAFETY: We allocated this ourselves, or it was provided by PAM.
+        unsafe { &*self.0 }
+    }
+}
+
+impl Drop for TextResponse {
+    /// Frees an owned response.
+    fn drop(&mut self) {
+        // SAFETY: We allocated this ourselves, or it was provided by PAM.
+        unsafe { TextResponseInner::free(self.0) }
     }
 }
 
-#[repr(C)]
-struct Message {
-    msg_style: MessageStyle,
-    msg: *const c_char,
-}
-
+/// An owned binary response to a PAM conversation.
+///
+/// It points to a value on the C heap.
 #[repr(C)]
-struct Response {
-    resp: *const c_char,
-    resp_retcode: libc::c_int, // Unused - always zero
-}
+struct BinaryResponse(*mut BinaryResponseInner);
 
-#[doc(hidden)]
-#[repr(C)]
-pub struct Inner {
-    conv: extern "C" fn(
-        num_msg: c_int,
-        pam_message: &&Message,
-        pam_response: &mut *const Response,
-        appdata_ptr: *const libc::c_void,
-    ) -> c_int,
-    appdata_ptr: *const libc::c_void,
+impl BinaryResponse {
+    /// Creates a binary response with the given data.
+    pub fn new(data: impl AsRef<[u8]>, data_type: u8) -> StdResult<Self, TryFromIntError> {
+        BinaryResponseInner::alloc(data, data_type).map(Self)
+    }
 }
 
-/// A communication channel with the user.
-///
-/// Use this to communicate with the user, if needed, beyond the standard
-/// things you can get/set with `get_user`/`get_authtok` and friends.
-/// The PAM client (i.e., the application that is logging in) will present
-/// the messages you send to the user and ask for responses.
-pub struct Conversation<'a>(&'a Inner);
-
-impl Conversation<'_> {
-    /// Sends a message to the PAM client.
-    ///
-    /// This will typically result in the user seeing a message or a prompt.
-    /// For details, see what [MessageStyle]s are available.
-    ///
-    /// Note that the user experience will depend on how each style
-    /// is implemented by the client, and that not all clients
-    /// will implement all message styles.
-    pub fn send(&self, style: MessageStyle, msg: &str) -> Result<Option<&CStr>> {
-        let mut resp_ptr: *const Response = ptr::null();
-        let msg_cstr = CString::new(msg).unwrap();
-        let msg = Message {
-            msg_style: style,
-            msg: msg_cstr.as_ptr(),
-        };
-        // TODO: These need to be freed!
-        let ret = (self.0.conv)(1, &&msg, &mut resp_ptr, self.0.appdata_ptr);
-        ErrorCode::result_from(ret)?;
-
-        let result = unsafe {
-            match (*resp_ptr).resp {
-                p if p.is_null() => None,
-                p => Some(CStr::from_ptr(p)),
-            }
-        };
-        Ok(result)
+impl Deref for BinaryResponse {
+    type Target = BinaryResponseInner;
+    fn deref(&self) -> &Self::Target {
+        // SAFETY: We allocated this ourselves, or it was provided by PAM.
+        unsafe { &*self.0 }
     }
 }
 
-impl Item for Conversation<'_> {
-    type Raw = Inner;
-
-    fn type_id() -> crate::items::ItemType {
-        crate::items::ItemType::Conversation
-    }
-
-    unsafe fn from_raw(raw: *const Self::Raw) -> Self {
-        Self(&*raw)
-    }
-
-    fn into_raw(self) -> *const Self::Raw {
-        self.0 as _
+impl Drop for BinaryResponse {
+    /// Frees an owned response.
+    fn drop(&mut self) {
+        // SAFETY: We allocated this ourselves, or it was provided by PAM.
+        unsafe { BinaryResponseInner::free(self.0) }
     }
 }
+
+#[cfg(test)]
+mod test {
+    use super::{BinaryResponse, TextResponse};
+
+    #[test]
+    fn test_text_response() {
+        let resp = TextResponse::new("it's a-me!").unwrap();
+        assert_eq!("it's a-me!", resp.contents().to_str().unwrap());
+    }
+    #[test]
+    fn test_binary_response() {
+        let data = [123, 210, 55];
+        let resp = BinaryResponse::new(&data, 99).unwrap();
+        assert_eq!(&data, resp.contents());
+    }
+}