#
cargo/util/network/
http.rs

    1//! Configures libcurl's http handles.
    2
    3use std::str;
    4use std::time::Duration;
    5
    6use anyhow::bail;
    7use curl::easy::Easy;
    8use curl::easy::InfoType;
    9use curl::easy::SslOpt;
    10use curl::easy::SslVersion;
    11use tracing::debug;
    12use tracing::trace;
    13
    14use crate::CargoResult;
    15use crate::GlobalContext;
    16use crate::util::context::SslVersionConfig;
    17use crate::util::context::SslVersionConfigRange;
    18use crate::version;
    19
    20/// Creates a new HTTP handle with appropriate global configuration for cargo.
    21pub fn http_handle(gctx: &GlobalContext) -> CargoResult<Easy> {
    22    let (mut handle, timeout) = http_handle_and_timeout(gctx)?;
    23    timeout.configure(&mut handle)?;
    24    Ok(handle)
    25}
    26
    27pub fn http_handle_and_timeout(gctx: &GlobalContext) -> CargoResult<(Easy, HttpTimeout)> {
    28    if let Some(offline_flag) = gctx.offline_flag() {
    29        bail!(
    30            "attempting to make an HTTP request, but {offline_flag} was \
    31             specified"
    32        )
    33    }
    34
    35    // The timeout option for libcurl by default times out the entire transfer,
    36    // but we probably don't want this. Instead we only set timeouts for the
    37    // connect phase as well as a "low speed" timeout so if we don't receive
    38    // many bytes in a large-ish period of time then we time out.
    39    let mut handle = Easy::new();
    40    let timeout = configure_http_handle(gctx, &mut handle)?;
    41    Ok((handle, timeout))
    42}
    43
    44// Only use a custom transport if any HTTP options are specified,
    45// such as proxies or custom certificate authorities.
    46//
    47// The custom transport, however, is not as well battle-tested.
    48pub fn needs_custom_http_transport(gctx: &GlobalContext) -> CargoResult<bool> {
    49    Ok(super::proxy::http_proxy_exists(gctx.http_config()?, gctx)
    50        || *gctx.http_config()? != Default::default()
    51        || gctx.get_env_os("HTTP_TIMEOUT").is_some())
    52}
    53
    54/// Configure a libcurl http handle with the defaults options for Cargo
    55pub fn configure_http_handle(gctx: &GlobalContext, handle: &mut Easy) -> CargoResult<HttpTimeout> {
    56    let http = gctx.http_config()?;
    57    if let Some(proxy) = super::proxy::http_proxy(http) {
    58        handle.proxy(&proxy)?;
    59    }
    60    if let Some(cainfo) = &http.cainfo {
    61        let cainfo = cainfo.resolve_path(gctx);
    62        handle.cainfo(&cainfo)?;
    63    }
    64    // Use `proxy_cainfo` if explicitly set; otherwise, fall back to `cainfo` as curl does #15376.
    65    if let Some(proxy_cainfo) = http.proxy_cainfo.as_ref().or(http.cainfo.as_ref()) {
    66        let proxy_cainfo = proxy_cainfo.resolve_path(gctx);
    67        handle.proxy_cainfo(&format!("{}", proxy_cainfo.display()))?;
    68    }
    69    if let Some(check) = http.check_revoke {
    70        handle.ssl_options(SslOpt::new().no_revoke(!check))?;
    71    }
    72
    73    if let Some(user_agent) = &http.user_agent {
    74        handle.useragent(user_agent)?;
    75    } else {
    76        handle.useragent(&format!("cargo/{}", version()))?;
    77    }
    78
    79    fn to_ssl_version(s: &str) -> CargoResult<SslVersion> {
    80        let version = match s {
    81            "default" => SslVersion::Default,
    82            "tlsv1" => SslVersion::Tlsv1,
    83            "tlsv1.0" => SslVersion::Tlsv10,
    84            "tlsv1.1" => SslVersion::Tlsv11,
    85            "tlsv1.2" => SslVersion::Tlsv12,
    86            "tlsv1.3" => SslVersion::Tlsv13,
    87            _ => bail!(
    88                "Invalid ssl version `{s}`,\
    89                 choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'."
    90            ),
    91        };
    92        Ok(version)
    93    }
    94
    95    // Empty string accept encoding expands to the encodings supported by the current libcurl.
    96    handle.accept_encoding("")?;
    97    if let Some(ssl_version) = &http.ssl_version {
    98        match ssl_version {
    99            SslVersionConfig::Single(s) => {
    100                let version = to_ssl_version(s.as_str())?;
    101                handle.ssl_version(version)?;
    102            }
    103            SslVersionConfig::Range(SslVersionConfigRange { min, max }) => {
    104                let min_version = min
    105                    .as_ref()
    106                    .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
    107                let max_version = max
    108                    .as_ref()
    109                    .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
    110                handle.ssl_min_max_version(min_version, max_version)?;
    111            }
    112        }
    113    } else if cfg!(windows) {
    114        // This is a temporary workaround for some bugs with libcurl and
    115        // schannel and TLS 1.3.
    116        //
    117        // Our libcurl on Windows is usually built with schannel.
    118        // On Windows 11 (or Windows Server 2022), libcurl recently (late
    119        // 2022) gained support for TLS 1.3 with schannel, and it now defaults
    120        // to 1.3. Unfortunately there have been some bugs with this.
    121        // https://github.com/curl/curl/issues/9431 is the most recent. Once
    122        // that has been fixed, and some time has passed where we can be more
    123        // confident that the 1.3 support won't cause issues, this can be
    124        // removed.
    125        //
    126        // Windows 10 is unaffected. libcurl does not support TLS 1.3 on
    127        // Windows 10. (Windows 10 sorta had support, but it required enabling
    128        // an advanced option in the registry which was buggy, and libcurl
    129        // does runtime checks to prevent it.)
    130        handle.ssl_min_max_version(SslVersion::Default, SslVersion::Tlsv12)?;
    131    }
    132
    133    if let Some(true) = http.debug {
    134        handle.verbose(true)?;
    135        tracing::debug!(target: "network", "{:#?}", curl::Version::get());
    136        handle.debug_function(|kind, data| {
    137            enum LogLevel {
    138                Debug,
    139                Trace,
    140            }
    141            use LogLevel::*;
    142            let (prefix, level) = match kind {
    143                InfoType::Text => ("*", Debug),
    144                InfoType::HeaderIn => ("<", Debug),
    145                InfoType::HeaderOut => (">", Debug),
    146                InfoType::DataIn => ("{", Trace),
    147                InfoType::DataOut => ("}", Trace),
    148                InfoType::SslDataIn | InfoType::SslDataOut => return,
    149                _ => return,
    150            };
    151            let starts_with_ignore_case = |line: &str, text: &str| -> bool {
    152                let line = line.as_bytes();
    153                let text = text.as_bytes();
    154                line[..line.len().min(text.len())].eq_ignore_ascii_case(text)
    155            };
    156            match str::from_utf8(data) {
    157                Ok(s) => {
    158                    for mut line in s.lines() {
    159                        if starts_with_ignore_case(line, "authorization:") {
    160                            line = "Authorization: [REDACTED]";
    161                        } else if starts_with_ignore_case(line, "h2h3 [authorization:") {
    162                            line = "h2h3 [Authorization: [REDACTED]]";
    163                        } else if starts_with_ignore_case(line, "set-cookie") {
    164                            line = "set-cookie: [REDACTED]";
    165                        }
    166                        match level {
    167                            Debug => debug!(target: "network", "http-debug: {prefix} {line}"),
    168                            Trace => trace!(target: "network", "http-debug: {prefix} {line}"),
    169                        }
    170                    }
    171                }
    172                Err(_) => {
    173                    let len = data.len();
    174                    match level {
    175                        Debug => {
    176                            debug!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
    177                        }
    178                        Trace => {
    179                            trace!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
    180                        }
    181                    }
    182                }
    183            }
    184        })?;
    185    }
    186
    187    HttpTimeout::new(gctx)
    188}
    189
    190#[must_use]
    191pub struct HttpTimeout {
    192    pub dur: Duration,
    193    pub low_speed_limit: u32,
    194}
    195
    196impl HttpTimeout {
    197    pub fn new(gctx: &GlobalContext) -> CargoResult<HttpTimeout> {
    198        let http_config = gctx.http_config()?;
    199        let low_speed_limit = http_config.low_speed_limit.unwrap_or(10);
    200        let seconds = http_config
    201            .timeout
    202            .or_else(|| {
    203                gctx.get_env("HTTP_TIMEOUT")
    204                    .ok()
    205                    .and_then(|s| s.parse().ok())
    206            })
    207            .unwrap_or(30);
    208        Ok(HttpTimeout {
    209            dur: Duration::new(seconds, 0),
    210            low_speed_limit,
    211        })
    212    }
    213
    214    pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
    215        // The timeout option for libcurl by default times out the entire
    216        // transfer, but we probably don't want this. Instead we only set
    217        // timeouts for the connect phase as well as a "low speed" timeout so
    218        // if we don't receive many bytes in a large-ish period of time then we
    219        // time out.
    220        handle.connect_timeout(self.dur)?;
    221        handle.low_speed_time(self.dur)?;
    222        handle.low_speed_limit(self.low_speed_limit)?;
    223        Ok(())
    224    }
    225}