diff src/libpam/conversation.rs @ 75:c30811b4afae

rename pam_ffi submodule to libpam.
author Paul Fisher <paul@pfish.zone>
date Fri, 06 Jun 2025 22:35:08 -0400
parents src/pam_ffi/conversation.rs@c7c596e6388f
children 351bdc13005e
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/conversation.rs	Fri Jun 06 22:35:08 2025 -0400
@@ -0,0 +1,162 @@
+use crate::constants::Result;
+use crate::conv::{Conversation, Message, Response};
+use crate::libpam::memory::Immovable;
+use crate::libpam::message::{MessageIndirector, OwnedMessages};
+use crate::libpam::response::{OwnedResponses, RawBinaryResponse, RawResponse, RawTextResponse};
+use crate::ErrorCode;
+use crate::ErrorCode::ConversationError;
+use std::ffi::c_int;
+use std::iter;
+use std::marker::PhantomData;
+use std::result::Result as StdResult;
+
+/// An opaque structure that is passed through PAM in a conversation.
+#[repr(C)]
+pub struct AppData {
+    _data: (),
+    _marker: Immovable,
+}
+
+/// The callback that PAM uses to get information in a conversation.
+///
+/// - `num_msg` is the number of messages in the `pam_message` array.
+/// - `messages` is a pointer to the messages being sent to the user.
+///   For details about its structure, see the documentation of
+///   [`OwnedMessages`](super::OwnedMessages).
+/// - `responses` is a pointer to an array of [`RawResponse`]s,
+///   which PAM sets in response to a module's request.
+///   This is an array of structs, not an array of pointers to a struct.
+///   There should always be exactly as many `responses` as `num_msg`.
+/// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed.
+pub type ConversationCallback = unsafe extern "C" fn(
+    num_msg: c_int,
+    messages: *const MessageIndirector,
+    responses: *mut *mut RawResponse,
+    appdata: *mut AppData,
+) -> c_int;
+
+/// The type used by PAM to call back into a conversation.
+#[repr(C)]
+pub struct LibPamConversation<'a> {
+    /// The function that is called to get information from the user.
+    callback: ConversationCallback,
+    /// The pointer that will be passed as the last parameter
+    /// to the conversation callback.
+    appdata: *mut AppData,
+    life: PhantomData<&'a mut ()>,
+    _marker: Immovable,
+}
+
+impl LibPamConversation<'_> {
+    fn wrap<C: Conversation>(conv: &mut C) -> Self {
+        Self {
+            callback: Self::wrapper_callback::<C>,
+            appdata: (conv as *mut C).cast(),
+            life: PhantomData,
+            _marker: Immovable(PhantomData),
+        }
+    }
+
+    unsafe extern "C" fn wrapper_callback<C: Conversation>(
+        count: c_int,
+        messages: *const MessageIndirector,
+        responses: *mut *mut RawResponse,
+        me: *mut AppData,
+    ) -> c_int {
+        let call = || {
+            let conv = me
+                .cast::<C>()
+                .as_mut()
+                .ok_or(ErrorCode::ConversationError)?;
+            let indir = messages.as_ref().ok_or(ErrorCode::ConversationError)?;
+            let response_ptr = responses.as_mut().ok_or(ErrorCode::ConversationError)?;
+            let messages: Vec<Message> = indir
+                .iter(count as usize)
+                .map(Message::try_from)
+                .collect::<StdResult<_, _>>()
+                .map_err(|_| ErrorCode::ConversationError)?;
+            let responses = conv.communicate(&messages)?;
+            let owned =
+                OwnedResponses::build(&responses).map_err(|_| ErrorCode::ConversationError)?;
+            *response_ptr = owned.into_ptr();
+            Ok(())
+        };
+        ErrorCode::result_to_c(call())
+    }
+}
+
+impl Conversation for LibPamConversation<'_> {
+    fn communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
+        let mut msgs_to_send = OwnedMessages::alloc(messages.len());
+        for (dst, src) in iter::zip(msgs_to_send.iter_mut(), messages.iter()) {
+            dst.set(*src).map_err(|_| ErrorCode::ConversationError)?
+        }
+        let mut response_pointer = std::ptr::null_mut();
+        // SAFETY: We're calling into PAM with valid everything.
+        let result = unsafe {
+            (self.callback)(
+                messages.len() as c_int,
+                msgs_to_send.indirector(),
+                &mut response_pointer,
+                self.appdata,
+            )
+        };
+        ErrorCode::result_from(result)?;
+        // SAFETY: This is a pointer we just got back from PAM.
+        let owned_responses =
+            unsafe { OwnedResponses::from_c_heap(response_pointer, messages.len()) };
+        convert_responses(messages, owned_responses)
+    }
+}
+
+fn convert_responses(
+    messages: &[Message],
+    mut raw_responses: OwnedResponses,
+) -> Result<Vec<Response>> {
+    let pairs = iter::zip(messages.iter(), raw_responses.iter_mut());
+    // We first collect into a Vec of Results so that we always process
+    // every single entry, which may involve freeing it.
+    let responses: Vec<_> = pairs.map(convert).collect();
+    // Only then do we return the first error, if present.
+    responses.into_iter().collect()
+}
+
+/// Converts one message-to-raw pair to a Response.
+fn convert((sent, received): (&Message, &mut RawResponse)) -> Result<Response> {
+    Ok(match sent {
+        Message::MaskedPrompt(_) => {
+            // SAFETY: Since this is a response to a text message,
+            // we know it is text.
+            let text_resp = unsafe { RawTextResponse::upcast(received) };
+            let ret = Response::MaskedText(
+                text_resp
+                    .contents()
+                    .map_err(|_| ErrorCode::ConversationError)?
+                    .into(),
+            );
+            // SAFETY: We're the only ones using this,
+            // and we haven't freed it.
+            text_resp.free_contents();
+            ret
+        }
+        Message::Prompt(_) | Message::RadioPrompt(_) => {
+            // SAFETY: Since this is a response to a text message,
+            // we know it is text.
+            let text_resp = unsafe { RawTextResponse::upcast(received) };
+            let ret = Response::Text(text_resp.contents().map_err(|_| ConversationError)?.into());
+            // SAFETY: We're the only ones using this,
+            // and we haven't freed it.
+            text_resp.free_contents();
+            ret
+        }
+        Message::ErrorMsg(_) | Message::InfoMsg(_) => Response::NoResponse,
+        Message::BinaryPrompt { .. } => {
+            let bin_resp = unsafe { RawBinaryResponse::upcast(received) };
+            let ret = Response::Binary(bin_resp.to_owned());
+            // SAFETY: We're the only ones using this,
+            // and we haven't freed it.
+            bin_resp.free_contents();
+            ret
+        }
+    })
+}