Mercurial > crates > nonstick
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 } |