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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/*!
 * Interactive command line conversation handler
 *
 * *This module is unavailable if pam-client is built without the `"cli"` feature.*
 */

/***********************************************************************
 * (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/.            *
 ***********************************************************************/

#![forbid(unsafe_code)]

use super::ConversationHandler;
use crate::error::ErrorCode;
use std::ffi::{CStr, CString};
use std::io::{self, BufRead, Write};

/// Newline trimming helper function
fn trim_newline(s: &mut String) {
	if s.ends_with('\n') {
		s.pop();
		if s.ends_with('\r') {
			s.pop();
		}
	}
}

/// Command-line implementation of `ConversationHandler`
///
/// *This struct is unavailable if pam-client is built without the `"cli"` feature.*
///
/// Prompts, info and error messages will be written to STDERR, non-secret
/// input in read from STDIN and [rpassword][`rpassword::read_password_from_tty`]
/// is used to prompt the user for passwords.
///
/// # Limitations
///
/// Please note that UTF-8 encoding is assumed for terminal I/O, so this
/// handler may fail to authenticate on legacy non-UTF-8 systems when the user
/// input contains non-ASCII characters.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Conversation {
	info_prefix: String,
	error_prefix: String,
}

impl Conversation {
	/// Creates a new CLI conversation handler.
	#[must_use]
	pub fn new() -> Self {
		Self {
			info_prefix: "[PAM INFO] ".to_string(),
			error_prefix: "[PAM ERROR] ".to_string(),
		}
	}

	/// The prefix text written before info text
	#[inline]
	#[must_use]
	pub fn info_prefix(&self) -> &str {
		&self.info_prefix
	}

	/// Updates the prefix put before info text
	pub fn set_info_prefix(&mut self, prefix: impl Into<String>) {
		self.info_prefix = prefix.into();
	}

	/// The prefix text written before error messages
	#[inline]
	#[must_use]
	pub fn error_prefix(&self) -> &str {
		&self.error_prefix
	}

	/// Updates the prefix put before error messages
	pub fn set_error_prefix(&mut self, prefix: impl Into<String>) {
		self.error_prefix = prefix.into();
	}
}

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

impl ConversationHandler for Conversation {
	fn prompt_echo_on(&mut self, msg: &CStr) -> Result<CString, ErrorCode> {
		let mut line = String::new();
		if io::stderr().lock().write_all(msg.to_bytes()).is_err() {
			return Err(ErrorCode::CONV_ERR);
		}
		match io::stdin().lock().read_line(&mut line) {
			Err(_) | Ok(0) => Err(ErrorCode::CONV_ERR),
			Ok(_) => {
				trim_newline(&mut line);
				CString::new(line).map_err(|_| ErrorCode::CONV_ERR)
			}
		}
	}

	fn prompt_echo_off(&mut self, msg: &CStr) -> Result<CString, ErrorCode> {
		let prompt = msg.to_string_lossy();
		match rpassword::read_password_from_tty(Some(&prompt)) {
			Err(_) => Err(ErrorCode::CONV_ERR),
			Ok(password) => CString::new(password).map_err(|_| ErrorCode::CONV_ERR),
		}
	}

	fn text_info(&mut self, msg: &CStr) {
		eprintln!("{}{}", &self.info_prefix, msg.to_string_lossy());
	}

	fn error_msg(&mut self, msg: &CStr) {
		eprintln!("{}{}", &self.error_prefix, msg.to_string_lossy());
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn test_trim() {
		let mut value = "Test\r\n".to_string();
		trim_newline(&mut value);
		assert_eq!(value, "Test");

		let mut value = "Test\n".to_string();
		trim_newline(&mut value);
		assert_eq!(value, "Test");
	}

	#[test]
	fn test_output() {
		let mut c = Conversation::default();
		c.set_info_prefix("INFO: ");
		c.set_error_prefix("ERROR: ");
		assert_eq!(c.info_prefix(), "INFO: ");
		assert_eq!(c.error_prefix(), "ERROR: ");
		c.text_info(&CString::new("test").unwrap());
		c.error_msg(&CString::new("test2").unwrap());
	}
}