1use crate::error::{Error, Result};
14use http_auth::{PasswordClient, PasswordParams};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Credentials {
19 pub username: String,
21 pub password: String,
23}
24
25impl Credentials {
26 pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
28 Credentials {
29 username: username.into(),
30 password: password.into(),
31 }
32 }
33}
34
35pub struct Authenticator {
42 credentials: Credentials,
43 client: PasswordClient,
44}
45
46impl Authenticator {
47 pub fn from_challenge(www_authenticate: &str, credentials: Credentials) -> Result<Self> {
53 let client = PasswordClient::try_from(www_authenticate)
54 .map_err(|e| Error::Auth(format!("parse WWW-Authenticate: {e}")))?;
55 Ok(Authenticator {
56 credentials,
57 client,
58 })
59 }
60
61 pub fn authorization(&mut self, method: &str, uri: &str) -> Result<String> {
68 self.client
69 .respond(&PasswordParams {
70 username: &self.credentials.username,
71 password: &self.credentials.password,
72 uri,
73 method,
74 body: Some(&[]),
75 })
76 .map_err(|e| Error::Auth(format!("compute Authorization: {e}")))
77 }
78}
79
80impl core::fmt::Debug for Authenticator {
81 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
82 f.debug_struct("Authenticator")
84 .field("username", &self.credentials.username)
85 .finish_non_exhaustive()
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 const CHALLENGE: &str = "Digest realm=\"IP Camera\",nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",qop=\"auth\",algorithm=MD5";
94
95 #[test]
96 fn digest_authorization_contains_required_fields() {
97 let mut auth =
98 Authenticator::from_challenge(CHALLENGE, Credentials::new("admin", "12345")).unwrap();
99 let value = auth
100 .authorization("DESCRIBE", "rtsp://camera.example.com/live")
101 .unwrap();
102 assert!(value.starts_with("Digest "), "got: {value}");
103 for needle in ["response=", "realm=", "nonce=", "uri=", "cnonce=", "nc="] {
104 assert!(value.contains(needle), "missing {needle} in {value}");
105 }
106 assert!(value.contains("uri=\"rtsp://camera.example.com/live\""));
107 }
108
109 #[test]
110 fn basic_authorization_is_computed() {
111 let mut auth = Authenticator::from_challenge(
112 "Basic realm=\"IP Camera\"",
113 Credentials::new("admin", "12345"),
114 )
115 .unwrap();
116 let value = auth.authorization("DESCRIBE", "rtsp://c/live").unwrap();
117 assert!(value.starts_with("Basic "));
118 assert_ne!(value, "Basic ");
119 }
120}