use async_trait::async_trait;
use crate::core::Digest;
use super::Checkpoint;
use super::note::b64;
use super::note::{NoteSignature, SignedNote};
use super::witness::{Cosignature, Witness, WitnessError};
#[derive(Debug, Clone)]
pub struct HttpWitness {
http: reqwest::Client,
prefix: String,
log_signature: NoteSignature,
}
impl HttpWitness {
pub fn new(
prefix: impl Into<String>,
log_signature: NoteSignature,
) -> Result<Self, WitnessError> {
let http = reqwest::Client::builder().build().map_err(|e| {
WitnessError::Unavailable(format!("could not build an HTTP client: {e}"))
})?;
Ok(Self {
http,
prefix: prefix.into().trim_end_matches('/').to_owned(),
log_signature,
})
}
fn body(&self, checkpoint: &Checkpoint, old_size: u64, proof: &[Digest]) -> String {
let mut out = format!("old {old_size}\n");
for hash in proof {
out.push_str(&b64(hash.as_bytes()));
out.push('\n');
}
out.push('\n');
let note = SignedNote::new(checkpoint.to_note())
.unwrap_or_else(|_| unreachable!("a checkpoint note is always a valid note body"))
.with_signature(self.log_signature.clone());
out.push_str(¬e.to_wire());
out
}
}
#[async_trait]
impl Witness for HttpWitness {
async fn cosign(
&self,
checkpoint: &Checkpoint,
old_size: u64,
proof: &[Digest],
) -> Result<Cosignature, WitnessError> {
let url = format!("{}/add-checkpoint", self.prefix);
let response = self
.http
.post(&url)
.body(self.body(checkpoint, old_size, proof))
.send()
.await
.map_err(|e| WitnessError::Unavailable(format!("{url}: {e}")))?;
let status = response.status().as_u16();
let text = response
.text()
.await
.map_err(|e| WitnessError::Unavailable(format!("{url}: reading the reply: {e}")))?;
match status {
200 => parse_cosignature(&text, &checkpoint.origin),
409 => Err(WitnessError::Stale {
origin: checkpoint.origin.clone(),
witness_size: text.trim().parse().unwrap_or_default(),
}),
422 => Err(WitnessError::Forked {
origin: checkpoint.origin.clone(),
seen: old_size,
offered: checkpoint.size,
}),
403 => Err(WitnessError::Unavailable(format!(
"{url}: the witness does not trust the key that signed this checkpoint — it \
cosigns for logs it recognises, so the log's key must be registered with the \
operator first"
))),
404 => Err(WitnessError::Unavailable(format!(
"{url}: the witness does not know the origin '{}'",
checkpoint.origin
))),
other => Err(WitnessError::Unavailable(format!(
"{url}: unexpected status {other}: {}",
text.trim()
))),
}
}
}
fn parse_cosignature(body: &str, origin: &str) -> Result<Cosignature, WitnessError> {
let framed = format!("witness\n\n{body}");
let note = SignedNote::parse(&framed).map_err(|e| {
WitnessError::Unavailable(format!("log '{origin}': unreadable cosignature: {e}"))
})?;
let first = note.signatures.into_iter().next().ok_or_else(|| {
WitnessError::Unavailable(format!(
"log '{origin}': the witness answered 200 with no signature, which is not a \
cosignature however encouraging the status code is"
))
})?;
Ok(Cosignature {
key_id: first.name,
signature: first.signature,
})
}