use base64::prelude::*;
use dragonfly_client_core::{
error::{ErrorType, OrErr},
Error, Result,
};
use http::header::{self, HeaderMap};
pub struct Credentials {
pub username: String,
pub password: String,
}
impl Credentials {
pub fn new(username: &str, password: &str) -> Credentials {
Self {
username: username.to_string(),
password: password.to_string(),
}
}
pub fn verify(&self, header: &HeaderMap) -> Result<()> {
let Some(auth_header) = header.get(header::AUTHORIZATION) else {
return Err(Error::Unauthorized);
};
if let Some((typ, payload)) = auth_header
.to_str()
.or_err(ErrorType::ParseError)?
.to_string()
.split_once(' ')
{
if typ.to_lowercase() != "basic" {
return Err(Error::Unauthorized);
};
let decoded = String::from_utf8(
BASE64_STANDARD
.decode(payload)
.or_err(ErrorType::ParseError)?,
)
.or_err(ErrorType::ParseError)?;
let Some((username, password)) = decoded.split_once(':') else {
return Err(Error::Unauthorized);
};
if username != self.username || password != self.password {
return Err(Error::Unauthorized);
}
return Ok(());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::type_complexity)]
use super::*;
use http::header::{HeaderValue, AUTHORIZATION};
#[test]
fn verify_accepts_only_matching_basic_credentials() {
let test_cases: Vec<(Option<&str>, fn(Result<()>))> = vec![
(None, |result| {
assert!(matches!(result, Err(Error::Unauthorized)))
}),
(Some("Bearer some_token"), |result| {
assert!(matches!(result, Err(Error::Unauthorized)))
}),
(Some("Basic invalid_base64"), |result| {
assert_eq!(
result.unwrap_err().to_string(),
"ParseError cause: Invalid symbol 95, offset 7."
)
}),
(Some("Basic //46eA=="), |result| {
assert_eq!(
result.unwrap_err().to_string(),
"ParseError cause: invalid utf-8 sequence of 1 bytes from index 0"
)
}),
(Some("Basic dXNlcg=="), |result| {
assert!(matches!(result, Err(Error::Unauthorized)))
}),
(Some("Basic dXNlcjpwYXNzX2Vycm9y"), |result| {
assert!(matches!(result, Err(Error::Unauthorized)))
}),
(Some("Basic dXNlcjpwYXNz"), |result| assert!(result.is_ok())),
(Some("basic dXNlcjpwYXNz"), |result| assert!(result.is_ok())),
];
let credentials = Credentials::new("user", "pass");
for (authorization, expect) in test_cases {
let mut header = HeaderMap::new();
if let Some(authorization) = authorization {
header.insert(AUTHORIZATION, HeaderValue::from_str(authorization).unwrap());
}
expect(credentials.verify(&header));
}
}
}