use std::{future::Future, pin::Pin, sync::Arc};
use crate::{BoxError, Request};
pub type AuthFuture<'a> = Pin<Box<dyn Future<Output = Result<Request, BoxError>> + Send + 'a>>;
#[derive(Debug, Clone, Copy)]
pub struct OperationSecurity(pub &'static [&'static [&'static str]]);
impl OperationSecurity {
pub const PUBLIC: Self = Self(&[]);
pub fn is_public(&self) -> bool {
self.0.is_empty()
}
}
pub trait SecurityCredential {
fn apply(&self, req: Request) -> impl Future<Output = Result<Request, BoxError>> + Send;
}
pub trait AuthSelector: Send + Sync + 'static {
fn apply_for(
&self,
req: Request,
requirements: &'static [&'static [&'static str]],
) -> AuthFuture<'_>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoAuth;
impl AuthSelector for NoAuth {
fn apply_for(
&self,
req: Request,
requirements: &'static [&'static [&'static str]],
) -> AuthFuture<'_> {
Box::pin(async move {
if requirements.is_empty() {
return Ok(req);
}
let schemes: Vec<&'static str> = requirements
.iter()
.flat_map(|alt| alt.iter().copied())
.collect();
Err(format!(
"operation requires auth ({schemes:?}) but no credentials were configured; \
call ApiClient::with_auth before dispatching"
)
.into())
})
}
}
pub(crate) fn default_auth() -> Arc<dyn AuthSelector> {
Arc::new(NoAuth)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiKeyLocation {
Header,
Query,
Cookie,
}
#[derive(Debug, Clone)]
pub struct ApiKeyCredential {
pub name: &'static str,
pub location: ApiKeyLocation,
pub value: String,
}
impl SecurityCredential for ApiKeyCredential {
fn apply(&self, req: Request) -> impl Future<Output = Result<Request, BoxError>> + Send {
let result = match self.location {
ApiKeyLocation::Header => apply_header(req, self.name, &self.value),
ApiKeyLocation::Query => apply_query(req, self.name, &self.value),
ApiKeyLocation::Cookie => apply_cookie(req, self.name, &self.value),
};
std::future::ready(result)
}
}
#[derive(Debug, Clone)]
pub struct BearerCredential {
pub token: String,
}
impl SecurityCredential for BearerCredential {
fn apply(&self, req: Request) -> impl Future<Output = Result<Request, BoxError>> + Send {
let header_value = format!("Bearer {}", self.token);
let result = apply_header(req, http::header::AUTHORIZATION.as_str(), &header_value);
std::future::ready(result)
}
}
#[derive(Debug, Clone)]
pub struct BasicCredential {
pub username: String,
pub password: String,
}
impl SecurityCredential for BasicCredential {
fn apply(&self, req: Request) -> impl Future<Output = Result<Request, BoxError>> + Send {
use base64::Engine as _;
let combined = format!("{}:{}", self.username, self.password);
let encoded = base64::engine::general_purpose::STANDARD.encode(combined.as_bytes());
let header_value = format!("Basic {encoded}");
let result = apply_header(req, http::header::AUTHORIZATION.as_str(), &header_value);
std::future::ready(result)
}
}
fn apply_header(mut req: Request, name: &str, value: &str) -> Result<Request, BoxError> {
let header_name = http::HeaderName::try_from(name)?;
let header_value = http::HeaderValue::try_from(value)?;
req.headers_mut().insert(header_name, header_value);
Ok(req)
}
fn apply_query(req: Request, name: &str, value: &str) -> Result<Request, BoxError> {
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
const QUERY_COMPONENT: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'%')
.add(b'&')
.add(b'+')
.add(b'/')
.add(b'<')
.add(b'=')
.add(b'>')
.add(b'?')
.add(b'@')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
let encoded_name = utf8_percent_encode(name, QUERY_COMPONENT).to_string();
let encoded_value = utf8_percent_encode(value, QUERY_COMPONENT).to_string();
let (mut parts, body) = req.into_parts();
let uri = parts.uri.clone();
let path = uri.path();
let existing = uri.query();
let sep = if existing.is_some() { '&' } else { '?' };
let mut new_path_and_query = String::with_capacity(
path.len() + existing.map_or(0, str::len) + encoded_name.len() + encoded_value.len() + 3,
);
new_path_and_query.push_str(path);
if let Some(existing) = existing {
new_path_and_query.push('?');
new_path_and_query.push_str(existing);
}
new_path_and_query.push(sep);
new_path_and_query.push_str(&encoded_name);
new_path_and_query.push('=');
new_path_and_query.push_str(&encoded_value);
let mut uri_parts = uri.into_parts();
uri_parts.path_and_query = Some(new_path_and_query.parse()?);
parts.uri = http::Uri::from_parts(uri_parts)?;
Ok(Request::from_parts(parts, body))
}
fn apply_cookie(mut req: Request, name: &str, value: &str) -> Result<Request, BoxError> {
let pair = format!("{name}={value}");
let headers = req.headers_mut();
let new_value = match headers.get(http::header::COOKIE) {
Some(existing) => {
let existing = existing.to_str()?;
http::HeaderValue::try_from(format!("{existing}; {pair}"))?
}
None => http::HeaderValue::try_from(pair)?,
};
headers.insert(http::header::COOKIE, new_value);
Ok(req)
}