1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Simple non-interactive conversation handler

/***********************************************************************
 * (c) 2021 Christoph Grenz <christophg+gitorious @ grenz-bonn.de>     *
 *                                                                     *
 * This Source Code Form is subject to the terms of the Mozilla Public *
 * License, v. 2.0. If a copy of the MPL was not distributed with this *
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.            *
 ***********************************************************************/

use std::iter::FusedIterator;
use std::ffi::{CStr, CString};
use std::vec;
use crate::error::ReturnCode;
use super::ConversationHandler;

/// Elements in [`Conversation::log`]
#[derive(Debug, Clone)]
pub enum LogEntry {
	Info(CString),
	Error(CString),
}

/// Non-interactive implementation of `ConversationHandler`
///
/// When a PAM module asks for a non-secret string, [`username`][`Self::username`]
/// will be returned and when a secret string is asked for,
/// [`password`][`Self::password`] will be returned.
///
/// All info and error messages will be recorded in [`log`][`Self::log`].
///
/// # Limitations
///
/// This is enough to handle many authentication flows non-interactively, but
/// flows with two-factor-authentication and things like
/// [`chauthok()`][`crate::Context::chauthtok()`] will most definitely fail.
///
/// Please also note that UTF-8 encoding is assumed for both username and
/// password, so this handler may fail to authenticate on legacy non-UTF-8
/// systems when one of the strings contains non-ASCII characters.
#[derive(Debug, Clone)]
pub struct Conversation {
	/// The username to use
	pub username: String,
	/// The password to use
	pub password: String,
	/// All received info/error messages
	pub log: vec::Vec<LogEntry>,
}

impl Conversation {
	/// Creates a new CLI conversation handler
	///
	/// If [`username`][`Self::username`] isn't manually set to a non-empty
	/// string, it will be automatically set to the `Context`s default
	/// username on context initialization.
	#[must_use]
	pub const fn new() -> Self {
		Self {
			username: String::new(),
			password: String::new(),
			log: vec::Vec::new()
		}
	}

	/// Creatse a new CLI conversation handler with preset credentials
	#[must_use]
	pub fn with_credentials(username: impl Into<String>, password: impl Into<String>) -> Self {
		Self {
			username: username.into(),
			password: password.into(),
			log: vec::Vec::new()
		}
	}

	/// Clears the error/info log
	pub fn clear_log(&mut self) {
		self.log.clear();
	}

	/// Lists only errors from the log
	pub fn errors(&self) -> impl Iterator<Item=&CString> + FusedIterator {
		self.log.iter().filter_map(|x| match x {
			LogEntry::Info(_) => None,
			LogEntry::Error(msg) => Some(msg)
		})
	}

	/// Lists only info messages from the log
	pub fn infos(&self) -> impl Iterator<Item=&CString> + FusedIterator {
		self.log.iter().filter_map(|x| match x {
			LogEntry::Info(msg) => Some(msg),
			LogEntry::Error(_) => None
		})
	}
}

impl Default for Conversation {
	fn default() -> Self {
		Self::new()
	}
}

impl ConversationHandler for Conversation {
	fn init(&mut self, default_user: Option<impl AsRef<str>>) {
		if let Some(user) = default_user {
			if self.username.is_empty() {
				self.username = user.as_ref().to_string();
			}
		}
	}

	fn prompt_echo_on(&mut self, _msg: &CStr) -> Result<CString, ReturnCode> {
		CString::new(self.username.clone()).map_err(|_| ReturnCode::CONV_ERR)
	}

	fn prompt_echo_off(&mut self, _msg: &CStr) -> Result<CString, ReturnCode> {
		CString::new(self.password.clone()).map_err(|_| ReturnCode::CONV_ERR)
	}

	fn text_info(&mut self, msg: &CStr) {
		self.log.push(LogEntry::Info(msg.to_owned()))
	}

	fn error_msg(&mut self, msg: &CStr) {
		self.log.push(LogEntry::Error(msg.to_owned()))
	}
}