use std::sync::Arc;
use std::time::Duration;
use reqwest::{header, Client, ClientBuilder, Url};
use serde::de::DeserializeOwned;
use serde_json::Value;
use thiserror::Error;
use trust_tasks_rs::{
erase_verifier, DynProofVerifier, ErrorResponse, Payload, ProofVerifier, TransportHandler,
TrustTask, TypeUri,
};
use crate::handler::HttpsHandler;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Default)]
pub struct HttpsClientBuilder {
server_url: Option<String>,
server_vid: Option<String>,
my_vid: Option<String>,
my_token: Option<String>,
strip_redundant_in_band: bool,
timeout: Option<Duration>,
connect_timeout: Option<Duration>,
response_verifier: Option<Arc<dyn DynProofVerifier>>,
}
impl HttpsClientBuilder {
pub fn server_url(mut self, url: impl Into<String>) -> Self {
self.server_url = Some(url.into());
self
}
pub fn server_vid(mut self, vid: impl Into<String>) -> Self {
self.server_vid = Some(vid.into());
self
}
pub fn my_vid(mut self, vid: impl Into<String>) -> Self {
self.my_vid = Some(vid.into());
self
}
pub fn my_token(mut self, token: impl Into<String>) -> Self {
self.my_token = Some(token.into());
self
}
pub fn strip_redundant_in_band(mut self, strip: bool) -> Self {
self.strip_redundant_in_band = strip;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = Some(timeout);
self
}
pub fn with_response_verifier<V>(mut self, verifier: V) -> Self
where
V: ProofVerifier + Send + Sync + 'static,
{
self.response_verifier = Some(erase_verifier(verifier));
self
}
pub fn build(self) -> Result<HttpsClient, ClientError> {
let server_url = self
.server_url
.ok_or_else(|| ClientError::Config("server_url is required".into()))?;
let base: Url = format!("{}/trust-tasks", server_url.trim_end_matches('/'))
.parse()
.map_err(|e| ClientError::Config(format!("server_url is not a valid URL: {e}")))?;
let http = ClientBuilder::new()
.timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT))
.connect_timeout(self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT))
.build()
.map_err(|e| ClientError::Config(e.to_string()))?;
Ok(HttpsClient {
http,
endpoint: base,
server_vid: self.server_vid,
my_vid: self.my_vid,
my_token: self.my_token,
strip_redundant_in_band: self.strip_redundant_in_band,
response_verifier: self.response_verifier,
})
}
}
pub struct HttpsClient {
http: Client,
endpoint: Url,
server_vid: Option<String>,
my_vid: Option<String>,
my_token: Option<String>,
strip_redundant_in_band: bool,
response_verifier: Option<Arc<dyn DynProofVerifier>>,
}
impl HttpsClient {
pub fn builder() -> HttpsClientBuilder {
HttpsClientBuilder::default()
}
pub async fn send<Req, Resp>(
&self,
mut request: TrustTask<Req>,
) -> Result<TrustTask<Resp>, ClientError>
where
Req: Payload + serde::Serialize,
Resp: Payload + DeserializeOwned,
{
if request.issuer.is_none() {
request.issuer = self.my_vid.clone();
}
if request.recipient.is_none() {
request.recipient = self.server_vid.clone();
}
if request.issued_at.is_none() {
request.issued_at = Some(chrono::Utc::now());
}
if self.strip_redundant_in_band {
HttpsHandler::new(self.my_vid.clone(), self.server_vid.clone())
.prepare_outbound(&mut request);
}
let mut req = self
.http
.post(self.endpoint.clone())
.header(header::CONTENT_TYPE, "application/json")
.json(&request);
if let Some(token) = &self.my_token {
req = req.bearer_auth(token);
}
let resp = req.send().await?;
let status = resp.status();
let body = resp.bytes().await?;
if status.is_success() {
if body.is_empty() {
return Err(ClientError::DuplicateAbsorbed {
http_status: status.as_u16(),
});
}
let untyped: TrustTask<Value> = serde_json::from_slice(&body)
.map_err(|e| ClientError::ResponseDecode(e.to_string()))?;
self.check_response_binding(&request, &untyped)?;
self.verify_response_proof(&untyped).await?;
let typed: TrustTask<Resp> = serde_json::from_slice(&body)
.map_err(|e| ClientError::ResponseDecode(e.to_string()))?;
Ok(typed)
} else {
match serde_json::from_slice::<ErrorResponse>(&body) {
Ok(error_doc) => {
if let Some(reported) = error_doc
.payload
.in_response_to
.as_ref()
.and_then(|r| r.id.as_deref())
{
if reported != request.id {
return Err(ClientError::ErrorResponseMismatch {
expected: request.id.clone(),
actual: reported.to_string(),
});
}
}
Err(ClientError::TrustTaskError {
http_status: status.as_u16(),
error: Box::new(error_doc),
})
}
Err(_) => Err(ClientError::HttpStatus {
http_status: status.as_u16(),
body: String::from_utf8_lossy(&body).to_string(),
}),
}
}
}
fn check_response_binding<Req: Payload + serde::Serialize>(
&self,
request: &TrustTask<Req>,
response: &TrustTask<Value>,
) -> Result<(), ClientError> {
let expected_thread = request
.thread_id
.clone()
.unwrap_or_else(|| request.id.clone());
if response.thread_id.as_deref() != Some(expected_thread.as_str()) {
return Err(ClientError::ResponseThreadMismatch {
expected: expected_thread,
actual: response.thread_id.clone(),
});
}
let expected_type: TypeUri = request.type_uri.with_response();
if response.type_uri != expected_type {
return Err(ClientError::ResponseTypeMismatch {
expected: expected_type.to_string(),
actual: response.type_uri.to_string(),
});
}
if let Some(expected) = self.server_vid.as_deref() {
if !self.party_matches(response.issuer.as_deref(), expected) {
return Err(ClientError::ResponseIssuerMismatch {
expected: expected.to_string(),
actual: response.issuer.clone(),
});
}
}
if let Some(expected) = self.my_vid.as_deref() {
if !self.party_matches(response.recipient.as_deref(), expected) {
return Err(ClientError::ResponseRecipientMismatch {
expected: expected.to_string(),
actual: response.recipient.clone(),
});
}
}
Ok(())
}
fn party_matches(&self, actual: Option<&str>, expected: &str) -> bool {
match actual {
Some(v) => v == expected,
None => self.strip_redundant_in_band,
}
}
async fn verify_response_proof(&self, response: &TrustTask<Value>) -> Result<(), ClientError> {
let Some(verifier) = &self.response_verifier else {
return Ok(());
};
if response.proof.is_none() {
return Err(ClientError::ResponseProofMissing);
}
verifier
.verify_json(response)
.await
.map_err(|e| ClientError::ResponseProofInvalid(e.to_string()))
}
}
#[derive(Debug, Error)]
pub enum ClientError {
#[error("client configuration error: {0}")]
Config(String),
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("server returned trust-task-error/0.1 (HTTP {http_status}): {error}")]
TrustTaskError {
http_status: u16,
error: Box<ErrorResponse>,
},
#[error("non-2xx HTTP response ({http_status}) with non-Trust-Task body: {body}")]
HttpStatus {
http_status: u16,
body: String,
},
#[error("response body did not match expected type: {0}")]
ResponseDecode(String),
#[error("consumer absorbed this document as a duplicate (HTTP {http_status}); no result body")]
DuplicateAbsorbed {
http_status: u16,
},
#[error("response threadId does not match the request: expected {expected}, got {actual:?}")]
ResponseThreadMismatch {
expected: String,
actual: Option<String>,
},
#[error("response type does not match the request: expected {expected}, got {actual}")]
ResponseTypeMismatch {
expected: String,
actual: String,
},
#[error("response issuer is not the configured server: expected {expected}, got {actual:?}")]
ResponseIssuerMismatch {
expected: String,
actual: Option<String>,
},
#[error("response recipient is not this client: expected {expected}, got {actual:?}")]
ResponseRecipientMismatch {
expected: String,
actual: Option<String>,
},
#[error("error response reports on a different document: expected {expected}, got {actual}")]
ErrorResponseMismatch {
expected: String,
actual: String,
},
#[error("response carried no proof but this client requires signed responses")]
ResponseProofMissing,
#[error("response proof verification failed: {0}")]
ResponseProofInvalid(String),
}