use super::conversation::{OwnedConversation, PamConv};
use crate::_doc::{guide, linklist, man7, stdlinks};
use crate::constants::{ErrorCode, RawFlags, Result, ReturnCode};
use crate::conv::Exchange;
use crate::environ::EnvironMapMut;
use crate::handle::PamShared;
use crate::items::{Items, ItemsMut};
use crate::libpam::environ::{LibPamEnviron, LibPamEnvironMut};
use crate::libpam::items::{ItemType, LibPamItems, LibPamItemsMut};
use crate::libpam::{items, memory};
use crate::logging::{Level, Location, Logger};
use crate::{AuthnFlags, AuthtokFlags, Conversation, EnvironMap, ModuleClient, Transaction};
use std::any::TypeId;
use std::cell::Cell;
use std::ffi::{c_char, c_int, c_void, CString, OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::ptr::NonNull;
use std::{any, fmt, ptr};
pub struct LibPamTransaction<C: Conversation> {
handle: *mut LibPamHandle,
last_return: Cell<Result<()>>,
conversation: Box<OwnedConversation<C>>,
}
impl<C: Conversation> fmt::Debug for LibPamTransaction<C> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct(any::type_name::<Self>())
.field("handle", &format!("{:p}", self.handle))
.field("last_return", &self.last_return.get())
.field("conversation", &format!("{:p}", self.conversation))
.finish()
}
}
#[derive(Debug, PartialEq)]
pub struct TransactionBuilder {
service_name: OsString,
username: Option<OsString>,
}
impl TransactionBuilder {
#[doc = linklist!(pam_start: adg, _std)]
#[doc = stdlinks!(3 pam_start)]
#[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_start")]
pub fn new_with_service(service_name: impl AsRef<OsStr>) -> Self {
Self {
service_name: service_name.as_ref().into(),
username: None,
}
}
pub fn service_name(mut self, service_name: impl AsRef<OsStr>) -> Self {
self.service_name = service_name.as_ref().into();
self
}
pub fn username(mut self, username: impl AsRef<OsStr>) -> Self {
self.username = Some(username.as_ref().into());
self
}
pub fn build<C: Conversation>(self, conv: C) -> Result<LibPamTransaction<C>> {
LibPamTransaction::start(self.service_name, self.username, conv)
}
}
impl<C: Conversation> LibPamTransaction<C> {
fn start(service_name: OsString, username: Option<OsString>, conversation: C) -> Result<Self> {
let mut conv = Box::new(OwnedConversation::new(conversation));
let service_cstr = CString::new(service_name.as_bytes()).expect("null is forbidden");
let username_cstr = memory::option_cstr_os(username.as_deref());
let username_cstr = memory::prompt_ptr(username_cstr.as_deref());
let mut handle: *mut libpam_sys::pam_handle = ptr::null_mut();
let conv_ptr: *mut OwnedConversation<_> = conv.as_mut() as _;
let result = unsafe {
libpam_sys::pam_start(
service_cstr.as_ptr(),
username_cstr,
conv_ptr.cast(),
&mut handle,
)
};
ErrorCode::result_from(result)?;
let handle = NonNull::new(handle).ok_or(ErrorCode::BufferError)?;
Ok(Self {
handle: handle.as_ptr().cast(),
last_return: Cell::new(Ok(())),
conversation: conv,
})
}
#[cfg_attr(
pam_impl = "LinuxPam",
doc = "Ends the PAM transaction \"quietly\" (on Linux-PAM only)."
)]
#[cfg_attr(
not(pam_impl = "LinuxPam"),
doc = "Exactly equivalent to `drop(self)` (except on Linux-PAM)."
)]
#[doc = ""]
#[doc = man7!(3 pam_end)]
pub fn end_silent(self) {
#[cfg(pam_impl = "LinuxPam")]
{
let mut me = std::mem::ManuallyDrop::new(self);
me.end_internal(libpam_sys::PAM_DATA_SILENT);
}
}
fn end_internal(&mut self, or_with: c_int) {
let last: c_int = ReturnCode::from(self.last_return.get()).into();
let result = last | or_with;
unsafe { libpam_sys::pam_end(self.handle.cast(), result) };
}
}
macro_rules! wrap {
(fn $name:ident($ftype:ident) { $pam_func:ident }) => {
fn $name(&mut self, flags: $ftype) -> Result<()> {
let flags: RawFlags = flags.into();
ErrorCode::result_from(unsafe {
libpam_sys::$pam_func((self as *mut Self).cast(), flags.into())
})
}
};
}
impl Transaction for LibPamHandle {
wrap!(fn authenticate(AuthnFlags) { pam_authenticate });
wrap!(fn account_management(AuthnFlags) { pam_acct_mgmt });
wrap!(fn change_authtok(AuthtokFlags) { pam_chauthtok });
}
impl<C: Conversation> Drop for LibPamTransaction<C> {
#[doc = linklist!(pam_end: adg, _std)]
#[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
#[doc = stdlinks!(3 pam_end)]
fn drop(&mut self) {
self.end_internal(0)
}
}
macro_rules! delegate {
(fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => {
fn $meth(&self $(, $param: $typ)*) -> Result<$ret> {
let result = unsafe { &*self.handle }.$meth($($param),*);
self.last_return.set(split(&result));
result
}
};
(fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => {
fn $meth(&mut self $(, $param: $typ)*) -> Result<$ret> {
let result = unsafe { &mut *self.handle }.$meth($($param),*);
self.last_return.set(split(&result));
result
}
};
(fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> $ret:ty) => {
fn $meth(&self $(, $param: $typ)*) -> $ret {
unsafe { &*self.handle }.$meth($($param),*)
}
};
(fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> $ret:ty) => {
fn $meth(&mut self $(, $param: $typ)*) -> $ret {
unsafe { &mut *self.handle }.$meth($($param),*)
}
};
(get = $get:ident$(, set = $set:ident)?) => {
delegate!(fn $get(&self) -> Result<Option<OsString>>);
$(delegate!(set = $set);)?
};
(set = $set:ident) => {
delegate!(fn $set(&mut self, value: Option<&OsStr>) -> Result<()>);
};
}
fn split<T>(result: &Result<T>) -> Result<()> {
result.as_ref().map(drop).map_err(|&e| e)
}
impl<C: Conversation> Logger for LibPamTransaction<C> {
delegate!(fn log(&self, level: Level, location: Location<'_>, entry: fmt::Arguments) -> ());
}
impl<C: Conversation> Transaction for LibPamTransaction<C> {
delegate!(fn authenticate(&mut self, flags: AuthnFlags) -> Result<()>);
delegate!(fn account_management(&mut self, flags: AuthnFlags) -> Result<()>);
delegate!(fn change_authtok(&mut self, flags: AuthtokFlags) -> Result<()>);
}
impl<C: Conversation> PamShared for LibPamTransaction<C> {
delegate!(fn environ(&self) -> impl EnvironMap);
delegate!(fn environ_mut(&mut self) -> impl EnvironMapMut);
delegate!(fn items(&self) -> impl Items);
delegate!(fn items_mut(&mut self) -> impl ItemsMut);
}
#[repr(transparent)]
pub struct LibPamHandle(libpam_sys::pam_handle);
impl LibPamHandle {
#[doc = linklist!(pam_end: adg, _std)]
#[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
#[doc = stdlinks!(3 pam_end)]
pub fn end(&mut self, result: Result<()>) {
let code: ReturnCode = result.into();
unsafe { libpam_sys::pam_end(self.inner_mut(), code.into()) };
}
#[cfg_attr(
not(pam_impl = "LinuxPam"),
doc = "Exactly equivalent to [`Self::end`], except on Linux-PAM."
)]
#[cfg_attr(
pam_impl = "LinuxPam",
doc = "Ends the transaction \"quietly\", reporting the given result."
)]
#[doc = linklist!(pam_end: adg, _std)]
#[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
#[doc = stdlinks!(3 pam_end)]
pub fn end_silent(&mut self, result: Result<()>) {
let result: c_int = ReturnCode::from(result).into();
#[cfg(pam_impl = "LinuxPam")]
let result = result | libpam_sys::PAM_DATA_SILENT;
unsafe {
libpam_sys::pam_end(self.inner_mut(), result);
}
}
pub fn inner(&self) -> &libpam_sys::pam_handle {
&self.0
}
pub fn inner_mut(&mut self) -> &mut libpam_sys::pam_handle {
&mut self.0
}
}
impl Logger for LibPamHandle {
fn log(&self, level: Level, loc: Location<'_>, entry: fmt::Arguments) {
let entry = match CString::new(entry.to_string()).ok() {
Some(e) => e,
None => return,
};
#[cfg(any(pam_impl = "LinuxPam", pam_impl = "Sun"))]
{
let level = match level {
Level::Error => libc::LOG_ERR,
Level::Warn => libc::LOG_WARNING,
Level::Info => libc::LOG_INFO,
Level::Debug => libc::LOG_DEBUG,
};
_ = loc;
#[cfg(pam_impl = "LinuxPam")]
unsafe {
libpam_sys::pam_syslog(self.inner(), level, b"%s\0".as_ptr().cast(), entry.as_ptr())
}
#[cfg(pam_impl = "Sun")]
unsafe {
libpam_sys::__pam_log(level, b"%s\0".as_ptr().cast(), entry.as_ptr())
}
}
#[cfg(pam_impl = "OpenPam")]
{
let func = CString::new(loc.function).unwrap_or(CString::default());
let level = match level {
Level::Error => libpam_sys::PAM_LOG_ERROR,
Level::Warn => libpam_sys::PAM_LOG_NOTICE,
Level::Info => libpam_sys::PAM_LOG_VERBOSE,
Level::Debug => libpam_sys::PAM_LOG_DEBUG,
};
unsafe {
libpam_sys::_openpam_log(
level as c_int,
func.as_ptr(),
b"%s\0".as_ptr().cast(),
entry.as_ptr(),
)
}
}
}
}
impl PamShared for LibPamHandle {
fn environ(&self) -> impl EnvironMap {
LibPamEnviron::new(self)
}
fn environ_mut(&mut self) -> impl EnvironMapMut {
LibPamEnvironMut::new(self)
}
fn items(&self) -> impl Items {
LibPamItems(self)
}
fn items_mut(&mut self) -> impl ItemsMut {
LibPamItemsMut(self)
}
}
impl Conversation for LibPamHandle {
fn communicate(&self, messages: &[Exchange]) {
match self.conversation_item() {
Ok(conv) => conv.communicate(messages),
Err(e) => {
for msg in messages {
msg.set_error(e)
}
}
}
}
}
impl ModuleClient for LibPamHandle {
fn username(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
let prompt = memory::option_cstr_os(prompt);
let mut output: *const c_char = ptr::null();
let ret = unsafe {
libpam_sys::pam_get_user(
self.inner_mut(),
&mut output,
memory::prompt_ptr(prompt.as_deref()),
)
};
ErrorCode::result_from(ret)?;
Ok(unsafe { memory::copy_pam_string(output).ok_or(ErrorCode::ConversationError)? })
}
fn authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
self.get_authtok(prompt, ItemType::AuthTok)
}
fn old_authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString> {
self.get_authtok(prompt, ItemType::OldAuthTok)
}
fn get_module_data<T: 'static>(&self, key: &str) -> Option<&T> {
let full_key = module_data_key::<T>(key);
let mut ptr: *const c_void = ptr::null();
unsafe {
ErrorCode::result_from(libpam_sys::pam_get_data(
self.inner(),
full_key.as_ptr(),
&mut ptr,
))
.ok()?;
(ptr as *const T).as_ref()
}
}
fn set_module_data<T: 'static>(&mut self, key: &str, data: T) -> Result<()> {
let full_key = module_data_key::<T>(key);
let data = Box::new(data);
ErrorCode::result_from(unsafe {
libpam_sys::pam_set_data(
self.inner_mut(),
full_key.as_ptr(),
Box::into_raw(data).cast(),
drop_module_data::<T>,
)
})
}
fn authtok_item(&self) -> Result<Option<OsString>> {
unsafe { items::get_cstr_item(self, ItemType::AuthTok) }
}
fn old_authtok_item(&self) -> Result<Option<OsString>> {
unsafe { items::get_cstr_item(self, ItemType::OldAuthTok) }
}
}
fn module_data_key<T: 'static>(key: &str) -> CString {
let tid = TypeId::of::<T>();
let cleanup_addr = drop_module_data::<T> as usize;
let key = format!("{key:?}::{tid:?}::{cleanup_addr:016x}");
CString::new(key).expect("null bytes somehow got into a debug string?")
}
extern "C" fn drop_module_data<T>(_: *mut libpam_sys::pam_handle, c_data: *mut c_void, _: c_int) {
unsafe {
let _: Box<T> = Box::from_raw(c_data.cast());
}
}
impl LibPamHandle {
#[cfg(any(pam_impl = "LinuxPam", pam_impl = "OpenPam"))]
fn get_authtok(&mut self, prompt: Option<&OsStr>, item_type: ItemType) -> Result<OsString> {
let prompt = memory::option_cstr_os(prompt);
let mut output: *const c_char = ptr::null();
let res = unsafe {
libpam_sys::pam_get_authtok(
self.inner_mut(),
item_type.into(),
&mut output,
memory::prompt_ptr(prompt.as_deref()),
)
};
ErrorCode::result_from(res)?;
unsafe { memory::copy_pam_string(output) }.ok_or(ErrorCode::ConversationError)
}
#[cfg(pam_impl = "Sun")]
fn get_authtok(&mut self, _prompt: Option<&OsStr>, item_type: ItemType) -> Result<OsString> {
unsafe { items::get_cstr_item(self, item_type) }?.ok_or(ErrorCode::ConversationError)
}
#[cfg(not(any(pam_impl = "LinuxPam", pam_impl = "OpenPam", pam_impl = "Sun")))]
fn get_authtok(&mut self, _: Option<&OsStr>, _: ItemType) -> Result<OsString> {
Err(ErrorCode::ConversationError)
}
fn conversation_item(&self) -> Result<&PamConv> {
let mut output: *const c_void = ptr::null();
let result = unsafe {
libpam_sys::pam_get_item(self.inner(), ItemType::Conversation.into(), &mut output)
};
ErrorCode::result_from(result)?;
let output: *const PamConv = output.cast();
unsafe { output.as_ref() }.ok_or(ErrorCode::ConversationError)
}
}