use std::fmt;
use std::time::Duration;
use std::sync::Arc;
use http::StatusCode;
use http::header::{AUTHORIZATION, CONTENT_TYPE};
use secrecy::{ExposeSecret, SecretSlice, SecretString};
use url::Url;
use crate::redact::RedactedUrl;
const FHIR_JSON: &str = "application/fhir+json";
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum FeedAddressError {
#[error("the audit repository FHIR base is not an https URL without a query or a fragment")]
Base,
#[error("the audit repository FHIR base is not https")]
Cleartext,
}
#[async_trait::async_trait]
pub trait Credentials: Send + Sync {
async fn authorization(&self) -> Result<SecretString, CredentialsError>;
async fn refused(&self, refused: &SecretString);
}
#[derive(Debug, thiserror::Error)]
#[error("no credential for the audit repository could be had")]
pub struct CredentialsError(#[source] pub Box<dyn std::error::Error + Send + Sync>);
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FeedError {
#[error("the audit repository refused the AuditEvent with {status}")]
Rejected {
status: StatusCode,
},
#[error("the audit repository did not accept the credential")]
Unauthorized,
#[error("no credential for the audit repository could be had")]
Credentials(#[source] CredentialsError),
#[error("the audit repository answered {status}")]
Unavailable {
status: StatusCode,
},
#[error("the audit repository could not be reached")]
Transport(#[source] reqwest::Error),
}
#[derive(Clone)]
pub struct FeedRepository {
endpoint: Url,
http: reqwest::Client,
timeout: Duration,
credentials: Option<Arc<dyn Credentials>>,
}
impl FeedRepository {
pub fn new(
base: Url,
http: reqwest::Client,
timeout: Duration,
) -> Result<Self, FeedAddressError> {
if base.scheme() != "https" {
return if base.scheme() == "http" {
Err(FeedAddressError::Cleartext)
} else {
Err(FeedAddressError::Base)
};
}
Self::any_scheme(base, http, timeout)
}
pub fn cleartext(
base: Url,
http: reqwest::Client,
timeout: Duration,
) -> Result<Self, FeedAddressError> {
Self::any_scheme(base, http, timeout)
}
fn any_scheme(
base: Url,
http: reqwest::Client,
timeout: Duration,
) -> Result<Self, FeedAddressError> {
let endpoint =
crate::fhir_base::under_base(base, "AuditEvent").ok_or(FeedAddressError::Base)?;
Ok(Self {
endpoint,
http,
timeout,
credentials: None,
})
}
#[must_use]
pub fn with_credentials(mut self, credentials: Arc<dyn Credentials>) -> Self {
self.credentials = Some(credentials);
self
}
#[must_use]
pub fn endpoint(&self) -> &Url {
&self.endpoint
}
pub async fn send(&self, record: &SecretSlice<u8>) -> Result<(), FeedError> {
let Some(credentials) = &self.credentials else {
return outcome(self.post(record, None).await?);
};
let first = credentials
.authorization()
.await
.map_err(FeedError::Credentials)?;
let status = self.post(record, Some(&first)).await?;
if status != StatusCode::UNAUTHORIZED {
return outcome(status);
}
credentials.refused(&first).await;
let fresh = credentials
.authorization()
.await
.map_err(FeedError::Credentials)?;
outcome(self.post(record, Some(&fresh)).await?)
}
async fn post(
&self,
record: &SecretSlice<u8>,
authorization: Option<&SecretString>,
) -> Result<StatusCode, FeedError> {
let mut request = self
.http
.post(self.endpoint.clone())
.header(CONTENT_TYPE, FHIR_JSON)
.body(record.expose_secret().to_vec())
.timeout(self.timeout);
if let Some(value) = authorization {
let mut header = http::HeaderValue::from_str(value.expose_secret())
.map_err(|invalid| FeedError::Credentials(CredentialsError(Box::new(invalid))))?;
header.set_sensitive(true);
request = request.header(AUTHORIZATION, header);
}
let answer = request
.send()
.await
.map_err(|error| FeedError::Transport(error.without_url()))?;
Ok(answer.status())
}
}
fn outcome(status: StatusCode) -> Result<(), FeedError> {
if status.is_success() {
return Ok(());
}
if status == StatusCode::UNAUTHORIZED {
return Err(FeedError::Unauthorized);
}
if status.is_client_error()
&& status != StatusCode::REQUEST_TIMEOUT
&& status != StatusCode::TOO_MANY_REQUESTS
{
return Err(FeedError::Rejected { status });
}
Err(FeedError::Unavailable { status })
}
impl fmt::Debug for FeedRepository {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FeedRepository")
.field("endpoint", &RedactedUrl(self.endpoint.as_str()))
.field("timeout", &self.timeout)
.field("credentials", &self.credentials.is_some())
.finish_non_exhaustive()
}
}