1use crate::command::AuthAction;
4use crate::protocol::{AUTH_ACCEPTED, AUTH_FAILED, 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.clone())?; let password = Password::try_new(p.clone())?; 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 pub fn is_enabled(&self) -> bool {
77 !self.users.is_empty()
78 }
79
80 pub fn validate_credentials(&self, username: &str, password: &str) -> bool {
84 if self.users.is_empty() {
85 true
87 } else {
88 self.users
90 .get(username)
91 .is_some_and(|stored_pass| stored_pass == password)
92 }
93 }
94
95 pub async fn handle_auth_command<W>(
98 &self,
99 auth_action: AuthAction<'_>,
100 writer: &mut W,
101 stored_username: Option<&str>,
102 ) -> std::io::Result<(usize, bool)>
103 where
104 W: AsyncWriteExt + Unpin,
105 {
106 match auth_action {
107 AuthAction::RequestPassword(_username) => {
108 writer.write_all(AUTH_REQUIRED).await?;
110 Ok((AUTH_REQUIRED.len(), false))
111 }
112 AuthAction::ValidateAndRespond { password } => {
113 let auth_success = if let Some(username) = stored_username {
115 self.validate_credentials(username, password)
116 } else {
117 false
119 };
120
121 let response = if auth_success {
122 AUTH_ACCEPTED
123 } else {
124 AUTH_FAILED
125 };
126 writer.write_all(response).await?;
127 Ok((response.len(), auth_success))
128 }
129 }
130 }
131
132 #[inline]
134 pub const fn user_response(&self) -> &'static [u8] {
135 AUTH_REQUIRED
136 }
137
138 #[inline]
140 pub const fn pass_response(&self) -> &'static [u8] {
141 AUTH_ACCEPTED
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 fn test_handler() -> AuthHandler {
150 AuthHandler::default()
151 }
152
153 mod auth_handler {
154 use super::*;
155
156 #[test]
157 fn test_default() {
158 let handler = AuthHandler::default();
159 assert!(!handler.is_enabled());
160 }
161
162 #[test]
163 fn test_new_with_both_credentials() {
164 let handler =
165 AuthHandler::new(Some("user".to_string()), Some("pass".to_string())).unwrap();
166 assert!(handler.is_enabled());
167 }
168
169 #[test]
170 fn test_new_with_only_username() {
171 let handler = AuthHandler::new(Some("user".to_string()), None).unwrap();
172 assert!(!handler.is_enabled());
173 }
174
175 #[test]
176 fn test_new_with_only_password() {
177 let handler = AuthHandler::new(None, Some("pass".to_string())).unwrap();
178 assert!(!handler.is_enabled());
179 }
180
181 #[test]
182 fn test_new_with_neither() {
183 let handler = AuthHandler::new(None, None).unwrap();
184 assert!(!handler.is_enabled());
185 }
186
187 #[test]
188 fn test_with_users_multiple() {
189 let users = vec![
190 ("alice".to_string(), "secret1".to_string()),
191 ("bob".to_string(), "secret2".to_string()),
192 ("charlie".to_string(), "secret3".to_string()),
193 ];
194 let handler = AuthHandler::with_users(users).unwrap();
195 assert!(handler.is_enabled());
196 assert!(handler.validate_credentials("alice", "secret1"));
197 assert!(handler.validate_credentials("bob", "secret2"));
198 assert!(handler.validate_credentials("charlie", "secret3"));
199 assert!(!handler.validate_credentials("alice", "wrong"));
200 assert!(!handler.validate_credentials("bob", "secret1")); assert!(!handler.validate_credentials("dave", "anything")); }
203
204 #[test]
205 fn test_with_users_empty() {
206 let handler = AuthHandler::with_users(vec![]).unwrap();
207 assert!(!handler.is_enabled());
208 assert!(handler.validate_credentials("anyone", "anything")); }
210
211 #[test]
212 fn test_with_users_rejects_empty_username() {
213 let users = vec![
214 ("alice".to_string(), "pass1".to_string()),
215 ("".to_string(), "pass2".to_string()), ];
217 let result = AuthHandler::with_users(users);
218 assert!(result.is_err());
219 }
220
221 #[test]
222 fn test_with_users_rejects_empty_password() {
223 let users = vec![
224 ("alice".to_string(), "pass1".to_string()),
225 ("bob".to_string(), "".to_string()), ];
227 let result = AuthHandler::with_users(users);
228 assert!(result.is_err());
229 }
230
231 #[test]
232 fn test_new_with_empty_username_fails() {
233 let result = AuthHandler::new(Some("".to_string()), Some("pass".to_string()));
234 assert!(result.is_err(), "Empty username should return error");
235 }
236
237 #[test]
238 fn test_new_with_empty_password_fails() {
239 let result = AuthHandler::new(Some("user".to_string()), Some("".to_string()));
240 assert!(result.is_err(), "Empty password should return error");
241 }
242
243 #[test]
244 fn test_new_with_whitespace_username_fails() {
245 let result = AuthHandler::new(Some(" ".to_string()), Some("pass".to_string()));
246 assert!(
247 result.is_err(),
248 "Whitespace-only username should return error"
249 );
250 }
251
252 #[test]
253 fn test_new_with_whitespace_password_fails() {
254 let result = AuthHandler::new(Some("user".to_string()), Some(" ".to_string()));
255 assert!(
256 result.is_err(),
257 "Whitespace-only password should return error"
258 );
259 }
260
261 #[test]
262 fn test_validate_when_disabled() {
263 let handler = AuthHandler::new(None, None).unwrap();
264 assert!(handler.validate_credentials("any", "thing"));
265 assert!(handler.validate_credentials("", ""));
266 assert!(handler.validate_credentials("foo", "bar"));
267 }
268
269 #[test]
270 fn test_validate_when_enabled() {
271 let handler =
272 AuthHandler::new(Some("alice".to_string()), Some("secret".to_string())).unwrap();
273 assert!(handler.validate_credentials("alice", "secret"));
274 assert!(!handler.validate_credentials("alice", "wrong"));
275 assert!(!handler.validate_credentials("bob", "secret"));
276 assert!(!handler.validate_credentials("bob", "wrong"));
277 }
278
279 #[test]
280 fn test_is_enabled_consistent() {
281 let disabled = AuthHandler::new(None, None).unwrap();
282 assert!(!disabled.is_enabled());
283 assert!(!disabled.is_enabled()); let enabled = AuthHandler::new(Some("u".to_string()), Some("p".to_string())).unwrap();
286 assert!(enabled.is_enabled());
287 assert!(enabled.is_enabled()); }
289 }
290
291 #[test]
292 fn test_user_response() {
293 let handler = test_handler();
294 let response = handler.user_response();
295 let response_str = String::from_utf8_lossy(response);
296
297 assert!(response_str.starts_with("381"));
299 assert!(response_str.contains("Password required") || response_str.contains("password"));
300 assert!(response_str.ends_with("\r\n"));
301 }
302
303 #[test]
304 fn test_pass_response() {
305 let handler = test_handler();
306 let response = handler.pass_response();
307 let response_str = String::from_utf8_lossy(response);
308
309 assert!(response_str.starts_with("281"));
311 assert!(response_str.contains("accepted") || response_str.contains("Authentication"));
312 assert!(response_str.ends_with("\r\n"));
313 }
314
315 #[test]
316 fn test_responses_are_static() {
317 let handler = test_handler();
319 let response1 = handler.user_response();
320 let response2 = handler.user_response();
321 assert_eq!(response1.as_ptr(), response2.as_ptr());
322
323 let response3 = handler.pass_response();
324 let response4 = handler.pass_response();
325 assert_eq!(response3.as_ptr(), response4.as_ptr());
326 }
327
328 #[test]
329 fn test_responses_are_different() {
330 let handler = test_handler();
332 let user_resp = handler.user_response();
333 let pass_resp = handler.pass_response();
334 assert_ne!(user_resp, pass_resp);
335 }
336
337 #[test]
338 fn test_responses_are_valid_utf8() {
339 let handler = test_handler();
341 let user_resp = handler.user_response();
342 assert!(std::str::from_utf8(user_resp).is_ok());
343
344 let pass_resp = handler.pass_response();
345 assert!(std::str::from_utf8(pass_resp).is_ok());
346 }
347
348 #[test]
349 fn test_auth_disabled_by_default() {
350 let handler = AuthHandler::default();
351 assert!(!handler.is_enabled());
352 assert!(handler.validate_credentials("any", "thing")); }
354
355 #[test]
356 fn test_auth_new_none_none() {
357 let handler = AuthHandler::new(None, None).unwrap();
358 assert!(!handler.is_enabled());
359 assert!(handler.validate_credentials("any", "thing"));
360 }
361
362 #[test]
363 fn test_auth_enabled_with_credentials() {
364 let handler =
365 AuthHandler::new(Some("mjc".to_string()), Some("nntp1337".to_string())).unwrap();
366 assert!(handler.is_enabled());
367 assert!(handler.validate_credentials("mjc", "nntp1337"));
368 assert!(!handler.validate_credentials("mjc", "wrong"));
369 assert!(!handler.validate_credentials("wrong", "nntp1337"));
370 }
371
372 #[test]
373 fn test_security_empty_credentials_rejected() {
374 let result = AuthHandler::new(Some("".to_string()), Some("pass".to_string()));
376 assert!(
377 result.is_err(),
378 "Empty username should be rejected to prevent silent auth bypass"
379 );
380
381 let result = AuthHandler::new(Some("user".to_string()), Some("".to_string()));
383 assert!(
384 result.is_err(),
385 "Empty password should be rejected to prevent silent auth bypass"
386 );
387
388 let result = AuthHandler::new(Some("".to_string()), Some("".to_string()));
390 assert!(
391 result.is_err(),
392 "Both empty should be rejected to prevent silent auth bypass"
393 );
394 }
395
396 #[test]
397 fn test_security_whitespace_credentials_rejected() {
398 let result = AuthHandler::new(Some(" ".to_string()), Some("pass".to_string()));
400 assert!(
401 result.is_err(),
402 "Whitespace-only username should be rejected"
403 );
404
405 let result = AuthHandler::new(Some("user".to_string()), Some(" ".to_string()));
407 assert!(
408 result.is_err(),
409 "Whitespace-only password should be rejected"
410 );
411 }
412
413 #[test]
414 fn test_security_explicit_config_prevents_silent_bypass() {
415 let username_from_config = Some("".to_string()); let password_from_config = Some("secret".to_string());
425
426 let result = AuthHandler::new(username_from_config, password_from_config);
427
428 assert!(
429 result.is_err(),
430 "Proxy must refuse to start with empty credentials from config. \
431 Silently disabling auth would be a critical security vulnerability!"
432 );
433 }
434}