#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
#![deny(trivial_casts, trivial_numeric_casts, unused_extern_crates, unused_qualifications)]
#![warn(
missing_debug_implementations,
missing_docs,
unused_import_braces,
dead_code,
clippy::unwrap_used,
clippy::expect_used,
clippy::missing_docs_in_private_items
)]
use std::{borrow::Borrow, fmt::Debug};
pub use error::ParseError;
use http::{header::InvalidHeaderValue, HeaderMap, HeaderValue};
use crate::error::CredentialsDecodeError;
mod error;
#[derive(Debug)]
pub enum AuthorizationHeader {
Basic {
username: String,
password: String,
},
Bearer {
token: String,
},
}
impl TryFrom<HeaderMap> for AuthorizationHeader {
type Error = ParseError;
fn try_from(value: HeaderMap) -> Result<Self, Self::Error> {
let auth_head_parts: Vec<&str> = value
.get(http::header::AUTHORIZATION)
.ok_or(ParseError::AuthHeaderMissing)?
.to_str()
.map_err(|_| ParseError::InvalidCharacters)?
.split(' ')
.collect();
let (auth_type, auth_content) =
auth_head_parts.split_first().ok_or(ParseError::BadFormat)?;
match (auth_type.to_lowercase().as_str(), auth_content) {
("basic", [auth_content, ..]) => Ok(Self::parse_basic_auth(auth_content)?),
("bearer", [auth_content, ..]) => {
Ok(Self::Bearer { token: (*auth_content).to_string() })
}
(auth_type, [..]) => Err(ParseError::unknown_authentication_type(auth_type)),
}
}
}
impl TryFrom<&HeaderMap> for AuthorizationHeader {
type Error = ParseError;
fn try_from(value: &HeaderMap) -> Result<Self, Self::Error> {
value.to_owned().try_into()
}
}
impl TryFrom<&AuthorizationHeader> for HeaderMap {
type Error = ParseError;
fn try_from(auth: &AuthorizationHeader) -> Result<Self, Self::Error> {
let mut headers = HeaderMap::new();
headers.insert(http::header::AUTHORIZATION, auth.header_value()?);
Ok(headers)
}
}
impl TryFrom<AuthorizationHeader> for HeaderMap {
type Error = ParseError;
fn try_from(auth: AuthorizationHeader) -> Result<Self, Self::Error> {
auth.borrow().try_into()
}
}
impl AuthorizationHeader {
pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
Self::Basic { username: username.into(), password: password.into() }
}
pub fn bearer(token: impl Into<String>) -> Self {
Self::Bearer { token: token.into() }
}
fn parse_basic_auth(auth: &str) -> Result<Self, CredentialsDecodeError> {
let auth_string = String::from_utf8(base64::decode(auth)?)?;
let (username, password) =
auth_string.split_once(':').ok_or(CredentialsDecodeError::DelimiterNotFound)?;
Ok(Self::Basic { username: username.to_owned(), password: password.to_owned() })
}
pub fn header_value(&self) -> Result<HeaderValue, InvalidHeaderValue> {
let value = match self {
Self::Basic { username, password } => {
format!("Basic {}", base64::encode(format!("{}:{}", username, password)))
}
Self::Bearer { token } => format!("Bearer {}", token),
};
value.parse()
}
}
#[cfg(test)]
mod tests {
use eyre::{bail, Result};
use super::*;
#[test]
fn header_parse_basic() -> Result<()> {
let mut header_map = http::header::HeaderMap::new();
header_map.insert(http::header::AUTHORIZATION, "Basic YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);
let auth = AuthorizationHeader::try_from(&header_map)?;
if let AuthorizationHeader::Basic { username, password } = auth {
assert_eq!("aladdin", username.as_str());
assert_eq!("opensesame", password.as_str());
} else {
bail!("Parsed header wasn't identified as Basic")
}
Ok(())
}
#[test]
fn header_parse_bearer() -> Result<()> {
let mut header_map = http::header::HeaderMap::new();
header_map.insert(http::header::AUTHORIZATION, "Bearer YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);
let auth = AuthorizationHeader::try_from(&header_map)?;
if let AuthorizationHeader::Bearer { token } = auth {
assert_eq!("YWxhZGRpbjpvcGVuc2VzYW1l", token.as_str());
} else {
bail!("Parsed header wasn't identified as Basic")
}
Ok(())
}
#[test]
fn header_parse_unimplemented() -> Result<()> {
let mut header_map = http::header::HeaderMap::new();
header_map
.insert(http::header::AUTHORIZATION, "FoxieAuth YWxhZGRpbjpvcGVuc2VzYW1l".parse()?);
let res = AuthorizationHeader::try_from(&header_map);
match res {
Err(ParseError::UnknownAuthenticationType(auth_type)) => {
assert_eq!("foxieauth", auth_type.as_str())
}
Err(_) => bail!("Wrong error type"),
Ok(_) => bail!("This authorization type shouldn't work at all"),
}
Ok(())
}
#[test]
fn header_create_basic_auth() -> Result<()> {
let header = AuthorizationHeader::basic("aladdin", "opensesame");
assert_eq!(
HeaderValue::from_str("Basic YWxhZGRpbjpvcGVuc2VzYW1l")?,
header.header_value()?
);
Ok(())
}
#[test]
fn header_create_bearer_auth() -> Result<()> {
let header = &AuthorizationHeader::bearer("fox");
let mut header_map = HeaderMap::new();
header_map.insert(http::header::AUTHORIZATION, HeaderValue::from_str("Bearer fox")?);
assert_eq!(header_map, header.try_into()?);
Ok(())
}
}