use std::fmt::{self, Debug, Formatter};
use base64::engine::{Engine, general_purpose};
use salvo_core::http::header::{AUTHORIZATION, HeaderName, HeaderValue, PROXY_AUTHORIZATION};
use salvo_core::http::{Request, Response, StatusCode};
use salvo_core::{Depot, Error, FlowCtrl, Handler, async_trait};
pub const USERNAME_KEY: &str = "::salvo::basic_auth::username";
pub trait BasicAuthValidator: Send + Sync {
fn validate(
&self,
username: &str,
password: &str,
depot: &mut Depot,
) -> impl Future<Output = bool> + Send;
}
pub trait BasicAuthDepotExt {
fn basic_auth_username(&self) -> Option<&str>;
}
impl BasicAuthDepotExt for Depot {
fn basic_auth_username(&self) -> Option<&str> {
self.get::<String>(USERNAME_KEY).map(|v| &**v).ok()
}
}
pub struct BasicAuth<V: BasicAuthValidator> {
realm: String,
header_names: Vec<HeaderName>,
validator: V,
}
impl<V: BasicAuthValidator> Debug for BasicAuth<V> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("BasicAuth")
.field("realm", &self.realm)
.field("header_names", &self.header_names)
.finish()
}
}
impl<V> BasicAuth<V>
where
V: BasicAuthValidator,
{
#[inline]
pub fn new(validator: V) -> Self {
Self {
realm: "realm".to_owned(),
header_names: vec![AUTHORIZATION, PROXY_AUTHORIZATION],
validator,
}
}
#[inline]
#[must_use]
pub fn set_realm(mut self, realm: impl Into<String>) -> Self {
self.realm = realm.into();
self
}
#[inline]
pub fn realm(&self) -> &str {
&self.realm
}
#[inline]
#[must_use]
pub fn set_header_names(mut self, header_names: impl Into<Vec<HeaderName>>) -> Self {
self.header_names = header_names.into();
self
}
#[inline]
pub fn header_names(&self) -> &Vec<HeaderName> {
&self.header_names
}
#[inline]
pub fn header_names_mut(&mut self) -> &mut Vec<HeaderName> {
&mut self.header_names
}
#[inline]
pub fn ask_credentials(&self, res: &mut Response) {
ask_credentials(res, &self.realm)
}
#[inline]
pub fn parse_credentials(&self, req: &Request) -> Result<(String, String), Error> {
parse_credentials(req, &self.header_names)
}
}
#[doc(hidden)]
#[inline]
pub fn ask_credentials(res: &mut Response, realm: impl AsRef<str>) {
let challenge = format!("Basic realm={:?}", realm.as_ref());
let value =
HeaderValue::try_from(challenge).unwrap_or_else(|_| HeaderValue::from_static("Basic"));
res.headers_mut().insert("WWW-Authenticate", value);
res.status_code(StatusCode::UNAUTHORIZED);
}
#[doc(hidden)]
pub fn parse_credentials(
req: &Request,
header_names: &[HeaderName],
) -> Result<(String, String), Error> {
for header_name in header_names {
if let Some(header_value) = req.headers().get(header_name) {
let authorization = header_value.to_str().unwrap_or_default();
if let Some(credentials) = parse_basic_authorization(authorization)? {
return Ok(credentials);
}
}
}
Err(Error::other("parse http header failed"))
}
fn parse_basic_authorization(authorization: &str) -> Result<Option<(String, String)>, Error> {
let Some((scheme, auth)) = authorization.split_once(' ') else {
return Ok(None);
};
if !scheme.eq_ignore_ascii_case("Basic") {
return Ok(None);
}
let auth = auth.trim_start();
if auth.is_empty() {
return Err(Error::other("`authorization` has bad format"));
}
let auth_bytes = general_purpose::STANDARD
.decode(auth)
.map_err(Error::other)?;
let auth = String::from_utf8(auth_bytes)
.map_err(|_| Error::other("credentials contain invalid UTF-8"))?;
if let Some((username, password)) = auth.split_once(':') {
Ok(Some((username.to_owned(), password.to_owned())))
} else {
Err(Error::other("`authorization` has bad format"))
}
}
#[async_trait]
impl<V> Handler for BasicAuth<V>
where
V: BasicAuthValidator + 'static,
{
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
if let Ok((username, password)) = self.parse_credentials(req)
&& self.validator.validate(&username, &password, depot).await
{
depot.insert(USERNAME_KEY, username);
ctrl.call_next(req, depot, res).await;
return;
}
self.ask_credentials(res);
ctrl.skip_rest();
}
}
#[cfg(test)]
mod tests {
use salvo_core::prelude::*;
use salvo_core::test::{ResponseExt, TestClient};
use super::*;
#[handler]
async fn hello() -> &'static str {
"Hello"
}
struct Validator;
impl BasicAuthValidator for Validator {
async fn validate(&self, username: &str, password: &str, _depot: &mut Depot) -> bool {
username == "root" && password == "pwd"
}
}
#[tokio::test]
async fn test_basic_auth() {
let auth_handler = BasicAuth::new(Validator);
let router = Router::with_hoop(auth_handler).goal(hello);
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:8698/")
.basic_auth("root", Some("pwd"))
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("Hello"));
let content = TestClient::get("http://127.0.0.1:8698/")
.basic_auth("root", Some("pwd2"))
.send(&service)
.await
.take_string()
.await
.unwrap();
assert!(content.contains("Unauthorized"));
}
#[test]
fn test_parse_credentials_utf8() {
let mut req = Request::new();
req.headers_mut().insert(
AUTHORIZATION,
"Basic cm9vdDpwd2Q=".parse().unwrap(), );
let result = parse_credentials(&req, &[AUTHORIZATION]);
assert!(result.is_ok());
let (username, password) = result.unwrap();
assert_eq!(username, "root");
assert_eq!(password, "pwd");
}
#[test]
fn test_parse_credentials_invalid_utf8() {
let mut req = Request::new();
req.headers_mut().insert(
AUTHORIZATION,
"Basic //86cHdk".parse().unwrap(), );
let result = parse_credentials(&req, &[AUTHORIZATION]);
assert!(result.is_err());
}
#[test]
fn test_parse_credentials_unicode() {
let mut req = Request::new();
req.headers_mut().insert(
AUTHORIZATION,
"Basic 55So5oi3OuWvhueggQ==".parse().unwrap(), );
let result = parse_credentials(&req, &[AUTHORIZATION]);
assert!(result.is_ok());
let (username, password) = result.unwrap();
assert_eq!(username, "用户");
assert_eq!(password, "密码");
}
#[test]
fn test_parse_credentials_falls_through_non_basic_header() {
let mut req = Request::new();
req.headers_mut()
.insert(AUTHORIZATION, "Bearer sometoken".parse().unwrap());
req.headers_mut()
.insert(PROXY_AUTHORIZATION, "Basic cm9vdDpwd2Q=".parse().unwrap());
let result = parse_credentials(&req, &[AUTHORIZATION, PROXY_AUTHORIZATION]);
assert!(result.is_ok());
let (username, password) = result.unwrap();
assert_eq!(username, "root");
assert_eq!(password, "pwd");
}
#[test]
fn test_parse_credentials_rejects_prefixed_scheme() {
let mut req = Request::new();
req.headers_mut()
.insert(AUTHORIZATION, "BasicX cm9vdDpwd2Q=".parse().unwrap());
assert!(parse_credentials(&req, &[AUTHORIZATION]).is_err());
}
#[test]
fn test_parse_credentials_accepts_case_insensitive_scheme() {
let mut req = Request::new();
req.headers_mut()
.insert(AUTHORIZATION, "basic cm9vdDpwd2Q=".parse().unwrap());
assert_eq!(
parse_credentials(&req, &[AUTHORIZATION]).unwrap(),
("root".to_owned(), "pwd".to_owned())
);
}
}