supermachine 0.7.70

Run any OCI/Docker image as a hardware-isolated microVM on macOS HVF (Linux KVM and Windows WHP in progress). Single library API, zero flags for the common case, sub-100 ms cold-restore from snapshot.
Documentation
//! Self-contained HTTP(S) GET for the builder's `ADD <url>` — a thin redirect
//! loop over the shared [`crate::net`] rustls client (bundled webpki roots, no
//! subprocess curl, no OS trust-store dependency). The SSRF guard, TLS, header
//! read, and `Content-Length`/`chunked` body decode all live in `crate::net`
//! so `ADD <url>` and the registry pull share exactly one HTTP path.

use crate::api::Error;

const MAX_REDIRECTS: usize = 5;

/// Fetch the bytes at `url` (http/https), following up to [`MAX_REDIRECTS`].
pub fn http_get(url: &str) -> Result<Vec<u8>, Error> {
    let mut current = url.to_owned();
    for _ in 0..=MAX_REDIRECTS {
        // No `output` — `ADD <url>` keeps the body in memory (capped at 2 GiB
        // inside `net`). Every hop is SSRF-guarded + IP-pinned in there.
        let resp = crate::net::http_get_once(&current, &[("Accept", "*/*")], None)
            .map_err(|e| Error::vm_msg(format!("ADD url: {e}")))?;
        match resp.status {
            200..=299 => return Ok(resp.body),
            301 | 302 | 303 | 307 | 308 => {
                let loc = crate::net::header_value(&resp.headers, "location").ok_or_else(|| {
                    Error::vm_msg(format!(
                        "ADD url: HTTP {} redirect without Location",
                        resp.status
                    ))
                })?;
                current = crate::net::resolve_redirect(&current, loc);
            }
            other => {
                return Err(Error::vm_msg(format!(
                    "ADD url: GET {current} -> HTTP {other}"
                )))
            }
        }
    }
    Err(Error::vm_msg(format!(
        "ADD url: too many redirects (>{MAX_REDIRECTS}) for {url}"
    )))
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Network-gated end-to-end smoke over real HTTPS — exercises the rustls
    /// handshake + webpki-roots cert verification + redirect/body decode in
    /// `crate::net` end to end. Skips cleanly when there's no network.
    #[test]
    fn https_get_smoke() {
        match http_get("https://example.com/") {
            Ok(body) => {
                let text = String::from_utf8_lossy(&body);
                assert!(
                    text.contains("Example Domain"),
                    "unexpected body from https://example.com/:\n{}",
                    &text[..text.len().min(256)]
                );
            }
            Err(e) => eprintln!("[skip] no network for HTTPS smoke: {e:?}"),
        }
    }

    /// A server that redirects to itself forever must hit the redirect cap and
    /// error, not loop unbounded.
    #[test]
    fn redirect_loop_capped() {
        use std::io::{Read as _, Write as _};
        std::env::set_var("SUPERMACHINE_BUILD_ALLOW_PRIVATE_FETCH", "1");
        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
        let port = listener.local_addr().unwrap().port();
        // http_get opens a fresh connection per hop (Connection: close); answer
        // each with a self-redirect. Bounded accepts so the thread can't wedge.
        std::thread::spawn(move || {
            for _ in 0..(MAX_REDIRECTS + 4) {
                match listener.accept() {
                    Ok((mut s, _)) => {
                        let mut buf = [0u8; 1024];
                        let _ = s.read(&mut buf);
                        let _ = s.write_all(
                            b"HTTP/1.1 302 Found\r\nLocation: /again\r\n\
                              Content-Length: 0\r\nConnection: close\r\n\r\n",
                        );
                    }
                    Err(_) => break,
                }
            }
        });
        let url = format!("http://127.0.0.1:{port}/start");
        let err = http_get(&url).unwrap_err();
        let msg = format!("{err:?}");
        assert!(msg.contains("too many redirects"), "got: {msg}");
    }
}