pub mod audit;
pub mod discovery;
pub mod error;
pub mod identifier;
pub mod request;
mod response;
pub mod security;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use http::header::{ACCEPT, CONTENT_TYPE};
use audit::{AuditEvent, AuditRecorder, EventOutcome, NetworkAccessPoint};
use discovery::Discovery;
use error::{Malformation, XcpdError};
use ihe_atna::balp::recording::{Late, Recorded, within};
use ihe_atna::user::OnBehalfOf;
use request::{DiscoveryQuery, Ids, RespondingGateway};
use security::XuaAssertion;
const SOAP: &str = "http://www.w3.org/2003/05/soap-envelope";
const WSA: &str = "http://www.w3.org/2005/08/addressing";
const WSSE: &str =
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
const HL7: &str = "urn:hl7-org:v3";
const ACTION: &str = "urn:hl7-org:v3:PRPA_IN201305UV02:CrossGatewayPatientDiscovery";
const REQUEST_SYSTEM: &str = "1.3.6.1.4.1.19376.1.2.27.1";
const ISSUE_SYSTEM: &str = "1.3.6.1.4.1.19376.1.2.27.3";
const SOAP_REQUEST: &str = "application/soap+xml; charset=UTF-8; action=\"urn:hl7-org:v3:PRPA_IN201305UV02:CrossGatewayPatientDiscovery\"";
#[derive(Clone)]
pub struct XcpdClient {
http: reqwest::Client,
audit: Option<Arc<dyn AuditRecorder>>,
}
impl XcpdClient {
#[must_use]
pub fn new(http: reqwest::Client) -> Self {
Self { http, audit: None }
}
#[must_use]
pub fn audited(mut self, recorder: Arc<dyn AuditRecorder>) -> Self {
self.audit = Some(recorder);
self
}
pub async fn discover(
&self,
gateway: &RespondingGateway,
query: &DiscoveryQuery,
assertion: Option<&XuaAssertion>,
on_behalf: &OnBehalfOf,
timeout: Duration,
) -> Result<Discovery, XcpdError> {
let deadline = ihe_atna::balp::recording::deadline(timeout);
let ids = Ids::fresh();
let written = request::envelope(query, gateway, &ids, assertion)?;
let answer = self.exchange(gateway, written.body, &ids, timeout).await;
if let Some(recorder) = &self.audit {
let event = AuditEvent {
date_time: jiff::Timestamp::now(),
outcome: match &answer {
Ok(_) => EventOutcome::Success,
Err(error) if error.status().is_some() => EventOutcome::MinorFailure,
Err(_) => EventOutcome::SeriousFailure,
},
process_id: std::process::id(),
destination: audit::destination(gateway.endpoint()),
destination_access_point: NetworkAccessPoint::of(gateway.endpoint()),
query: written.query,
home_community: query.community().cloned(),
on_behalf: on_behalf.clone(),
};
match within(deadline, recorder.record(event)).await {
Recorded::Refused(error) => return Err(XcpdError::Audit(error)),
Recorded::Late if answer.is_ok() => {
return Err(XcpdError::Audit(audit::AuditError(Box::new(Late::new()))));
}
Recorded::Accepted | Recorded::Late => {}
}
}
answer
}
async fn exchange(
&self,
gateway: &RespondingGateway,
body: Vec<u8>,
ids: &Ids,
timeout: Duration,
) -> Result<Discovery, XcpdError> {
let response = self
.http
.post(gateway.endpoint().clone())
.header(CONTENT_TYPE, SOAP_REQUEST)
.header(ACCEPT, "application/soap+xml")
.timeout(timeout)
.body(body)
.send()
.await
.map_err(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, &ids.message_urn())
}
}
impl fmt::Debug for XcpdClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("XcpdClient").finish_non_exhaustive()
}
}
async fn body_of(mut response: reqwest::Response) -> Result<Vec<u8>, XcpdError> {
let status = response.status();
let mut body = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(transport)? {
if body.len().saturating_add(chunk.len()) > response::LIMIT {
if status != http::StatusCode::OK {
return Err(XcpdError::Rejected { status });
}
return Err(Malformation::TooLarge {
limit: response::LIMIT,
}
.into());
}
body.extend_from_slice(&chunk);
}
Ok(body)
}
fn transport(error: reqwest::Error) -> XcpdError {
let error = error.without_url();
if error.is_timeout() {
XcpdError::Timeout
} else {
XcpdError::Transport(error)
}
}