use crate::error::{Error, Result};
use http_auth::{PasswordClient, PasswordParams};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Credentials {
pub username: String,
pub password: String,
}
impl Credentials {
pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
Credentials {
username: username.into(),
password: password.into(),
}
}
}
pub struct Authenticator {
credentials: Credentials,
client: PasswordClient,
}
impl Authenticator {
pub fn from_challenge(www_authenticate: &str, credentials: Credentials) -> Result<Self> {
let client = PasswordClient::try_from(www_authenticate)
.map_err(|e| Error::Auth(format!("parse WWW-Authenticate: {e}")))?;
Ok(Authenticator {
credentials,
client,
})
}
pub fn authorization(&mut self, method: &str, uri: &str) -> Result<String> {
self.client
.respond(&PasswordParams {
username: &self.credentials.username,
password: &self.credentials.password,
uri,
method,
body: Some(&[]),
})
.map_err(|e| Error::Auth(format!("compute Authorization: {e}")))
}
}
impl core::fmt::Debug for Authenticator {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Authenticator")
.field("username", &self.credentials.username)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
const CHALLENGE: &str = "Digest realm=\"IP Camera\",nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",qop=\"auth\",algorithm=MD5";
#[test]
fn digest_authorization_contains_required_fields() {
let mut auth =
Authenticator::from_challenge(CHALLENGE, Credentials::new("admin", "12345")).unwrap();
let value = auth
.authorization("DESCRIBE", "rtsp://camera.example.com/live")
.unwrap();
assert!(value.starts_with("Digest "), "got: {value}");
for needle in ["response=", "realm=", "nonce=", "uri=", "cnonce=", "nc="] {
assert!(value.contains(needle), "missing {needle} in {value}");
}
assert!(value.contains("uri=\"rtsp://camera.example.com/live\""));
}
#[test]
fn basic_authorization_is_computed() {
let mut auth = Authenticator::from_challenge(
"Basic realm=\"IP Camera\"",
Credentials::new("admin", "12345"),
)
.unwrap();
let value = auth.authorization("DESCRIBE", "rtsp://c/live").unwrap();
assert!(value.starts_with("Basic "));
assert_ne!(value, "Basic ");
}
}