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;
use base64::Engine;
use serde::Deserialize;
use serde_json::json;
const RPC_WINDOW_BYTES: u64 = 3 * 1024 * 1024;
const MAX_RESOURCE_BYTES: u64 = 64 * 1024 * 1024;
const MAX_WINDOWS: usize = (MAX_RESOURCE_BYTES / RPC_WINDOW_BYTES) as usize + 16;
#[derive(Deserialize)]
struct GetContent {
#[serde(default)]
total_length: u64,
#[serde(default)]
offset: u64,
#[serde(default)]
next_offset: Option<u64>,
#[serde(default)]
complete: Option<bool>,
#[serde(default)]
ciphertext: Option<String>,
#[serde(default)]
inclusion_proof: Option<String>,
#[serde(default)]
chunk_lens: Option<Vec<u32>>,
}
async fn rpc_call<T, R>(
transport: &T,
base: &str,
method: &str,
params: serde_json::Value,
) -> Result<R>
where
T: HttpTransport + ?Sized,
R: for<'de> Deserialize<'de>,
{
let body = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }).to_string();
let resp = transport
.post_json(base, body)
.await
.map_err(|e| ResolveError::Transport(e.0))?;
if !resp.is_success() {
return Err(ResolveError::Transport(format!(
"rpc {method} returned HTTP {}",
resp.status
)));
}
let envelope: serde_json::Value = serde_json::from_slice(&resp.body)
.map_err(|e| ResolveError::Rpc(format!("rpc {method}: malformed JSON ({e})")))?;
if let Some(err) = envelope.get("error").filter(|e| !e.is_null()) {
let msg = err
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("error");
return Err(ResolveError::Rpc(format!("rpc {method}: {msg}")));
}
let result = envelope
.get("result")
.cloned()
.ok_or_else(|| ResolveError::Rpc(format!("rpc {method}: no result")))?;
serde_json::from_value(result)
.map_err(|e| ResolveError::Rpc(format!("rpc {method}: unexpected result shape ({e})")))
}
fn trusted_root(parsed: &ParsedUrn) -> Result<String> {
parsed.root_hex().ok_or(ResolveError::RootRequired)
}
pub(crate) async fn fetch<T: HttpTransport + ?Sized>(
transport: &T,
base: &str,
parsed: &ParsedUrn,
) -> Result<Fetched> {
let base = base.trim_end_matches('/');
let root = trusted_root(parsed)?;
let retrieval_key = parsed.retrieval_key_hex();
let mut buf: Vec<u8> = Vec::new();
let mut total: Option<u64> = None;
let mut proof = String::new();
let mut chunk_lens: Vec<u32> = Vec::new();
let mut offset: u64 = 0;
let mut windows: usize = 0;
loop {
windows += 1;
if windows > MAX_WINDOWS {
return Err(ResolveError::Rpc("too many content windows".into()));
}
let r: GetContent = rpc_call(
transport,
base,
"dig.getContent",
json!({
"store_id": parsed.store_id_hex(),
"root": root,
"retrieval_key": retrieval_key,
"offset": offset,
"length": RPC_WINDOW_BYTES,
}),
)
.await?;
if total.is_none() {
if r.total_length == 0 {
return Err(ResolveError::NotFound);
}
if r.total_length > MAX_RESOURCE_BYTES {
return Err(ResolveError::Rpc("resource exceeds maximum size".into()));
}
total = Some(r.total_length);
buf.reserve(r.total_length as usize); }
let total_len = total.unwrap();
if chunk_lens.is_empty() {
if let Some(lens) = &r.chunk_lens {
chunk_lens = lens.clone();
}
}
if let Some(ct_b64) = &r.ciphertext {
let chunk = base64::engine::general_purpose::STANDARD
.decode(ct_b64.trim().as_bytes())
.map_err(|_| ResolveError::Rpc("ciphertext is not valid base64".into()))?;
if buf.len() as u64 + chunk.len() as u64 > total_len {
return Err(ResolveError::Rpc(
"gateway returned more bytes than declared".into(),
));
}
buf.extend_from_slice(&chunk);
}
if let Some(p) = &r.inclusion_proof {
if !p.is_empty() {
proof = p.clone();
}
}
let _ = r.offset; match (r.complete, r.next_offset) {
(Some(true), _) | (_, None) => break,
(_, Some(next)) => {
if next <= offset {
return Err(ResolveError::Rpc("content offset did not advance".into()));
}
offset = next;
}
}
}
let bytes = crypto::verify_and_decrypt(parsed, &buf, &proof, &root, &chunk_lens)?;
let content_type = content_type::derive(parsed.resource_key(), &bytes);
Ok(Fetched {
data: ResolvedData::new(bytes, content_type),
root: Some(root),
artifacts: Some(DiskArtifacts {
ciphertext: buf,
proof_b64: proof,
chunk_lens,
}),
})
}