///|
/// The largest body this client will accept from an update endpoint.
///
/// A manifest and a signature are both small. A server that offers something
/// enormous in their place is either broken or hostile, and either way the
/// client should stop rather than fill memory on the way to a signature check
/// it was going to fail.
pub let maximum_metadata_bytes : Int = 1024 * 1024
///|
/// Fetches update metadata over HTTPS.
///
/// Plain HTTP is refused. The signature is what makes a response trustworthy,
/// so TLS is not what protects this exchange — but there is no reason to
/// advertise which release a user is running to anyone on the path, and no
/// reason to accept a redirect into cleartext.
pub async fn http_fetch(url : String) -> Bytes raise UpdateError {
guard url.has_prefix("https://") else {
raise FetchFailed(url~, detail="only https URLs are fetched")
}
let (response, body) = @http.get(url) catch {
error => raise FetchFailed(url~, detail=@debug.render(Repr(error)))
}
guard response.code == 200 else {
raise FetchFailed(url~, detail="server responded \{response.code}")
}
let bytes = body.binary()
guard bytes.length() <= maximum_metadata_bytes else {
raise FetchFailed(
url~,
detail="body is \{bytes.length()} bytes, over the \{maximum_metadata_bytes} byte limit",
)
}
bytes
}
///|
/// Streams an artifact over HTTPS without retaining its body.
///
/// `expected_size` comes from the authenticated manifest. A `Content-Length`
/// mismatch can therefore be refused before reading the body, but the consumer
/// still counts actual chunks because the header may be absent after content
/// decoding or on a chunked response.
pub async fn http_fetch_artifact(
url : String,
expected_size : Int64,
consume : async (Bytes) -> Unit raise UpdateError,
) -> Unit raise UpdateError {
guard url.has_prefix("https://") else {
raise FetchFailed(url~, detail="only https URLs are fetched")
}
let (response, client) = @http.get_stream(url) catch {
error => raise FetchFailed(url~, detail=@debug.render(Repr(error)))
}
defer client.close()
guard response.code == 200 else {
raise FetchFailed(url~, detail="server responded \{response.code}")
}
match response.headers.get("content-length") {
Some(text) => {
let declared = @string.parse_int64(text[:]) catch {
error => raise FetchFailed(url~, detail=@debug.render(Repr(error)))
}
guard declared == expected_size else {
raise ArtifactSizeMismatch(expected=expected_size, actual=declared)
}
}
None => ()
}
let mut received = 0L
while (client.read_some() catch {
error => raise FetchFailed(url~, detail=@debug.render(Repr(error)))
})
is Some(chunk) {
let chunk_size = chunk.length().to_int64()
let actual_size = artifact_size_after_chunk(
expected_size, received, chunk_size,
)
consume(chunk)
received = actual_size
if received == expected_size {
return
}
}
guard received == expected_size else {
raise ArtifactSizeMismatch(expected=expected_size, actual=received)
}
}