pam_client/
conv_cli.rs

1/*!
2 * Interactive command line conversation handler
3 *
4 * *This module is unavailable if pam-client is built without the `"cli"` feature.*
5 */
6
7/***********************************************************************
8 * (c) 2021 Christoph Grenz <christophg+gitorious @ grenz-bonn.de>     *
9 *                                                                     *
10 * This Source Code Form is subject to the terms of the Mozilla Public *
11 * License, v. 2.0. If a copy of the MPL was not distributed with this *
12 * file, You can obtain one at http://mozilla.org/MPL/2.0/.            *
13 ***********************************************************************/
14
15#![forbid(unsafe_code)]
16
17use super::ConversationHandler;
18use crate::error::ErrorCode;
19use std::ffi::{CStr, CString};
20use std::io::{self, BufRead, Write};
21
22/// Newline trimming helper function
23fn trim_newline(s: &mut String) {
24	if s.ends_with('\n') {
25		s.pop();
26		if s.ends_with('\r') {
27			s.pop();
28		}
29	}
30}
31
32/// Command-line implementation of `ConversationHandler`
33///
34/// *This struct is unavailable if pam-client is built without the `"cli"` feature.*
35///
36/// Prompts, info and error messages will be written to STDERR, non-secret
37/// input in read from STDIN and [rpassword][`rpassword::prompt_password`]
38/// is used to prompt the user for passwords.
39///
40/// # Limitations
41///
42/// Please note that UTF-8 encoding is assumed for terminal I/O, so this
43/// handler may fail to authenticate on legacy non-UTF-8 systems when the user
44/// input contains non-ASCII characters.
45#[derive(Debug, Clone)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub struct Conversation {
48	info_prefix: String,
49	error_prefix: String,
50}
51
52impl Conversation {
53	/// Creates a new CLI conversation handler.
54	#[must_use]
55	pub fn new() -> Self {
56		Self {
57			info_prefix: "[PAM INFO] ".to_string(),
58			error_prefix: "[PAM ERROR] ".to_string(),
59		}
60	}
61
62	/// The prefix text written before info text
63	#[inline]
64	#[must_use]
65	pub fn info_prefix(&self) -> &str {
66		&self.info_prefix
67	}
68
69	/// Updates the prefix put before info text
70	pub fn set_info_prefix(&mut self, prefix: impl Into<String>) {
71		self.info_prefix = prefix.into();
72	}
73
74	/// The prefix text written before error messages
75	#[inline]
76	#[must_use]
77	pub fn error_prefix(&self) -> &str {
78		&self.error_prefix
79	}
80
81	/// Updates the prefix put before error messages
82	pub fn set_error_prefix(&mut self, prefix: impl Into<String>) {
83		self.error_prefix = prefix.into();
84	}
85}
86
87impl Default for Conversation {
88	fn default() -> Self {
89		Self::new()
90	}
91}
92
93impl ConversationHandler for Conversation {
94	fn prompt_echo_on(&mut self, msg: &CStr) -> Result<CString, ErrorCode> {
95		let mut line = String::new();
96		if io::stderr().lock().write_all(msg.to_bytes()).is_err() {
97			return Err(ErrorCode::CONV_ERR);
98		}
99		let result = io::stdin().lock().read_line(&mut line);
100		match result {
101			Err(_) | Ok(0) => Err(ErrorCode::CONV_ERR),
102			Ok(_) => {
103				trim_newline(&mut line);
104				CString::new(line).map_err(|_| ErrorCode::CONV_ERR)
105			}
106		}
107	}
108
109	fn prompt_echo_off(&mut self, msg: &CStr) -> Result<CString, ErrorCode> {
110		let prompt = msg.to_string_lossy();
111		match rpassword::prompt_password(&prompt) {
112			Err(_) => Err(ErrorCode::CONV_ERR),
113			Ok(password) => CString::new(password).map_err(|_| ErrorCode::CONV_ERR),
114		}
115	}
116
117	fn text_info(&mut self, msg: &CStr) {
118		eprintln!("{}{}", &self.info_prefix, msg.to_string_lossy());
119	}
120
121	fn error_msg(&mut self, msg: &CStr) {
122		eprintln!("{}{}", &self.error_prefix, msg.to_string_lossy());
123	}
124}
125
126#[cfg(test)]
127mod tests {
128	use super::*;
129
130	#[test]
131	fn test_trim() {
132		let mut value = "Test\r\n".to_string();
133		trim_newline(&mut value);
134		assert_eq!(value, "Test");
135
136		let mut value = "Test\n".to_string();
137		trim_newline(&mut value);
138		assert_eq!(value, "Test");
139	}
140
141	#[test]
142	fn test_output() {
143		let mut c = Conversation::default();
144		c.set_info_prefix("INFO: ");
145		c.set_error_prefix("ERROR: ");
146		assert_eq!(c.info_prefix(), "INFO: ");
147		assert_eq!(c.error_prefix(), "ERROR: ");
148		c.text_info(&CString::new("test").unwrap());
149		c.error_msg(&CString::new("test2").unwrap());
150
151		assert!(format!("{:?}", &c).contains("ERROR: "));
152	}
153}