use crate::net::bounded_fetch::{BoundedFetchConfig, BoundedFetchErrorKind, bounded_fetch};
use thiserror::Error;
pub const MAX_MANIFEST_BYTES: u64 = 5 * 1024 * 1024;
pub const MAX_TOOL_BYTES: u64 = 1024 * 1024;
pub const MAX_SIG_BYTES: u64 = 64 * 1024;
#[derive(Debug, Error)]
pub enum FetchError {
#[error("fetch failed for {url}: {message}")]
Network { url: String, message: String },
#[error("fetch returned HTTP {status} for {url}")]
HttpStatus { url: String, status: u16 },
#[error("response body too large for {url}: capped at {cap} bytes")]
TooLarge { url: String, cap: u64 },
#[error("read error for {url}: {source}")]
Read {
url: String,
#[source]
source: std::io::Error,
},
#[error("non-https url refused: {0}")]
NonHttps(String),
}
pub fn fetch_bounded(url: &str, max_bytes: u64) -> Result<Vec<u8>, FetchError> {
let cfg = BoundedFetchConfig {
insecure_loopback_env: "JARVY_REGISTRY_ALLOW_INSECURE_FETCH",
};
bounded_fetch(url, max_bytes, cfg).map_err(|kind| {
let redacted = crate::network::redact_credentials(url).into_owned();
match kind {
BoundedFetchErrorKind::NonHttps => FetchError::NonHttps(redacted),
BoundedFetchErrorKind::Network(message) => FetchError::Network {
url: redacted,
message,
},
BoundedFetchErrorKind::HttpStatus(status) => FetchError::HttpStatus {
url: redacted,
status,
},
BoundedFetchErrorKind::TooLarge => FetchError::TooLarge {
url: redacted,
cap: max_bytes,
},
BoundedFetchErrorKind::Read(source) => FetchError::Read {
url: redacted,
source,
},
}
})
}
#[cfg(test)]
#[allow(unsafe_code, clippy::undocumented_unsafe_blocks)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial(registry_env)]
fn refuses_http_url() {
unsafe {
std::env::remove_var("JARVY_REGISTRY_ALLOW_INSECURE_FETCH");
}
let err = fetch_bounded("http://example.com/x", 1024).unwrap_err();
assert!(matches!(err, FetchError::NonHttps(_)));
}
#[test]
#[serial(registry_env)]
fn refuses_ftp_url() {
unsafe {
std::env::remove_var("JARVY_REGISTRY_ALLOW_INSECURE_FETCH");
}
let err = fetch_bounded("ftp://example.com/x", 1024).unwrap_err();
assert!(matches!(err, FetchError::NonHttps(_)));
}
#[test]
#[serial(registry_env)]
fn refuses_non_loopback_even_with_env() {
unsafe {
std::env::set_var("JARVY_REGISTRY_ALLOW_INSECURE_FETCH", "1");
}
let err = fetch_bounded("http://attacker.example/x", 1024).unwrap_err();
assert!(matches!(err, FetchError::NonHttps(_)));
unsafe {
std::env::remove_var("JARVY_REGISTRY_ALLOW_INSECURE_FETCH");
}
}
#[test]
#[serial(registry_env)]
fn refuses_userinfo_authority_bypass() {
unsafe {
std::env::set_var("JARVY_REGISTRY_ALLOW_INSECURE_FETCH", "1");
}
for url in [
"http://127.0.0.1:80@attacker.example/x",
"http://localhost:80@attacker.example/x",
"http://127.0.0.1@attacker.example/x",
"http://user:pass@127.0.0.1:8080/x",
] {
let err = fetch_bounded(url, 1024).unwrap_err();
assert!(
matches!(err, FetchError::NonHttps(_)),
"must refuse userinfo-bearing URL {url:?}"
);
}
unsafe {
std::env::remove_var("JARVY_REGISTRY_ALLOW_INSECURE_FETCH");
}
}
}