1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2#![deny(trivial_casts, trivial_numeric_casts, unused_extern_crates, unused_qualifications)]
3#![warn(
4 missing_debug_implementations,
5 missing_docs,
6 unused_import_braces,
7 dead_code,
8 clippy::unwrap_used,
9 clippy::expect_used,
10 clippy::missing_docs_in_private_items
11)]
12
13use std::{borrow::Borrow, fmt::Debug};
16
17pub use error::ParseError;
18use http::{header::InvalidHeaderValue, HeaderMap, HeaderValue};
19
20use crate::error::CredentialsDecodeError;
21
22mod error;
23
24#[derive(Debug)]
26pub enum AuthorizationHeader {
27 Basic {
29 username: String,
31 password: String,
33 },
34 Bearer {
36 token: String,
38 },
39}
40
41impl TryFrom<HeaderMap> for AuthorizationHeader {
42 type Error = ParseError;
43
44 fn try_from(value: HeaderMap) -> Result<Self, Self::Error> {
45 let auth_head_parts: Vec<&str> = value
46 .get(http::header::AUTHORIZATION)
47 .ok_or(ParseError::AuthHeaderMissing)?
48 .to_str()
49 .map_err(|_| ParseError::InvalidCharacters)?
50 .split(' ')
51 .collect();
52
53 let (auth_type, auth_content) =
54 auth_head_parts.split_first().ok_or(ParseError::BadFormat)?;
55
56 match (auth_type.to_lowercase().as_str(), auth_content) {
57 ("basic", [auth_content, ..]) => Ok(Self::parse_basic_auth(auth_content)?),
58 ("bearer", [auth_content, ..]) => {
59 Ok(Self::Bearer { token: (*auth_content).to_string() })
60 }
61 (auth_type, [..]) => Err(ParseError::unknown_authentication_type(auth_type)),
62 }
63 }
64}
65
66impl TryFrom<&HeaderMap> for AuthorizationHeader {
67 type Error = ParseError;
68
69 fn try_from(value: &HeaderMap) -> Result<Self, Self::Error> {
70 value.to_owned().try_into()
71 }
72}
73
74impl TryFrom<&AuthorizationHeader> for HeaderMap {
75 type Error = ParseError;
76 fn try_from(auth: &AuthorizationHeader) -> Result<Self, Self::Error> {
77 let mut headers = HeaderMap::new();
78 headers.insert(http::header::AUTHORIZATION, auth.header_value()?);
79 Ok(headers)
80 }
81}
82
83impl TryFrom<AuthorizationHeader> for HeaderMap {
84 type Error = ParseError;
85 fn try_from(auth: AuthorizationHeader) -> Result<Self, Self::Error> {
86 auth.borrow().try_into()
87 }
88}
89
90impl AuthorizationHeader {
91 pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
93 Self::Basic { username: username.into(), password: password.into() }
94 }
95
96 pub fn bearer(token: impl Into<String>) -> Self {
98 Self::Bearer { token: token.into() }
99 }
100
101 fn parse_basic_auth(auth: &str) -> Result<Self, CredentialsDecodeError> {
103 let auth_string = String::from_utf8(base64::decode(auth)?)?;
104 let (username, password) =
105 auth_string.split_once(':').ok_or(CredentialsDecodeError::DelimiterNotFound)?;
106 Ok(Self::Basic { username: username.to_owned(), password: password.to_owned() })
107 }
108
109 pub fn header_value(&self) -> Result<HeaderValue, InvalidHeaderValue> {
120 let value = match self {
121 Self::Basic { username, password } => {
122 format!("Basic {}", base64::encode(format!("{}:{}", username, password)))
123 }
124 Self::Bearer { token } => format!("Bearer {}", token),
125 };
126 value.parse()
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use eyre::{bail, Result};
133
134 use super::*;
135
136 #[test]
137 fn header_parse_basic() -> Result<()> {
138 let mut header_map = http::header::HeaderMap::new();
139 header_map.insert(http::header::AUTHORIZATION, "Basic YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);
140
141 let auth = AuthorizationHeader::try_from(&header_map)?;
142 if let AuthorizationHeader::Basic { username, password } = auth {
143 assert_eq!("aladdin", username.as_str());
144 assert_eq!("opensesame", password.as_str());
145 } else {
146 bail!("Parsed header wasn't identified as Basic")
147 }
148 Ok(())
149 }
150
151 #[test]
152 fn header_parse_bearer() -> Result<()> {
153 let mut header_map = http::header::HeaderMap::new();
154 header_map.insert(http::header::AUTHORIZATION, "Bearer YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);
155
156 let auth = AuthorizationHeader::try_from(&header_map)?;
157 if let AuthorizationHeader::Bearer { token } = auth {
158 assert_eq!("YWxhZGRpbjpvcGVuc2VzYW1l", token.as_str());
159 } else {
160 bail!("Parsed header wasn't identified as Basic")
161 }
162 Ok(())
163 }
164
165 #[test]
166 fn header_parse_unimplemented() -> Result<()> {
167 let mut header_map = http::header::HeaderMap::new();
168 header_map
169 .insert(http::header::AUTHORIZATION, "FoxieAuth YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);
170
171 let res = AuthorizationHeader::try_from(&header_map);
172 match res {
173 Err(ParseError::UnknownAuthenticationType(auth_type)) => {
174 assert_eq!("foxieauth", auth_type.as_str())
175 }
176 Err(_) => bail!("Wrong error type"),
177 Ok(_) => bail!("This authorization type shouldn't work at all"),
178 }
179 Ok(())
180 }
181
182 #[test]
183 fn header_create_basic_auth() -> Result<()> {
184 let header = AuthorizationHeader::basic("aladdin", "opensesame");
185 assert_eq!(
186 HeaderValue::from_str("Basic YWxhZGRpbjpvcGVuc2VzYW1l")?,
187 header.header_value()?
188 );
189 Ok(())
190 }
191
192 #[test]
193 fn header_create_bearer_auth() -> Result<()> {
194 let header = &AuthorizationHeader::bearer("fox");
195 let mut header_map = HeaderMap::new();
196 header_map.insert(http::header::AUTHORIZATION, HeaderValue::from_str("Bearer fox")?);
197 assert_eq!(header_map, header.try_into()?);
198 Ok(())
199 }
200}