use std::io::Read;
#[derive(Debug)]
pub enum BoundedFetchErrorKind {
Network(String),
HttpStatus(u16),
TooLarge,
Read(std::io::Error),
NonHttps,
}
#[derive(Debug, Clone, Copy)]
pub struct BoundedFetchConfig {
pub insecure_loopback_env: &'static str,
}
pub fn bounded_fetch(
url: &str,
max_bytes: u64,
cfg: BoundedFetchConfig,
) -> Result<Vec<u8>, BoundedFetchErrorKind> {
if !url.starts_with("https://") && !insecure_loopback_allowed(url, cfg.insecure_loopback_env) {
return Err(BoundedFetchErrorKind::NonHttps);
}
let agent = crate::net::agent::agent();
let response = agent
.get(url)
.header("User-Agent", crate::net::agent::USER_AGENT)
.call()
.map_err(|e| BoundedFetchErrorKind::Network(e.to_string()))?;
if response.status() != 200 {
return Err(BoundedFetchErrorKind::HttpStatus(
response.status().as_u16(),
));
}
let mut body = response.into_body();
let reader = body.as_reader();
let mut limited = reader.take(max_bytes + 1);
let mut buf = Vec::with_capacity(8 * 1024);
limited
.read_to_end(&mut buf)
.map_err(BoundedFetchErrorKind::Read)?;
if buf.len() as u64 > max_bytes {
return Err(BoundedFetchErrorKind::TooLarge);
}
Ok(buf)
}
fn insecure_loopback_allowed(url: &str, env_var: &'static str) -> bool {
#[cfg(not(feature = "test-bypass"))]
{
let _ = (url, env_var);
false
}
#[cfg(feature = "test-bypass")]
{
if std::env::var_os(env_var).is_none() {
return false;
}
is_plain_loopback_http(url)
}
}
#[cfg(any(feature = "test-bypass", test))]
pub(crate) fn is_plain_loopback_http(url: &str) -> bool {
let Some(after_scheme) = url.strip_prefix("http://") else {
return false;
};
let authority_end = after_scheme
.find(['/', '?', '#'])
.unwrap_or(after_scheme.len());
let authority = &after_scheme[..authority_end];
if authority.contains('@') {
return false;
}
let host_end = authority.find(':').unwrap_or(authority.len());
let host = &authority[..host_end];
matches!(host, "127.0.0.1" | "localhost")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parser_accepts_clean_loopback() {
assert!(is_plain_loopback_http("http://127.0.0.1:8080/x"));
assert!(is_plain_loopback_http("http://localhost:8080/x"));
assert!(is_plain_loopback_http("http://127.0.0.1/"));
}
#[test]
fn parser_refuses_userinfo_bypass() {
assert!(!is_plain_loopback_http(
"http://127.0.0.1:80@attacker.example/x"
));
assert!(!is_plain_loopback_http("http://user@127.0.0.1:8080/x"));
assert!(!is_plain_loopback_http(
"http://localhost@attacker.example/x"
));
}
}