comparison src/conv.rs @ 73:ac6881304c78

Do conversations, along with way too much stuff. This implements conversations, along with all the memory management brouhaha that goes along with it. The conversation now lives directly on the handle rather than being a thing you have to get from it and then call manually. It Turns Out this makes things a lot easier! I guess we reorganized things again. For the last time. For real. I promise. This all passes ASAN, so it seems Pretty Good!
author Paul Fisher <paul@pfish.zone>
date Thu, 05 Jun 2025 03:41:38 -0400
parents 47eb242a4f88
children c7c596e6388f
comparison
equal deleted inserted replaced
72:47eb242a4f88 73:ac6881304c78
2 2
3 // Temporarily allowed until we get the actual conversation functions hooked up. 3 // Temporarily allowed until we get the actual conversation functions hooked up.
4 #![allow(dead_code)] 4 #![allow(dead_code)]
5 5
6 use crate::constants::Result; 6 use crate::constants::Result;
7 use crate::pam_ffi::LibPamConversation; 7 use crate::ErrorCode;
8 use crate::pam_ffi::Message;
9 use secure_string::SecureString; 8 use secure_string::SecureString;
10 // TODO: In most cases, we should be passing around references to strings 9 // TODO: In most cases, we should be passing around references to strings
11 // or binary data. Right now we don't because that turns type inference and 10 // or binary data. Right now we don't because that turns type inference and
12 // trait definitions/implementations into a HUGE MESS. 11 // trait definitions/implementations into a HUGE MESS.
13 // 12 //
14 // Ideally, we would be using some kind of `TD: TextData` and `BD: BinaryData` 13 // Ideally, we would be using some kind of `TD: TextData` and `BD: BinaryData`
15 // associated types in the various Conversation traits to avoid copying 14 // associated types in the various Conversation traits to avoid copying
16 // when unnecessary. 15 // when unnecessary.
17 16
17 /// The types of message and request that can be sent to a user.
18 ///
19 /// The data within each enum value is the prompt (or other information)
20 /// that will be presented to the user.
21 #[derive(Clone, Copy, Debug)]
22 pub enum Message<'a> {
23 /// Requests information from the user; will be masked when typing.
24 ///
25 /// Response: [`MaskedText`](Response::MaskedText)
26 MaskedPrompt(&'a str),
27 /// Requests information from the user; will not be masked.
28 ///
29 /// Response: [`Text`](Response::Text)
30 Prompt(&'a str),
31 /// "Yes/No/Maybe conditionals" (a Linux-PAM extension).
32 ///
33 /// Response: [`Text`](Response::Text)
34 /// (Linux-PAM documentation doesn't define its contents.)
35 RadioPrompt(&'a str),
36 /// Raises an error message to the user.
37 ///
38 /// Response: [`NoResponse`](Response::NoResponse)
39 Error(&'a str),
40 /// Sends an informational message to the user.
41 ///
42 /// Response: [`NoResponse`](Response::NoResponse)
43 Info(&'a str),
44 /// Requests binary data from the client (a Linux-PAM extension).
45 ///
46 /// This is used for non-human or non-keyboard prompts (security key?).
47 /// NOT part of the X/Open PAM specification.
48 ///
49 /// Response: [`Binary`](Response::Binary)
50 BinaryPrompt {
51 /// Some binary data.
52 data: &'a [u8],
53 /// A "type" that you can use for signalling. Has no strict definition in PAM.
54 data_type: u8,
55 },
56 }
57
18 /// The responses that PAM will return from a request. 58 /// The responses that PAM will return from a request.
19 #[derive(Debug, PartialEq, derive_more::From)] 59 #[derive(Debug, PartialEq, derive_more::From)]
20 pub enum Response { 60 pub enum Response {
21 /// Used to fill in list entries where there is no response expected. 61 /// Used to fill in list entries where there is no response expected.
22 /// 62 ///
42 /// 82 ///
43 /// Used in response to: 83 /// Used in response to:
44 /// 84 ///
45 /// - [`BinaryPrompt`](Message::BinaryPrompt) 85 /// - [`BinaryPrompt`](Message::BinaryPrompt)
46 Binary(BinaryData), 86 Binary(BinaryData),
87 }
88
89 /// The function type for a conversation.
90 ///
91 /// A macro to save typing `FnMut(&[Message]) -> Result<Vec<Response>>`.
92 #[macro_export]
93 macro_rules! conv_type {
94 () => {FnMut(&[Message]) -> Result<Vec<Response>>};
95 (impl) => { impl FnMut(&[Message]) -> Result<Vec<Response>> }
47 } 96 }
48 97
49 /// A channel for PAM modules to request information from the user. 98 /// A channel for PAM modules to request information from the user.
50 /// 99 ///
51 /// This trait is used by both applications and PAM modules: 100 /// This trait is used by both applications and PAM modules:
57 pub trait Conversation { 106 pub trait Conversation {
58 /// Sends messages to the user. 107 /// Sends messages to the user.
59 /// 108 ///
60 /// The returned Vec of messages always contains exactly as many entries 109 /// The returned Vec of messages always contains exactly as many entries
61 /// as there were messages in the request; one corresponding to each. 110 /// as there were messages in the request; one corresponding to each.
62 fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>>; 111 fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>>;
112 }
113
114 fn conversation_func(func: conv_type!(impl)) -> impl Conversation {
115 Convo(func)
116 }
117
118 struct Convo<C: FnMut(&[Message]) -> Result<Vec<Response>>>(C);
119
120 impl<C: FnMut(&[Message]) -> Result<Vec<Response>>> Conversation for Convo<C> {
121 fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
122 self.0(messages)
123 }
124 }
125
126 /// Provides methods to make it easier to send exactly one message.
127 ///
128 /// This is primarily used by PAM modules, so that a module that only needs
129 /// one piece of information at a time doesn't have a ton of boilerplate.
130 /// You may also find it useful for testing PAM application libraries.
131 ///
132 /// ```
133 /// # use nonstick::{PamHandleModule, Conversation, Result};
134 /// # fn _do_test(mut pam_handle: impl PamHandleModule) -> Result<()> {
135 /// use nonstick::ConversationMux;
136 ///
137 /// let token = pam_handle.masked_prompt("enter your one-time token")?;
138 /// # Ok(())
139 /// # }
140 pub trait ConversationMux {
141 /// Prompts the user for something.
142 fn prompt(&mut self, request: &str) -> Result<String>;
143 /// Prompts the user for something, but hides what the user types.
144 fn masked_prompt(&mut self, request: &str) -> Result<SecureString>;
145 /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension).
146 ///
147 /// PAM documentation doesn't define the format of the response.
148 fn radio_prompt(&mut self, request: &str) -> Result<String>;
149 /// Alerts the user to an error.
150 fn error(&mut self, message: &str);
151 /// Sends an informational message to the user.
152 fn info(&mut self, message: &str);
153 /// Requests binary data from the user (a Linux-PAM extension).
154 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>;
155 }
156
157 impl<C: Conversation> ConversationMux for C {
158 /// Prompts the user for something.
159 fn prompt(&mut self, request: &str) -> Result<String> {
160 let resp = self.converse(&[Message::Prompt(request)])?.pop();
161 match resp {
162 Some(Response::Text(s)) => Ok(s),
163 _ => Err(ErrorCode::ConversationError),
164 }
165 }
166
167 /// Prompts the user for something, but hides what the user types.
168 fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
169 let resp = self.converse(&[Message::MaskedPrompt(request)])?.pop();
170 match resp {
171 Some(Response::MaskedText(s)) => Ok(s),
172 _ => Err(ErrorCode::ConversationError),
173 }
174 }
175
176 /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension).
177 ///
178 /// PAM documentation doesn't define the format of the response.
179 fn radio_prompt(&mut self, request: &str) -> Result<String> {
180 let resp = self.converse(&[Message::RadioPrompt(request)])?.pop();
181 match resp {
182 Some(Response::Text(s)) => Ok(s),
183 _ => Err(ErrorCode::ConversationError),
184 }
185 }
186
187 /// Alerts the user to an error.
188 fn error(&mut self, message: &str) {
189 let _ = self.converse(&[Message::Error(message)]);
190 }
191
192 /// Sends an informational message to the user.
193 fn info(&mut self, message: &str) {
194 let _ = self.converse(&[Message::Info(message)]);
195 }
196
197 /// Requests binary data from the user (a Linux-PAM extension).
198 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> {
199 let resp = self
200 .converse(&[Message::BinaryPrompt { data, data_type }])?
201 .pop();
202 match resp {
203 Some(Response::Binary(d)) => Ok(d),
204 _ => Err(ErrorCode::ConversationError),
205 }
206 }
63 } 207 }
64 208
65 /// Trait that an application can implement if they want to handle messages 209 /// Trait that an application can implement if they want to handle messages
66 /// one at a time. 210 /// one at a time.
67 pub trait DemuxedConversation { 211 pub trait DemuxedConversation {
79 fn info(&mut self, message: &str); 223 fn info(&mut self, message: &str);
80 /// Requests binary data from the user (a Linux-PAM extension). 224 /// Requests binary data from the user (a Linux-PAM extension).
81 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>; 225 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>;
82 } 226 }
83 227
84 impl Conversation for LibPamConversation { 228 impl<DM> Conversation for DM
85 fn send(&mut self, _: &[Message]) -> Result<Vec<Response>> { 229 where
86 todo!() 230 DM: DemuxedConversation,
87 } 231 {
88 } 232 fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
89
90 impl<D: DemuxedConversation> Conversation for D {
91 fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
92 messages 233 messages
93 .iter() 234 .iter()
94 .map(|msg| match *msg { 235 .map(|msg| match *msg {
95 Message::Prompt(prompt) => self.prompt(prompt).map(Response::from), 236 Message::Prompt(prompt) => self.prompt(prompt).map(Response::from),
96 Message::MaskedPrompt(prompt) => self.masked_prompt(prompt).map(Response::from), 237 Message::MaskedPrompt(prompt) => self.masked_prompt(prompt).map(Response::from),
193 Response::MaskedText("my secrets".into()), 334 Response::MaskedText("my secrets".into()),
194 Response::NoResponse, 335 Response::NoResponse,
195 Response::NoResponse, 336 Response::NoResponse,
196 ], 337 ],
197 tester 338 tester
198 .send(&[ 339 .converse(&[
199 Message::Prompt("what"), 340 Message::Prompt("what"),
200 Message::MaskedPrompt("reveal"), 341 Message::MaskedPrompt("reveal"),
201 Message::Error("whoopsie"), 342 Message::Error("whoopsie"),
202 Message::Info("did you know"), 343 Message::Info("did you know"),
203 ]) 344 ])
206 assert!(tester.error_ran); 347 assert!(tester.error_ran);
207 assert!(tester.info_ran); 348 assert!(tester.info_ran);
208 349
209 assert_eq!( 350 assert_eq!(
210 ErrorCode::PermissionDenied, 351 ErrorCode::PermissionDenied,
211 tester.send(&[Message::Prompt("give_err")]).unwrap_err(), 352 tester.converse(&[Message::Prompt("give_err")]).unwrap_err(),
212 ); 353 );
213 354
214 // Test the Linux-PAM extensions separately. 355 // Test the Linux-PAM extensions separately.
215 356
216 assert_eq!( 357 assert_eq!(
217 vec![ 358 vec![
218 Response::Text("zero".to_owned()), 359 Response::Text("zero".to_owned()),
219 Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)), 360 Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)),
220 ], 361 ],
221 tester 362 tester
222 .send(&[ 363 .converse(&[
223 Message::RadioPrompt("channel?"), 364 Message::RadioPrompt("channel?"),
224 Message::BinaryPrompt { 365 Message::BinaryPrompt {
225 data: &[10, 9, 8], 366 data: &[10, 9, 8],
226 data_type: 66 367 data_type: 66
227 }, 368 },
228 ]) 369 ])
229 .unwrap() 370 .unwrap()
230 ); 371 );
231 } 372 }
232 } 373
374 #[test]
375 fn test_mux() {
376 use super::ConversationMux;
377 struct MuxTester;
378
379 impl Conversation for MuxTester {
380 fn converse(&mut self, messages: &[Message]) -> crate::Result<Vec<Response>> {
381 if let [msg] = messages {
382 match msg {
383 Message::Info(info) => {
384 assert_eq!("let me tell you", *info);
385 Ok(vec![Response::NoResponse])
386 }
387 Message::Error(error) => {
388 assert_eq!("oh no", *error);
389 Ok(vec![Response::NoResponse])
390 }
391 Message::Prompt("should_error") => Err(ErrorCode::BufferError),
392 Message::Prompt(ask) => {
393 assert_eq!("question", *ask);
394 Ok(vec![Response::Text("answer".to_owned())])
395 }
396 Message::MaskedPrompt("return_wrong_type") => {
397 Ok(vec![Response::NoResponse])
398 }
399 Message::MaskedPrompt(ask) => {
400 assert_eq!("password!", *ask);
401 Ok(vec![Response::MaskedText(SecureString::from(
402 "open sesame",
403 ))])
404 }
405 Message::BinaryPrompt { data, data_type } => {
406 assert_eq!(&[1, 2, 3], data);
407 assert_eq!(69, *data_type);
408 Ok(vec![Response::Binary(super::BinaryData::new(
409 vec![3, 2, 1],
410 42,
411 ))])
412 }
413 Message::RadioPrompt(ask) => {
414 assert_eq!("radio?", *ask);
415 Ok(vec![Response::Text("yes".to_owned())])
416 }
417 }
418 } else {
419 panic!("messages is the wrong size ({len})", len = messages.len())
420 }
421 }
422 }
423
424 let mut tester = MuxTester;
425
426 assert_eq!("answer", tester.prompt("question").unwrap());
427 assert_eq!(
428 SecureString::from("open sesame"),
429 tester.masked_prompt("password!").unwrap()
430 );
431 tester.error("oh no");
432 tester.info("let me tell you");
433 {
434 assert_eq!("yes", tester.radio_prompt("radio?").unwrap());
435 assert_eq!(
436 super::BinaryData::new(vec![3, 2, 1], 42),
437 tester.binary_prompt(&[1, 2, 3], 69).unwrap(),
438 )
439 }
440 assert_eq!(
441 ErrorCode::BufferError,
442 tester.prompt("should_error").unwrap_err(),
443 );
444 assert_eq!(
445 ErrorCode::ConversationError,
446 tester.masked_prompt("return_wrong_type").unwrap_err()
447 )
448 }
449 }