use crate::command::AuthAction;
use crate::protocol::{AUTH_ACCEPTED, AUTH_FAILED, AUTH_OUT_OF_SEQUENCE, AUTH_REQUIRED};
use crate::types::{Password, Username, ValidationError};
use std::collections::HashMap;
use tokio::io::AsyncWriteExt;
#[derive(Default)]
pub struct AuthHandler {
users: HashMap<String, String>,
}
impl std::fmt::Debug for AuthHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthHandler")
.field("enabled", &!self.users.is_empty())
.field("user_count", &self.users.len())
.finish_non_exhaustive()
}
}
impl AuthHandler {
pub fn new(
username: Option<String>,
password: Option<String>,
) -> Result<Self, ValidationError> {
let mut users = HashMap::new();
if let (Some(u), Some(p)) = (username, password) {
let username = Username::try_new(u)?; let password = Password::try_new(p)?; users.insert(username.as_str().to_string(), password.as_str().to_string());
}
Ok(Self { users })
}
pub fn with_users(user_list: Vec<(String, String)>) -> Result<Self, ValidationError> {
let mut users = HashMap::new();
for (u, p) in user_list {
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());
}
Ok(Self { users })
}
#[inline]
#[must_use]
pub fn is_enabled(&self) -> bool {
!self.users.is_empty()
}
#[must_use]
pub fn validate_credentials(&self, username: &str, password: &str) -> bool {
if self.users.is_empty() {
true
} else {
self.users
.get(username)
.is_some_and(|stored_pass| stored_pass == password)
}
}
pub async fn handle_auth_command<W>(
&self,
auth_action: AuthAction<'_>,
writer: &mut W,
stored_username: Option<&str>,
) -> std::io::Result<(usize, bool)>
where
W: AsyncWriteExt + Unpin,
{
match auth_action {
AuthAction::RequestPassword(_username) => {
writer.write_all(AUTH_REQUIRED).await?;
Ok((AUTH_REQUIRED.len(), false))
}
AuthAction::ValidateAndRespond { password } => {
if stored_username.is_none() {
writer.write_all(AUTH_OUT_OF_SEQUENCE).await?;
return Ok((AUTH_OUT_OF_SEQUENCE.len(), false));
}
let auth_success = stored_username
.is_some_and(|username| self.validate_credentials(username, password));
let response = if auth_success {
AUTH_ACCEPTED
} else {
AUTH_FAILED
};
writer.write_all(response).await?;
Ok((response.len(), auth_success))
}
AuthAction::UnknownSubcommand => {
use crate::protocol::AUTH_UNKNOWN_SUBCOMMAND;
writer.write_all(AUTH_UNKNOWN_SUBCOMMAND).await?;
Ok((AUTH_UNKNOWN_SUBCOMMAND.len(), false))
}
}
}
#[inline]
#[must_use]
pub const fn user_response(&self) -> &'static [u8] {
AUTH_REQUIRED
}
#[inline]
#[must_use]
pub const fn pass_response(&self) -> &'static [u8] {
AUTH_ACCEPTED
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_handler() -> AuthHandler {
AuthHandler::default()
}
mod auth_handler {
use super::*;
#[test]
fn test_default() {
let handler = AuthHandler::default();
assert!(!handler.is_enabled());
}
#[test]
fn test_new_with_both_credentials() {
let handler =
AuthHandler::new(Some("user".to_string()), Some("pass".to_string())).unwrap();
assert!(handler.is_enabled());
}
#[test]
fn test_new_with_only_username() {
let handler = AuthHandler::new(Some("user".to_string()), None).unwrap();
assert!(!handler.is_enabled());
}
#[test]
fn test_new_with_only_password() {
let handler = AuthHandler::new(None, Some("pass".to_string())).unwrap();
assert!(!handler.is_enabled());
}
#[test]
fn test_new_with_neither() {
let handler = AuthHandler::new(None, None).unwrap();
assert!(!handler.is_enabled());
}
#[test]
fn test_with_users_multiple() {
let users = vec![
("alice".to_string(), "secret1".to_string()),
("bob".to_string(), "secret2".to_string()),
("charlie".to_string(), "secret3".to_string()),
];
let handler = AuthHandler::with_users(users).unwrap();
assert!(handler.is_enabled());
assert!(handler.validate_credentials("alice", "secret1"));
assert!(handler.validate_credentials("bob", "secret2"));
assert!(handler.validate_credentials("charlie", "secret3"));
assert!(!handler.validate_credentials("alice", "wrong"));
assert!(!handler.validate_credentials("bob", "secret1")); assert!(!handler.validate_credentials("dave", "anything")); }
#[test]
fn test_with_users_empty() {
let handler = AuthHandler::with_users(vec![]).unwrap();
assert!(!handler.is_enabled());
assert!(handler.validate_credentials("anyone", "anything")); }
#[test]
fn test_with_users_rejects_empty_username() {
let users = vec![
("alice".to_string(), "pass1".to_string()),
(String::new(), "pass2".to_string()), ];
let result = AuthHandler::with_users(users);
assert!(result.is_err());
}
#[test]
fn test_with_users_rejects_empty_password() {
let users = vec![
("alice".to_string(), "pass1".to_string()),
("bob".to_string(), String::new()), ];
let result = AuthHandler::with_users(users);
assert!(result.is_err());
}
#[test]
fn test_new_with_empty_username_fails() {
let result = AuthHandler::new(Some(String::new()), Some("pass".to_string()));
assert!(result.is_err(), "Empty username should return error");
}
#[test]
fn test_new_with_empty_password_fails() {
let result = AuthHandler::new(Some("user".to_string()), Some(String::new()));
assert!(result.is_err(), "Empty password should return error");
}
#[test]
fn test_new_with_whitespace_username_fails() {
let result = AuthHandler::new(Some(" ".to_string()), Some("pass".to_string()));
assert!(
result.is_err(),
"Whitespace-only username should return error"
);
}
#[test]
fn test_new_with_whitespace_password_fails() {
let result = AuthHandler::new(Some("user".to_string()), Some(" ".to_string()));
assert!(
result.is_err(),
"Whitespace-only password should return error"
);
}
#[test]
fn test_validate_when_disabled() {
let handler = AuthHandler::new(None, None).unwrap();
assert!(handler.validate_credentials("any", "thing"));
assert!(handler.validate_credentials("", ""));
assert!(handler.validate_credentials("foo", "bar"));
}
#[test]
fn test_validate_when_enabled() {
let handler =
AuthHandler::new(Some("alice".to_string()), Some("secret".to_string())).unwrap();
assert!(handler.validate_credentials("alice", "secret"));
assert!(!handler.validate_credentials("alice", "wrong"));
assert!(!handler.validate_credentials("bob", "secret"));
assert!(!handler.validate_credentials("bob", "wrong"));
}
#[test]
fn test_is_enabled_consistent() {
let disabled = AuthHandler::new(None, None).unwrap();
assert!(!disabled.is_enabled());
assert!(!disabled.is_enabled());
let enabled = AuthHandler::new(Some("u".to_string()), Some("p".to_string())).unwrap();
assert!(enabled.is_enabled());
assert!(enabled.is_enabled()); }
}
#[test]
fn test_user_response() {
let handler = test_handler();
let response = handler.user_response();
let response_str = String::from_utf8_lossy(response);
assert!(response_str.starts_with("381"));
assert!(response_str.contains("Password required") || response_str.contains("password"));
assert!(response_str.ends_with("\r\n"));
}
#[test]
fn test_pass_response() {
let handler = test_handler();
let response = handler.pass_response();
let response_str = String::from_utf8_lossy(response);
assert!(response_str.starts_with("281"));
assert!(response_str.contains("accepted") || response_str.contains("Authentication"));
assert!(response_str.ends_with("\r\n"));
}
#[test]
fn test_responses_are_static() {
let handler = test_handler();
let response1 = handler.user_response();
let response2 = handler.user_response();
assert_eq!(response1.as_ptr(), response2.as_ptr());
let response3 = handler.pass_response();
let response4 = handler.pass_response();
assert_eq!(response3.as_ptr(), response4.as_ptr());
}
#[test]
fn test_responses_are_different() {
let handler = test_handler();
let user_resp = handler.user_response();
let pass_resp = handler.pass_response();
assert_ne!(user_resp, pass_resp);
}
#[test]
fn test_responses_are_valid_utf8() {
let handler = test_handler();
let user_resp = handler.user_response();
assert!(std::str::from_utf8(user_resp).is_ok());
let pass_resp = handler.pass_response();
assert!(std::str::from_utf8(pass_resp).is_ok());
}
#[test]
fn test_auth_disabled_by_default() {
let handler = AuthHandler::default();
assert!(!handler.is_enabled());
assert!(handler.validate_credentials("any", "thing")); }
#[test]
fn test_auth_new_none_none() {
let handler = AuthHandler::new(None, None).unwrap();
assert!(!handler.is_enabled());
assert!(handler.validate_credentials("any", "thing"));
}
#[test]
fn test_auth_enabled_with_credentials() {
let handler =
AuthHandler::new(Some("mjc".to_string()), Some("nntp1337".to_string())).unwrap();
assert!(handler.is_enabled());
assert!(handler.validate_credentials("mjc", "nntp1337"));
assert!(!handler.validate_credentials("mjc", "wrong"));
assert!(!handler.validate_credentials("wrong", "nntp1337"));
}
#[test]
fn test_security_empty_credentials_rejected() {
let result = AuthHandler::new(Some(String::new()), Some("pass".to_string()));
assert!(
result.is_err(),
"Empty username should be rejected to prevent silent auth bypass"
);
let result = AuthHandler::new(Some("user".to_string()), Some(String::new()));
assert!(
result.is_err(),
"Empty password should be rejected to prevent silent auth bypass"
);
let result = AuthHandler::new(Some(String::new()), Some(String::new()));
assert!(
result.is_err(),
"Both empty should be rejected to prevent silent auth bypass"
);
}
#[test]
fn test_security_whitespace_credentials_rejected() {
let result = AuthHandler::new(Some(" ".to_string()), Some("pass".to_string()));
assert!(
result.is_err(),
"Whitespace-only username should be rejected"
);
let result = AuthHandler::new(Some("user".to_string()), Some(" ".to_string()));
assert!(
result.is_err(),
"Whitespace-only password should be rejected"
);
}
#[test]
fn test_security_explicit_config_prevents_silent_bypass() {
let username_from_config = Some(String::new()); let password_from_config = Some("secret".to_string());
let result = AuthHandler::new(username_from_config, password_from_config);
assert!(
result.is_err(),
"Proxy must refuse to start with empty credentials from config. \
Silently disabling auth would be a critical security vulnerability!"
);
}
}