use crate::cache::DiskArtifacts;
use crate::content_type;
use crate::crypto;
use crate::error::{ResolveError, Result};
use crate::resolver::{Fetched, ResolvedData};
use crate::transport::HttpTransport;
use crate::urn::ParsedUrn;
fn serve_url(base: &str, parsed: &ParsedUrn) -> String {
let store = match parsed.root_hex() {
Some(root) => format!("{}:{}", parsed.store_id_hex(), root),
None => parsed.store_id_hex(),
};
format!("{base}/s/{store}/{}", parsed.resource_key())
}
pub(crate) async fn fetch<T: HttpTransport + ?Sized>(
transport: &T,
base: &str,
parsed: &ParsedUrn,
) -> Result<Fetched> {
let url = serve_url(base, parsed);
let resp = transport
.get(&url)
.await
.map_err(|e| ResolveError::Transport(e.0))?;
if resp.status == 404 {
return Err(ResolveError::NotFound);
}
if !resp.is_success() {
return Err(ResolveError::Transport(format!(
"node returned HTTP {}",
resp.status
)));
}
let node_root = parsed.root_hex().or_else(|| {
resp.header("x-dig-root")
.map(str::trim)
.filter(|r| !r.is_empty())
.map(str::to_string)
});
let verified_plaintext = resp
.header("x-dig-verified")
.map(|v| v.trim().eq_ignore_ascii_case("true"))
.unwrap_or(false);
if verified_plaintext {
let content_type = header_content_type(&resp)
.unwrap_or_else(|| content_type::derive(parsed.resource_key(), &resp.body));
return Ok(Fetched {
data: ResolvedData::new(resp.body, content_type),
root: node_root,
artifacts: None, });
}
let is_ciphertext = resp
.header("x-dig-encrypted")
.map(|v| v.trim().eq_ignore_ascii_case("true"))
.unwrap_or(false)
|| resp.header("x-dig-inclusion-proof").is_some();
if is_ciphertext {
let proof_b64 = resp
.header("x-dig-inclusion-proof")
.map(str::to_string)
.ok_or_else(|| {
ResolveError::VerifyFailed(
"ciphertext node response missing X-Dig-Inclusion-Proof".into(),
)
})?;
let root = node_root.ok_or_else(|| {
ResolveError::VerifyFailed("ciphertext node response missing a root".into())
})?;
let chunk_lens = parse_chunk_lens(resp.header("x-dig-chunk-lens"))?;
let bytes = crypto::verify_and_decrypt(parsed, &resp.body, &proof_b64, &root, &chunk_lens)?;
let content_type = content_type::derive(parsed.resource_key(), &bytes);
return Ok(Fetched {
data: ResolvedData::new(bytes, content_type),
root: Some(root),
artifacts: Some(DiskArtifacts {
ciphertext: resp.body,
proof_b64,
chunk_lens,
}),
});
}
Err(ResolveError::VerifyFailed(
"node response was neither X-Dig-Verified plaintext nor decryptable ciphertext".into(),
))
}
fn header_content_type(resp: &crate::transport::HttpResponse) -> Option<String> {
resp.header("content-type")
.map(|c| c.split(';').next().unwrap_or(c).trim().to_string())
.filter(|c| !c.is_empty())
}
fn parse_chunk_lens(header: Option<&str>) -> Result<Vec<u32>> {
let Some(raw) = header.map(str::trim).filter(|s| !s.is_empty()) else {
return Ok(Vec::new());
};
raw.split(',')
.map(|n| n.trim().parse::<u32>())
.collect::<core::result::Result<Vec<u32>, _>>()
.map_err(|_| ResolveError::VerifyFailed("invalid X-Dig-Chunk-Lens header".into()))
}