@@ -1181,6 +1181,107 @@ mod tests { |
| 1181 | let _ = std::fs::remove_file(&socket_path); | 1181 | let _ = std::fs::remove_file(&socket_path); |
| 1182 | } | 1182 | } |
| 1183 | | 1183 | |
| | 1184 | + #[test] |
| | 1185 | + fn helper_client_handles_visible_prompt_round_trip() { |
| | 1186 | + let socket_path = temp_socket_path(); |
| | 1187 | + let listener = UnixListener::bind(&socket_path).expect("bind test socket"); |
| | 1188 | + |
| | 1189 | + let server = thread::spawn(move || { |
| | 1190 | + let (mut stream, _) = listener.accept().expect("accept"); |
| | 1191 | + let read_stream = stream.try_clone().expect("clone"); |
| | 1192 | + let mut reader = BufReader::new(read_stream); |
| | 1193 | + |
| | 1194 | + let mut first_line = String::new(); |
| | 1195 | + reader.read_line(&mut first_line).expect("read first line"); |
| | 1196 | + let first = first_line.trim().to_string(); |
| | 1197 | + if first == "operator" { |
| | 1198 | + let mut cookie = String::new(); |
| | 1199 | + reader.read_line(&mut cookie).expect("read cookie"); |
| | 1200 | + } |
| | 1201 | + |
| | 1202 | + stream |
| | 1203 | + .write_all(b"PAM_TEXT_INFO Enter one-time code\n") |
| | 1204 | + .expect("write info"); |
| | 1205 | + stream |
| | 1206 | + .write_all(b"PAM_PROMPT_ECHO_ON Code:\n") |
| | 1207 | + .expect("write prompt"); |
| | 1208 | + stream.flush().expect("flush prompt"); |
| | 1209 | + |
| | 1210 | + let mut code = String::new(); |
| | 1211 | + reader.read_line(&mut code).expect("read code"); |
| | 1212 | + assert_eq!(code.trim(), "123456"); |
| | 1213 | + |
| | 1214 | + stream.write_all(b"SUCCESS\n").expect("write success"); |
| | 1215 | + stream.flush().expect("flush success"); |
| | 1216 | + }); |
| | 1217 | + |
| | 1218 | + let client = HelperSocketClient::new(&socket_path); |
| | 1219 | + let mut prompts = FakePrompt { |
| | 1220 | + plain_response: PromptResponse::Submitted("123456".to_string()), |
| | 1221 | + ..FakePrompt::default() |
| | 1222 | + }; |
| | 1223 | + |
| | 1224 | + let outcome = client |
| | 1225 | + .authenticate("operator", "cookie-visible", &mut prompts) |
| | 1226 | + .expect("authenticate visible"); |
| | 1227 | + assert_eq!(outcome, HelperOutcome::Authorized); |
| | 1228 | + assert_eq!(prompts.infos, vec!["Enter one-time code"]); |
| | 1229 | + assert_eq!(prompts.success_count, 1); |
| | 1230 | + |
| | 1231 | + server.join().expect("server join"); |
| | 1232 | + let _ = std::fs::remove_file(&socket_path); |
| | 1233 | + } |
| | 1234 | + |
| | 1235 | + #[test] |
| | 1236 | + fn helper_client_recovers_after_inline_error_message() { |
| | 1237 | + let socket_path = temp_socket_path(); |
| | 1238 | + let listener = UnixListener::bind(&socket_path).expect("bind test socket"); |
| | 1239 | + |
| | 1240 | + let server = thread::spawn(move || { |
| | 1241 | + let (mut stream, _) = listener.accept().expect("accept"); |
| | 1242 | + let read_stream = stream.try_clone().expect("clone"); |
| | 1243 | + let mut reader = BufReader::new(read_stream); |
| | 1244 | + |
| | 1245 | + let mut first_line = String::new(); |
| | 1246 | + reader.read_line(&mut first_line).expect("read first line"); |
| | 1247 | + if first_line.trim() == "operator" { |
| | 1248 | + let mut cookie = String::new(); |
| | 1249 | + reader.read_line(&mut cookie).expect("read cookie"); |
| | 1250 | + } |
| | 1251 | + |
| | 1252 | + stream |
| | 1253 | + .write_all(b"PAM_ERROR_MSG Incorrect code, try again\n") |
| | 1254 | + .expect("write error"); |
| | 1255 | + stream |
| | 1256 | + .write_all(b"PAM_PROMPT_ECHO_OFF Password:\n") |
| | 1257 | + .expect("write prompt"); |
| | 1258 | + stream.flush().expect("flush prompt"); |
| | 1259 | + |
| | 1260 | + let mut secret = String::new(); |
| | 1261 | + reader.read_line(&mut secret).expect("read secret"); |
| | 1262 | + assert_eq!(secret.trim(), "correct horse"); |
| | 1263 | + |
| | 1264 | + stream.write_all(b"SUCCESS\n").expect("write success"); |
| | 1265 | + stream.flush().expect("flush success"); |
| | 1266 | + }); |
| | 1267 | + |
| | 1268 | + let client = HelperSocketClient::new(&socket_path); |
| | 1269 | + let mut prompts = FakePrompt { |
| | 1270 | + secret_response: PromptResponse::Submitted("correct horse".to_string()), |
| | 1271 | + ..FakePrompt::default() |
| | 1272 | + }; |
| | 1273 | + |
| | 1274 | + let outcome = client |
| | 1275 | + .authenticate("operator", "cookie-inline-error", &mut prompts) |
| | 1276 | + .expect("authenticate recovery"); |
| | 1277 | + assert_eq!(outcome, HelperOutcome::Authorized); |
| | 1278 | + assert_eq!(prompts.errors, vec!["Incorrect code, try again"]); |
| | 1279 | + assert_eq!(prompts.success_count, 1); |
| | 1280 | + |
| | 1281 | + server.join().expect("server join"); |
| | 1282 | + let _ = std::fs::remove_file(&socket_path); |
| | 1283 | + } |
| | 1284 | + |
| 1184 | #[test] | 1285 | #[test] |
| 1185 | fn helper_client_reports_timeout_from_prompt_provider() { | 1286 | fn helper_client_reports_timeout_from_prompt_provider() { |
| 1186 | let socket_path = temp_socket_path(); | 1287 | let socket_path = temp_socket_path(); |