use base64::Engine;
use std::fmt;
use url::Url;
use crate::auth::digest_auth::{AuthChallenge, AuthProvider, AuthScheme, Secret};
use crate::error::{Aria2Error, Result};
#[derive(Clone)]
pub struct BasicAuthProvider {
username: Secret<String>,
password: Secret<String>,
https_only: bool,
}
impl BasicAuthProvider {
pub fn new(username: String, password: String, https_only: bool) -> Self {
BasicAuthProvider {
username: Secret::new(username),
password: Secret::new(password),
https_only,
}
}
pub fn is_https_only(&self) -> bool {
self.https_only
}
pub fn build_authorization_header_with_url(
&self,
challenge: &AuthChallenge,
url: &str,
) -> Result<String> {
if self.https_only {
let parsed_url =
Url::parse(url).map_err(|e| Aria2Error::Parse(format!("Invalid URL: {}", e)))?;
if parsed_url.scheme() != "https" {
return Err(Aria2Error::DownloadFailed(
"Basic authentication requires HTTPS connection to prevent credential \
interception"
.to_string(),
));
}
}
self.build_authorization_header(challenge)
}
fn encode_credentials(&self) -> String {
let creds = format!(
"{}:{}",
self.username.expose_secret(),
self.password.expose_secret()
);
base64::engine::general_purpose::STANDARD.encode(creds)
}
}
impl fmt::Debug for BasicAuthProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BasicAuthProvider")
.field("username", &self.username)
.field("password", &self.password)
.field("https_only", &self.https_only)
.finish()
}
}
#[async_trait::async_trait]
impl AuthProvider for BasicAuthProvider {
fn scheme(&self) -> AuthScheme {
AuthScheme::Basic
}
fn build_authorization_header(&self, _challenge: &AuthChallenge) -> Result<String> {
let encoded = self.encode_credentials();
Ok(format!("Basic {}", encoded))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_auth_construction() {
let provider =
BasicAuthProvider::new("testuser".to_string(), "testpass".to_string(), false);
assert!(!provider.is_https_only());
assert_eq!(provider.scheme(), AuthScheme::Basic);
}
#[test]
fn test_basic_auth_with_https_enforcement() {
let provider =
BasicAuthProvider::new("secure_user".to_string(), "secure_pass".to_string(), true);
assert!(provider.is_https_only());
}
#[test]
fn test_basic_auth_debug_masking() {
let provider =
BasicAuthProvider::new("admin".to_string(), "super-secret".to_string(), true);
let debug_output = format!("{:?}", provider);
assert!(debug_output.contains("Secret(***)"));
assert!(!debug_output.contains("super-secret"));
assert!(!debug_output.contains("admin"));
}
#[test]
fn test_basic_auth_clone() {
let provider =
BasicAuthProvider::new("original".to_string(), "password".to_string(), false);
let cloned = provider.clone();
let challenge = AuthChallenge {
scheme: AuthScheme::Basic,
realm: "Test".to_string(),
nonce: None,
opaque: None,
qop: None,
stale: false,
};
let result1 = provider.build_authorization_header(&challenge).unwrap();
let result2 = cloned.build_authorization_header(&challenge).unwrap();
assert_eq!(result1, result2);
}
#[test]
fn test_basic_auth_special_characters_in_credentials() {
let provider = BasicAuthProvider::new(
"user@domain.com".to_string(),
"p@ss:w0rd!".to_string(),
false,
);
let challenge = AuthChallenge {
scheme: AuthScheme::Basic,
realm: "Test".to_string(),
nonce: None,
opaque: None,
qop: None,
stale: false,
};
let result = provider.build_authorization_header(&challenge).unwrap();
assert!(result.starts_with("Basic "));
let encoded_part = result.trim_start_matches("Basic ");
let decoded = base64::engine::general_purpose::STANDARD
.decode(encoded_part)
.unwrap();
let decoded_str = String::from_utf8(decoded).unwrap();
assert_eq!(decoded_str, "user@domain.com:p@ss:w0rd!");
}
#[test]
fn test_basic_auth_empty_password() {
let provider = BasicAuthProvider::new("user".to_string(), "".to_string(), false);
let challenge = AuthChallenge {
scheme: AuthScheme::Basic,
realm: "Test".to_string(),
nonce: None,
opaque: None,
qop: None,
stale: false,
};
let result = provider.build_authorization_header(&challenge).unwrap();
assert_eq!(result, "Basic dXNlcjo=");
}
#[test]
fn test_basic_auth_unicode_credentials() {
let provider = BasicAuthProvider::new("用户名".to_string(), "密码".to_string(), false);
let challenge = AuthChallenge {
scheme: AuthScheme::Basic,
realm: "Test".to_string(),
nonce: None,
opaque: None,
qop: None,
stale: false,
};
let result = provider.build_authorization_header(&challenge).unwrap();
assert!(result.starts_with("Basic "));
let encoded_part = result.trim_start_matches("Basic ");
let decoded = base64::engine::general_purpose::STANDARD
.decode(encoded_part)
.unwrap();
let decoded_str = String::from_utf8(decoded).unwrap();
assert_eq!(decoded_str, "用户名:密码");
}
}