pub mod error;
pub mod question;
mod request;
mod response;
use std::fmt;
use std::time::Duration;
use http::header::{ACCEPT, CONTENT_TYPE, HeaderName};
use url::Url;
use uuid::Uuid;
use error::{ClientError, InvalidInput, Malformation, MitzError};
use question::{ClosedAnswer, ClosedQuestion};
const SOAP: &str = "http://www.w3.org/2003/05/soap-envelope";
const WSA: &str = "http://www.w3.org/2005/08/addressing";
const XACML: &str = "urn:oasis:names:tc:xacml:3.0:core:schema:wd-17";
const XACML_SAML_PROTOCOL: &str =
"urn:oasis:names:tc:xacml:3.0:profile:saml2.0:v2:schema:protocol:wd-14";
const HL7: &str = "urn:hl7-org:v3";
pub const ACTION: &str = "XACMLAuthorizationDecisionQueryRequest";
pub const BSN_ROOT: &str = crate::identification::BSN_OID;
pub const URA_ROOT: &str = "2.16.528.1.1007.3.3";
pub const UZI_ROOT: &str = "2.16.528.1.1007.3.1";
pub const CARE_PROVIDER_TYPE_SYSTEM: &str = "2.16.840.1.113883.2.4.15.1060";
pub const DATA_CATEGORY_SYSTEM: &str = "2.16.840.1.113883.2.4.3.111.5.10.1";
pub const ROLE_SYSTEM: &str = "2.16.840.1.113883.2.4.15.111";
pub const PURPOSE_SYSTEM: &str = "2.16.840.1.113883.1.11.20448";
pub const REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
const SOAP_REQUEST: &str =
"application/soap+xml; charset=UTF-8; action=\"XACMLAuthorizationDecisionQueryRequest\"";
#[derive(Clone)]
pub struct MitzClient {
endpoint: Url,
http: reqwest::Client,
}
impl MitzClient {
pub fn new(endpoint: Url, http: reqwest::ClientBuilder) -> Result<Self, ClientError> {
if endpoint.scheme() != "https" {
return Err(ClientError::Endpoint(InvalidInput::Endpoint));
}
Self::any_scheme(endpoint, http)
}
pub fn unencrypted_for_development(
endpoint: Url,
http: reqwest::ClientBuilder,
) -> Result<Self, ClientError> {
Self::any_scheme(endpoint, http)
}
fn any_scheme(endpoint: Url, http: reqwest::ClientBuilder) -> Result<Self, ClientError> {
let bare = endpoint.username().is_empty()
&& endpoint.password().is_none()
&& endpoint.query().is_none()
&& endpoint.fragment().is_none();
if !matches!(endpoint.scheme(), "http" | "https") || endpoint.cannot_be_a_base() || !bare {
return Err(ClientError::Endpoint(InvalidInput::Endpoint));
}
let http = http
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(ClientError::Build)?;
Ok(Self { endpoint, http })
}
#[must_use]
pub fn endpoint(&self) -> &Url {
&self.endpoint
}
pub async fn ask(
&self,
question: &ClosedQuestion,
timeout: Duration,
) -> Result<ClosedAnswer, MitzError> {
let message = Uuid::new_v4();
let body =
request::envelope(question, &self.endpoint, message).map_err(MitzError::Encode)?;
let response = self
.http
.post(self.endpoint.clone())
.header(CONTENT_TYPE, SOAP_REQUEST)
.header(ACCEPT, "application/soap+xml")
.header(REQUEST_ID, message.to_string())
.timeout(timeout)
.body(body)
.send()
.await
.map_err(error::transport)?;
let status = response.status();
let media = response
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
let answer = body_of(response).await?;
response::read(status, media.as_deref(), &answer, question)
}
}
impl fmt::Debug for MitzClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MitzClient")
.field("endpoint", &self.endpoint.as_str())
.finish_non_exhaustive()
}
}
async fn body_of(mut response: reqwest::Response) -> Result<Vec<u8>, MitzError> {
let status = response.status();
let mut body = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(error::transport)? {
if body.len().saturating_add(chunk.len()) > response::LIMIT {
if status != http::StatusCode::OK {
return Err(MitzError::Rejected { status });
}
return Err(Malformation::TooLarge {
limit: response::LIMIT,
}
.into());
}
body.extend_from_slice(&chunk);
}
Ok(body)
}