1use crate::command::AuthAction;
4use crate::protocol::{AUTH_ACCEPTED, AUTH_FAILED, AUTH_OUT_OF_SEQUENCE, AUTH_REQUIRED};
5use crate::types::{Password, Username, ValidationError};
6use std::collections::HashMap;
7use tokio::io::AsyncWriteExt;
8
9#[derive(Default)]
11pub struct AuthHandler {
12 users: HashMap<String, String>,
14}
15
16impl std::fmt::Debug for AuthHandler {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 f.debug_struct("AuthHandler")
19 .field("enabled", &!self.users.is_empty())
20 .field("user_count", &self.users.len())
21 .finish_non_exhaustive()
22 }
23}
24
25impl AuthHandler {
26 pub fn new(
42 username: Option<String>,
43 password: Option<String>,
44 ) -> Result<Self, ValidationError> {
45 let mut users = HashMap::new();
46
47 if let (Some(u), Some(p)) = (username, password) {
48 let username = Username::try_new(u)?; let password = Password::try_new(p)?; users.insert(username.as_str().to_string(), password.as_str().to_string());
52 }
53
54 Ok(Self { users })
55 }
56
57 pub fn with_users(user_list: Vec<(String, String)>) -> Result<Self, ValidationError> {
62 let mut users = HashMap::new();
63
64 for (u, p) in user_list {
65 let username = Username::try_new(u.clone())?;
67 let password = Password::try_new(p.clone())?;
68 users.insert(username.as_str().to_string(), password.as_str().to_string());
69 }
70
71 Ok(Self { users })
72 }
73
74 #[inline]
76 #[must_use]
77 pub fn is_enabled(&self) -> bool {
78 !self.users.is_empty()
79 }
80
81 #[must_use]
85 pub fn validate_credentials(&self, username: &str, password: &str) -> bool {
86 if self.users.is_empty() {
87 true
89 } else {
90 self.users
92 .get(username)
93 .is_some_and(|stored_pass| stored_pass == password)
94 }
95 }
96
97 pub async fn handle_auth_command<W>(
103 &self,
104 auth_action: AuthAction<'_>,
105 writer: &mut W,
106 stored_username: Option<&str>,
107 ) -> std::io::Result<(usize, bool)>
108 where
109 W: AsyncWriteExt + Unpin,
110 {
111 match auth_action {
112 AuthAction::RequestPassword(_username) => {
113 writer.write_all(AUTH_REQUIRED).await?;
115 Ok((AUTH_REQUIRED.len(), false))
116 }
117 AuthAction::ValidateAndRespond { password } => {
118 if stored_username.is_none() {
121 writer.write_all(AUTH_OUT_OF_SEQUENCE).await?;
122 return Ok((AUTH_OUT_OF_SEQUENCE.len(), false));
123 }
124
125 let auth_success = stored_username
127 .is_some_and(|username| self.validate_credentials(username, password));
128
129 let response = if auth_success {
130 AUTH_ACCEPTED
131 } else {
132 AUTH_FAILED
133 };
134 writer.write_all(response).await?;
135 Ok((response.len(), auth_success))
136 }
137 AuthAction::UnknownSubcommand => {
138 use crate::protocol::AUTH_UNKNOWN_SUBCOMMAND;
142 writer.write_all(AUTH_UNKNOWN_SUBCOMMAND).await?;
143 Ok((AUTH_UNKNOWN_SUBCOMMAND.len(), false))
144 }
145 }
146 }
147
148 #[inline]
150 #[must_use]
151 pub const fn user_response(&self) -> &'static [u8] {
152 AUTH_REQUIRED
153 }
154
155 #[inline]
157 #[must_use]
158 pub const fn pass_response(&self) -> &'static [u8] {
159 AUTH_ACCEPTED
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 fn test_handler() -> AuthHandler {
168 AuthHandler::default()
169 }
170
171 mod auth_handler {
172 use super::*;
173
174 #[test]
175 fn test_default() {
176 let handler = AuthHandler::default();
177 assert!(!handler.is_enabled());
178 }
179
180 #[test]
181 fn test_new_with_both_credentials() {
182 let handler =
183 AuthHandler::new(Some("user".to_string()), Some("pass".to_string())).unwrap();
184 assert!(handler.is_enabled());
185 }
186
187 #[test]
188 fn test_new_with_only_username() {
189 let handler = AuthHandler::new(Some("user".to_string()), None).unwrap();
190 assert!(!handler.is_enabled());
191 }
192
193 #[test]
194 fn test_new_with_only_password() {
195 let handler = AuthHandler::new(None, Some("pass".to_string())).unwrap();
196 assert!(!handler.is_enabled());
197 }
198
199 #[test]
200 fn test_new_with_neither() {
201 let handler = AuthHandler::new(None, None).unwrap();
202 assert!(!handler.is_enabled());
203 }
204
205 #[test]
206 fn test_with_users_multiple() {
207 let users = vec![
208 ("alice".to_string(), "secret1".to_string()),
209 ("bob".to_string(), "secret2".to_string()),
210 ("charlie".to_string(), "secret3".to_string()),
211 ];
212 let handler = AuthHandler::with_users(users).unwrap();
213 assert!(handler.is_enabled());
214 assert!(handler.validate_credentials("alice", "secret1"));
215 assert!(handler.validate_credentials("bob", "secret2"));
216 assert!(handler.validate_credentials("charlie", "secret3"));
217 assert!(!handler.validate_credentials("alice", "wrong"));
218 assert!(!handler.validate_credentials("bob", "secret1")); assert!(!handler.validate_credentials("dave", "anything")); }
221
222 #[test]
223 fn test_with_users_empty() {
224 let handler = AuthHandler::with_users(vec![]).unwrap();
225 assert!(!handler.is_enabled());
226 assert!(handler.validate_credentials("anyone", "anything")); }
228
229 #[test]
230 fn test_with_users_rejects_empty_username() {
231 let users = vec![
232 ("alice".to_string(), "pass1".to_string()),
233 (String::new(), "pass2".to_string()), ];
235 let result = AuthHandler::with_users(users);
236 assert!(result.is_err());
237 }
238
239 #[test]
240 fn test_with_users_rejects_empty_password() {
241 let users = vec![
242 ("alice".to_string(), "pass1".to_string()),
243 ("bob".to_string(), String::new()), ];
245 let result = AuthHandler::with_users(users);
246 assert!(result.is_err());
247 }
248
249 #[test]
250 fn test_new_with_empty_username_fails() {
251 let result = AuthHandler::new(Some(String::new()), Some("pass".to_string()));
252 assert!(result.is_err(), "Empty username should return error");
253 }
254
255 #[test]
256 fn test_new_with_empty_password_fails() {
257 let result = AuthHandler::new(Some("user".to_string()), Some(String::new()));
258 assert!(result.is_err(), "Empty password should return error");
259 }
260
261 #[test]
262 fn test_new_with_whitespace_username_fails() {
263 let result = AuthHandler::new(Some(" ".to_string()), Some("pass".to_string()));
264 assert!(
265 result.is_err(),
266 "Whitespace-only username should return error"
267 );
268 }
269
270 #[test]
271 fn test_new_with_whitespace_password_fails() {
272 let result = AuthHandler::new(Some("user".to_string()), Some(" ".to_string()));
273 assert!(
274 result.is_err(),
275 "Whitespace-only password should return error"
276 );
277 }
278
279 #[test]
280 fn test_validate_when_disabled() {
281 let handler = AuthHandler::new(None, None).unwrap();
282 assert!(handler.validate_credentials("any", "thing"));
283 assert!(handler.validate_credentials("", ""));
284 assert!(handler.validate_credentials("foo", "bar"));
285 }
286
287 #[test]
288 fn test_validate_when_enabled() {
289 let handler =
290 AuthHandler::new(Some("alice".to_string()), Some("secret".to_string())).unwrap();
291 assert!(handler.validate_credentials("alice", "secret"));
292 assert!(!handler.validate_credentials("alice", "wrong"));
293 assert!(!handler.validate_credentials("bob", "secret"));
294 assert!(!handler.validate_credentials("bob", "wrong"));
295 }
296
297 #[test]
298 fn test_is_enabled_consistent() {
299 let disabled = AuthHandler::new(None, None).unwrap();
300 assert!(!disabled.is_enabled());
301 assert!(!disabled.is_enabled()); let enabled = AuthHandler::new(Some("u".to_string()), Some("p".to_string())).unwrap();
304 assert!(enabled.is_enabled());
305 assert!(enabled.is_enabled()); }
307 }
308
309 #[test]
310 fn test_user_response() {
311 let handler = test_handler();
312 let response = handler.user_response();
313 let response_str = String::from_utf8_lossy(response);
314
315 assert!(response_str.starts_with("381"));
317 assert!(response_str.contains("Password required") || response_str.contains("password"));
318 assert!(response_str.ends_with("\r\n"));
319 }
320
321 #[test]
322 fn test_pass_response() {
323 let handler = test_handler();
324 let response = handler.pass_response();
325 let response_str = String::from_utf8_lossy(response);
326
327 assert!(response_str.starts_with("281"));
329 assert!(response_str.contains("accepted") || response_str.contains("Authentication"));
330 assert!(response_str.ends_with("\r\n"));
331 }
332
333 #[test]
334 fn test_responses_are_static() {
335 let handler = test_handler();
337 let response1 = handler.user_response();
338 let response2 = handler.user_response();
339 assert_eq!(response1.as_ptr(), response2.as_ptr());
340
341 let response3 = handler.pass_response();
342 let response4 = handler.pass_response();
343 assert_eq!(response3.as_ptr(), response4.as_ptr());
344 }
345
346 #[test]
347 fn test_responses_are_different() {
348 let handler = test_handler();
350 let user_resp = handler.user_response();
351 let pass_resp = handler.pass_response();
352 assert_ne!(user_resp, pass_resp);
353 }
354
355 #[test]
356 fn test_responses_are_valid_utf8() {
357 let handler = test_handler();
359 let user_resp = handler.user_response();
360 assert!(std::str::from_utf8(user_resp).is_ok());
361
362 let pass_resp = handler.pass_response();
363 assert!(std::str::from_utf8(pass_resp).is_ok());
364 }
365
366 #[test]
367 fn test_auth_disabled_by_default() {
368 let handler = AuthHandler::default();
369 assert!(!handler.is_enabled());
370 assert!(handler.validate_credentials("any", "thing")); }
372
373 #[test]
374 fn test_auth_new_none_none() {
375 let handler = AuthHandler::new(None, None).unwrap();
376 assert!(!handler.is_enabled());
377 assert!(handler.validate_credentials("any", "thing"));
378 }
379
380 #[test]
381 fn test_auth_enabled_with_credentials() {
382 let handler =
383 AuthHandler::new(Some("mjc".to_string()), Some("nntp1337".to_string())).unwrap();
384 assert!(handler.is_enabled());
385 assert!(handler.validate_credentials("mjc", "nntp1337"));
386 assert!(!handler.validate_credentials("mjc", "wrong"));
387 assert!(!handler.validate_credentials("wrong", "nntp1337"));
388 }
389
390 #[test]
391 fn test_security_empty_credentials_rejected() {
392 let result = AuthHandler::new(Some(String::new()), Some("pass".to_string()));
394 assert!(
395 result.is_err(),
396 "Empty username should be rejected to prevent silent auth bypass"
397 );
398
399 let result = AuthHandler::new(Some("user".to_string()), Some(String::new()));
401 assert!(
402 result.is_err(),
403 "Empty password should be rejected to prevent silent auth bypass"
404 );
405
406 let result = AuthHandler::new(Some(String::new()), Some(String::new()));
408 assert!(
409 result.is_err(),
410 "Both empty should be rejected to prevent silent auth bypass"
411 );
412 }
413
414 #[test]
415 fn test_security_whitespace_credentials_rejected() {
416 let result = AuthHandler::new(Some(" ".to_string()), Some("pass".to_string()));
418 assert!(
419 result.is_err(),
420 "Whitespace-only username should be rejected"
421 );
422
423 let result = AuthHandler::new(Some("user".to_string()), Some(" ".to_string()));
425 assert!(
426 result.is_err(),
427 "Whitespace-only password should be rejected"
428 );
429 }
430
431 #[test]
432 fn test_security_explicit_config_prevents_silent_bypass() {
433 let username_from_config = Some(String::new()); let password_from_config = Some("secret".to_string());
443
444 let result = AuthHandler::new(username_from_config, password_from_config);
445
446 assert!(
447 result.is_err(),
448 "Proxy must refuse to start with empty credentials from config. \
449 Silently disabling auth would be a critical security vulnerability!"
450 );
451 }
452}