use reqwest::{Client, StatusCode};
use url::Url;
use uuid::Uuid;
use crate::error::Error;
use crate::models::electricity::{
IdentificationParameter, MaloIdentResultNegative, MaloIdentResultPositive, ReferenceId,
};
use p256::ecdsa::SigningKey;
use crate::transport::content_security::SignedInputs;
#[derive(Clone, Debug)]
pub struct MaloIdentClient {
inner: Client,
base_url: Url,
signing: Option<Signing>,
}
#[derive(Clone, Debug)]
struct Signing {
key: SigningKey,
cert_header: String,
}
impl MaloIdentClient {
pub fn new(base_url: Url, client: Client) -> Self {
Self {
inner: client,
base_url,
signing: None,
}
}
#[must_use]
pub fn with_signing(mut self, key: SigningKey, cert_der: &[u8]) -> Self {
self.signing = Some(Signing {
key,
cert_header: crate::transport::content_security::cert_header_value(cert_der),
});
self
}
pub async fn request_malo_id(
&self,
transaction_id: Uuid,
creation_date_time: &str,
params: &IdentificationParameter,
initial_transaction_id: Option<Uuid>,
) -> Result<(), Error> {
let url = self.url("maloId/request/v1")?;
let canonical = self.canonical_body(params)?;
let mut req = self
.inner
.post(url.clone())
.header("transactionId", transaction_id.to_string())
.header("creationDateTime", creation_date_time)
.header("Content-Type", "application/json")
.body(canonical.clone());
let initial = initial_transaction_id.map(|id| id.to_string());
if let Some(id) = &initial {
req = req.header("initialTransactionId", id);
}
req = self.sign_if_enabled(
req,
&SignedInputs {
uri: url.as_str(),
payload: &canonical,
creation_date_time,
transaction_id: &transaction_id.to_string(),
initial_transaction_id: initial.as_deref(),
},
)?;
check_accepted(req.send().await?).await
}
pub async fn send_positive_response(
&self,
transaction_id: Uuid,
creation_date_time: &str,
reference_id: ReferenceId,
result: &MaloIdentResultPositive,
initial_transaction_id: Option<Uuid>,
) -> Result<(), Error> {
let mut url = self.url("maloId/dataForMarketLocationPositive/v1")?;
url.query_pairs_mut()
.append_pair("referenceId", &reference_id.to_string());
let canonical = self.canonical_body(result)?;
let mut req = self
.inner
.post(url.clone())
.header("transactionId", transaction_id.to_string())
.header("creationDateTime", creation_date_time)
.header("Content-Type", "application/json")
.body(canonical.clone());
let initial = initial_transaction_id.map(|id| id.to_string());
if let Some(id) = &initial {
req = req.header("initialTransactionId", id);
}
req = self.sign_if_enabled(
req,
&SignedInputs {
uri: url.as_str(),
payload: &canonical,
creation_date_time,
transaction_id: &transaction_id.to_string(),
initial_transaction_id: initial.as_deref(),
},
)?;
check_accepted(req.send().await?).await
}
pub async fn send_negative_response(
&self,
transaction_id: Uuid,
creation_date_time: &str,
reference_id: ReferenceId,
result: &MaloIdentResultNegative,
initial_transaction_id: Option<Uuid>,
) -> Result<(), Error> {
let mut url = self.url("maloId/dataForMarketLocationNegative/v1")?;
url.query_pairs_mut()
.append_pair("referenceId", &reference_id.to_string());
let canonical = self.canonical_body(result)?;
let mut req = self
.inner
.post(url.clone())
.header("transactionId", transaction_id.to_string())
.header("creationDateTime", creation_date_time)
.header("Content-Type", "application/json")
.body(canonical.clone());
let initial = initial_transaction_id.map(|id| id.to_string());
if let Some(id) = &initial {
req = req.header("initialTransactionId", id);
}
req = self.sign_if_enabled(
req,
&SignedInputs {
uri: url.as_str(),
payload: &canonical,
creation_date_time,
transaction_id: &transaction_id.to_string(),
initial_transaction_id: initial.as_deref(),
},
)?;
check_accepted(req.send().await?).await
}
fn url(&self, path: &str) -> Result<Url, Error> {
self.base_url.join(path).map_err(Error::Url)
}
fn canonical_body<T: serde::Serialize>(&self, value: &T) -> Result<Vec<u8>, Error> {
if self.signing.is_some() {
return crate::transport::content_security::canonical_json(value);
}
serde_json::to_vec(value).map_err(Error::Json)
}
#[inline]
fn sign_if_enabled(
&self,
req: reqwest::RequestBuilder,
inputs: &SignedInputs<'_>,
) -> Result<reqwest::RequestBuilder, Error> {
if let Some(signing) = &self.signing {
use crate::transport::content_security::{
self, HEADER_CERT, HEADER_DIGEST, HEADER_SIGNATURE,
};
let (digest, sig) = content_security::sign_request(inputs, &signing.key)?;
return Ok(req
.header(HEADER_DIGEST, digest)
.header(HEADER_SIGNATURE, sig)
.header(HEADER_CERT, &signing.cert_header));
}
Ok(req)
}
}
async fn check_accepted(resp: reqwest::Response) -> Result<(), Error> {
match resp.status() {
StatusCode::ACCEPTED => Ok(()),
s => Err(Error::Http {
status: s.as_u16(),
body: resp.text().await.unwrap_or_default(),
}),
}
}