adnv 1.5.0

CLI that tracks, checks and auto-updates infrastructure dependency versions across Terraform, Helm, Docker, Git and npm
Documentation
//! Shared constructors for the outgoing HTTP clients.
//!
//! Every client is built here so that the transport policy is decided in one
//! place instead of being re-chosen, or forgotten, at each call site.

use crate::{Error, Result};

/// Ceiling on a registry response read into memory. Registry indexes are large
/// — the Helm one runs to a few megabytes — but not unbounded, and `text()` or
/// `json()` would otherwise buffer whatever the server decides to send.
const MAX_BODY_BYTES: usize = 64 * 1024 * 1024;

/// The default client: HTTPS only, and few enough redirects that a host which
/// is compromised or doubles as an open redirector cannot bounce a request far.
///
/// `https_only` matters beyond eavesdropping: these requests decide which
/// versions exist, and therefore what `adnv update` writes into the files of
/// the repository.
pub fn client() -> Result<reqwest::Client> {
    Ok(reqwest::Client::builder()
        .https_only(true)
        .redirect(reqwest::redirect::Policy::limited(3))
        .build()?)
}

/// A GitHub client, authenticated from `ADNV_GITHUB_TOKEN` when it is set.
/// Without it GitHub allows 60 requests an hour and hides private repositories.
pub fn github() -> Result<octocrab::Octocrab> {
    let builder = octocrab::Octocrab::builder();
    let builder = match std::env::var("ADNV_GITHUB_TOKEN") {
        Ok(token) => builder.personal_token(token),
        Err(_) => builder,
    };

    builder
        .build()
        .map_err(|err| crate::Error::github(err, "Could not build GitHub client"))
}

/// Reads a response body, refusing to buffer more than [`MAX_BODY_BYTES`].
///
/// `Response::text` and `Response::json` have no ceiling, so a multi-gigabyte
/// index — or a server that simply never stops writing — takes the process down
/// with an allocation failure.
pub async fn body(response: reqwest::Response) -> Result<String> {
    use futures::TryStreamExt as _;

    let url = response.url().clone();
    let mut stream = response.bytes_stream();
    let mut body = Vec::new();

    while let Some(chunk) = stream.try_next().await? {
        if body.len() + chunk.len() > MAX_BODY_BYTES {
            return Err(Error::io(
                std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    format!("response from {url} exceeds {MAX_BODY_BYTES} bytes"),
                ),
                "Refusing to read an oversized response",
            ));
        }

        body.extend_from_slice(&chunk);
    }

    String::from_utf8(body).map_err(|err| {
        Error::io(
            std::io::Error::new(std::io::ErrorKind::InvalidData, err),
            format!("Response from {url} is not valid UTF-8"),
        )
    })
}