blob: 73880f60e16fdfa7521db07cc36ce921afa3d6d0 [file] [log] [blame]
//! Configures libcurl's http handles.
use std::str;
use std::time::Duration;
use anyhow::bail;
use curl::easy::Easy;
use curl::easy::InfoType;
use curl::easy::SslOpt;
use curl::easy::SslVersion;
use tracing::debug;
use tracing::trace;
use crate::util::config::SslVersionConfig;
use crate::util::config::SslVersionConfigRange;
use crate::version;
use crate::CargoResult;
use crate::Config;
/// Creates a new HTTP handle with appropriate global configuration for cargo.
pub fn http_handle(config: &Config) -> CargoResult<Easy> {
let (mut handle, timeout) = http_handle_and_timeout(config)?;
timeout.configure(&mut handle)?;
Ok(handle)
}
pub fn http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeout)> {
if config.frozen() {
bail!(
"attempting to make an HTTP request, but --frozen was \
specified"
)
}
if config.offline() {
bail!(
"attempting to make an HTTP request, but --offline was \
specified"
)
}
// The timeout option for libcurl by default times out the entire transfer,
// but we probably don't want this. Instead we only set timeouts for the
// connect phase as well as a "low speed" timeout so if we don't receive
// many bytes in a large-ish period of time then we time out.
let mut handle = Easy::new();
let timeout = configure_http_handle(config, &mut handle)?;
Ok((handle, timeout))
}
// Only use a custom transport if any HTTP options are specified,
// such as proxies or custom certificate authorities.
//
// The custom transport, however, is not as well battle-tested.
pub fn needs_custom_http_transport(config: &Config) -> CargoResult<bool> {
Ok(
super::proxy::http_proxy_exists(config.http_config()?, config)
|| *config.http_config()? != Default::default()
|| config.get_env_os("HTTP_TIMEOUT").is_some(),
)
}
/// Configure a libcurl http handle with the defaults options for Cargo
pub fn configure_http_handle(config: &Config, handle: &mut Easy) -> CargoResult<HttpTimeout> {
let http = config.http_config()?;
if let Some(proxy) = super::proxy::http_proxy(http) {
handle.proxy(&proxy)?;
}
if let Some(cainfo) = &http.cainfo {
let cainfo = cainfo.resolve_path(config);
handle.cainfo(&cainfo)?;
}
if let Some(check) = http.check_revoke {
handle.ssl_options(SslOpt::new().no_revoke(!check))?;
}
if let Some(user_agent) = &http.user_agent {
handle.useragent(user_agent)?;
} else {
handle.useragent(&format!("cargo {}", version()))?;
}
fn to_ssl_version(s: &str) -> CargoResult<SslVersion> {
let version = match s {
"default" => SslVersion::Default,
"tlsv1" => SslVersion::Tlsv1,
"tlsv1.0" => SslVersion::Tlsv10,
"tlsv1.1" => SslVersion::Tlsv11,
"tlsv1.2" => SslVersion::Tlsv12,
"tlsv1.3" => SslVersion::Tlsv13,
_ => bail!(
"Invalid ssl version `{s}`,\
choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'."
),
};
Ok(version)
}
// Empty string accept encoding expands to the encodings supported by the current libcurl.
handle.accept_encoding("")?;
if let Some(ssl_version) = &http.ssl_version {
match ssl_version {
SslVersionConfig::Single(s) => {
let version = to_ssl_version(s.as_str())?;
handle.ssl_version(version)?;
}
SslVersionConfig::Range(SslVersionConfigRange { min, max }) => {
let min_version = min
.as_ref()
.map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
let max_version = max
.as_ref()
.map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
handle.ssl_min_max_version(min_version, max_version)?;
}
}
} else if cfg!(windows) {
// This is a temporary workaround for some bugs with libcurl and
// schannel and TLS 1.3.
//
// Our libcurl on Windows is usually built with schannel.
// On Windows 11 (or Windows Server 2022), libcurl recently (late
// 2022) gained support for TLS 1.3 with schannel, and it now defaults
// to 1.3. Unfortunately there have been some bugs with this.
// https://github.com/curl/curl/issues/9431 is the most recent. Once
// that has been fixed, and some time has passed where we can be more
// confident that the 1.3 support won't cause issues, this can be
// removed.
//
// Windows 10 is unaffected. libcurl does not support TLS 1.3 on
// Windows 10. (Windows 10 sorta had support, but it required enabling
// an advanced option in the registry which was buggy, and libcurl
// does runtime checks to prevent it.)
handle.ssl_min_max_version(SslVersion::Default, SslVersion::Tlsv12)?;
}
if let Some(true) = http.debug {
handle.verbose(true)?;
tracing::debug!("{:#?}", curl::Version::get());
handle.debug_function(|kind, data| {
enum LogLevel {
Debug,
Trace,
}
use LogLevel::*;
let (prefix, level) = match kind {
InfoType::Text => ("*", Debug),
InfoType::HeaderIn => ("<", Debug),
InfoType::HeaderOut => (">", Debug),
InfoType::DataIn => ("{", Trace),
InfoType::DataOut => ("}", Trace),
InfoType::SslDataIn | InfoType::SslDataOut => return,
_ => return,
};
let starts_with_ignore_case = |line: &str, text: &str| -> bool {
let line = line.as_bytes();
let text = text.as_bytes();
line[..line.len().min(text.len())].eq_ignore_ascii_case(text)
};
match str::from_utf8(data) {
Ok(s) => {
for mut line in s.lines() {
if starts_with_ignore_case(line, "authorization:") {
line = "Authorization: [REDACTED]";
} else if starts_with_ignore_case(line, "h2h3 [authorization:") {
line = "h2h3 [Authorization: [REDACTED]]";
} else if starts_with_ignore_case(line, "set-cookie") {
line = "set-cookie: [REDACTED]";
}
match level {
Debug => debug!("http-debug: {prefix} {line}"),
Trace => trace!("http-debug: {prefix} {line}"),
}
}
}
Err(_) => {
let len = data.len();
match level {
Debug => debug!("http-debug: {prefix} ({len} bytes of data)"),
Trace => trace!("http-debug: {prefix} ({len} bytes of data)"),
}
}
}
})?;
}
HttpTimeout::new(config)
}
#[must_use]
pub struct HttpTimeout {
pub dur: Duration,
pub low_speed_limit: u32,
}
impl HttpTimeout {
pub fn new(config: &Config) -> CargoResult<HttpTimeout> {
let http_config = config.http_config()?;
let low_speed_limit = http_config.low_speed_limit.unwrap_or(10);
let seconds = http_config
.timeout
.or_else(|| {
config
.get_env("HTTP_TIMEOUT")
.ok()
.and_then(|s| s.parse().ok())
})
.unwrap_or(30);
Ok(HttpTimeout {
dur: Duration::new(seconds, 0),
low_speed_limit,
})
}
pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
// The timeout option for libcurl by default times out the entire
// transfer, but we probably don't want this. Instead we only set
// timeouts for the connect phase as well as a "low speed" timeout so
// if we don't receive many bytes in a large-ish period of time then we
// time out.
handle.connect_timeout(self.dur)?;
handle.low_speed_time(self.dur)?;
handle.low_speed_limit(self.low_speed_limit)?;
Ok(())
}
}