aria2_core/auth/
basic_auth.rs1use base64::Engine;
13use std::fmt;
14use url::Url;
15
16use crate::auth::digest_auth::{AuthChallenge, AuthProvider, AuthScheme, Secret};
17use crate::error::{Aria2Error, Result};
18
19#[derive(Clone)]
35pub struct BasicAuthProvider {
36 username: Secret<String>,
38 password: Secret<String>,
40 https_only: bool,
42}
43
44impl BasicAuthProvider {
45 pub fn new(username: String, password: String, https_only: bool) -> Self {
57 BasicAuthProvider {
58 username: Secret::new(username),
59 password: Secret::new(password),
60 https_only,
61 }
62 }
63
64 pub fn is_https_only(&self) -> bool {
66 self.https_only
67 }
68
69 pub fn build_authorization_header_with_url(
87 &self,
88 challenge: &AuthChallenge,
89 url: &str,
90 ) -> Result<String> {
91 if self.https_only {
92 let parsed_url =
93 Url::parse(url).map_err(|e| Aria2Error::Parse(format!("Invalid URL: {}", e)))?;
94
95 if parsed_url.scheme() != "https" {
96 return Err(Aria2Error::DownloadFailed(
97 "Basic authentication requires HTTPS connection to prevent credential \
98 interception"
99 .to_string(),
100 ));
101 }
102 }
103
104 self.build_authorization_header(challenge)
105 }
106
107 fn encode_credentials(&self) -> String {
111 let creds = format!(
112 "{}:{}",
113 self.username.expose_secret(),
114 self.password.expose_secret()
115 );
116 base64::engine::general_purpose::STANDARD.encode(creds)
117 }
118}
119
120impl fmt::Debug for BasicAuthProvider {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 f.debug_struct("BasicAuthProvider")
123 .field("username", &self.username)
124 .field("password", &self.password)
125 .field("https_only", &self.https_only)
126 .finish()
127 }
128}
129
130#[async_trait::async_trait]
131impl AuthProvider for BasicAuthProvider {
132 fn scheme(&self) -> AuthScheme {
133 AuthScheme::Basic
134 }
135
136 fn build_authorization_header(&self, _challenge: &AuthChallenge) -> Result<String> {
147 let encoded = self.encode_credentials();
148 Ok(format!("Basic {}", encoded))
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_basic_auth_construction() {
158 let provider =
159 BasicAuthProvider::new("testuser".to_string(), "testpass".to_string(), false);
160
161 assert!(!provider.is_https_only());
162 assert_eq!(provider.scheme(), AuthScheme::Basic);
163 }
164
165 #[test]
166 fn test_basic_auth_with_https_enforcement() {
167 let provider =
168 BasicAuthProvider::new("secure_user".to_string(), "secure_pass".to_string(), true);
169
170 assert!(provider.is_https_only());
171 }
172
173 #[test]
174 fn test_basic_auth_debug_masking() {
175 let provider =
176 BasicAuthProvider::new("admin".to_string(), "super-secret".to_string(), true);
177
178 let debug_output = format!("{:?}", provider);
179
180 assert!(debug_output.contains("Secret(***)"));
182 assert!(!debug_output.contains("super-secret"));
183 assert!(!debug_output.contains("admin"));
184 }
185
186 #[test]
187 fn test_basic_auth_clone() {
188 let provider =
189 BasicAuthProvider::new("original".to_string(), "password".to_string(), false);
190
191 let cloned = provider.clone();
192
193 let challenge = AuthChallenge {
195 scheme: AuthScheme::Basic,
196 realm: "Test".to_string(),
197 nonce: None,
198 opaque: None,
199 qop: None,
200 stale: false,
201 };
202
203 let result1 = provider.build_authorization_header(&challenge).unwrap();
204 let result2 = cloned.build_authorization_header(&challenge).unwrap();
205
206 assert_eq!(result1, result2);
207 }
208
209 #[test]
210 fn test_basic_auth_special_characters_in_credentials() {
211 let provider = BasicAuthProvider::new(
213 "user@domain.com".to_string(),
214 "p@ss:w0rd!".to_string(),
215 false,
216 );
217
218 let challenge = AuthChallenge {
219 scheme: AuthScheme::Basic,
220 realm: "Test".to_string(),
221 nonce: None,
222 opaque: None,
223 qop: None,
224 stale: false,
225 };
226
227 let result = provider.build_authorization_header(&challenge).unwrap();
228 assert!(result.starts_with("Basic "));
229
230 let encoded_part = result.trim_start_matches("Basic ");
232 let decoded = base64::engine::general_purpose::STANDARD
233 .decode(encoded_part)
234 .unwrap();
235 let decoded_str = String::from_utf8(decoded).unwrap();
236 assert_eq!(decoded_str, "user@domain.com:p@ss:w0rd!");
237 }
238
239 #[test]
240 fn test_basic_auth_empty_password() {
241 let provider = BasicAuthProvider::new("user".to_string(), "".to_string(), false);
243
244 let challenge = AuthChallenge {
245 scheme: AuthScheme::Basic,
246 realm: "Test".to_string(),
247 nonce: None,
248 opaque: None,
249 qop: None,
250 stale: false,
251 };
252
253 let result = provider.build_authorization_header(&challenge).unwrap();
254 assert_eq!(result, "Basic dXNlcjo=");
256 }
257
258 #[test]
259 fn test_basic_auth_unicode_credentials() {
260 let provider = BasicAuthProvider::new("用户名".to_string(), "密码".to_string(), false);
262
263 let challenge = AuthChallenge {
264 scheme: AuthScheme::Basic,
265 realm: "Test".to_string(),
266 nonce: None,
267 opaque: None,
268 qop: None,
269 stale: false,
270 };
271
272 let result = provider.build_authorization_header(&challenge).unwrap();
273 assert!(result.starts_with("Basic "));
274
275 let encoded_part = result.trim_start_matches("Basic ");
277 let decoded = base64::engine::general_purpose::STANDARD
278 .decode(encoded_part)
279 .unwrap();
280 let decoded_str = String::from_utf8(decoded).unwrap();
281 assert_eq!(decoded_str, "用户名:密码");
282 }
283}