diff --git a/src/bin/cargo/main.rs b/src/bin/cargo/main.rs index 9fb6635ea77d..40d8cff417ab 100644 --- a/src/bin/cargo/main.rs +++ b/src/bin/cargo/main.rs @@ -293,12 +293,12 @@ fn init_git(config: &Config) { /// configured to use libcurl instead of the built-in networking support so /// that those configuration settings can be used. fn init_git_transports(config: &Config) { - match cargo::ops::needs_custom_http_transport(config) { + match cargo::http::needs_custom_http_transport(config) { Ok(true) => {} _ => return, } - let handle = match cargo::ops::http_handle(config) { + let handle = match cargo::http::http_handle(config) { Ok(handle) => handle, Err(..) => return, }; diff --git a/src/cargo/core/package.rs b/src/cargo/core/package.rs index 44e2ccbfd39e..f4ab448d28dd 100644 --- a/src/cargo/core/package.rs +++ b/src/cargo/core/package.rs @@ -24,10 +24,11 @@ use crate::core::resolver::{HasDevUnits, Resolve}; use crate::core::source::MaybePackage; use crate::core::{Dependency, Manifest, PackageId, SourceId, Target}; use crate::core::{SourceMap, Summary, Workspace}; -use crate::ops; use crate::util::config::PackageCacheLock; use crate::util::errors::{CargoResult, HttpNotSuccessful, DEBUG_HEADERS}; use crate::util::interning::InternedString; +use crate::util::network::http::http_handle_and_timeout; +use crate::util::network::http::HttpTimeout; use crate::util::network::retry::{Retry, RetryResult}; use crate::util::network::sleep::SleepTracker; use crate::util::{self, internal, Config, Progress, ProgressStyle}; @@ -348,7 +349,7 @@ pub struct Downloads<'a, 'cfg> { /// Note that timeout management is done manually here instead of in libcurl /// because we want to apply timeouts to an entire batch of operations, not /// any one particular single operation. - timeout: ops::HttpTimeout, + timeout: HttpTimeout, /// Last time bytes were received. updated_at: Cell, /// This is a slow-speed check. It is reset to `now + timeout_duration` @@ -441,7 +442,7 @@ impl<'cfg> PackageSet<'cfg> { pub fn enable_download<'a>(&'a self) -> CargoResult> { assert!(!self.downloading.replace(true)); - let timeout = ops::HttpTimeout::new(self.config)?; + let timeout = HttpTimeout::new(self.config)?; Ok(Downloads { start: Instant::now(), set: self, @@ -713,7 +714,7 @@ impl<'a, 'cfg> Downloads<'a, 'cfg> { debug!("downloading {} as {}", id, token); assert!(self.pending_ids.insert(id)); - let (mut handle, _timeout) = ops::http_handle_and_timeout(self.set.config)?; + let (mut handle, _timeout) = http_handle_and_timeout(self.set.config)?; handle.get(true)?; handle.url(&url)?; handle.follow_location(true)?; // follow redirects diff --git a/src/cargo/lib.rs b/src/cargo/lib.rs index a03d51199787..253e83d7a864 100644 --- a/src/cargo/lib.rs +++ b/src/cargo/lib.rs @@ -150,6 +150,7 @@ use anyhow::Error; use log::debug; pub use crate::util::errors::{AlreadyPrintedError, InternalError, VerboseError}; +pub use crate::util::network::http; pub use crate::util::{indented_lines, CargoResult, CliError, CliResult, Config}; pub use crate::version::version; diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 4b6aea991bde..d4ec442dd918 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -21,11 +21,15 @@ pub use self::cargo_test::{run_benches, run_tests, TestOptions}; pub use self::cargo_uninstall::uninstall; pub use self::fix::{fix, fix_exec_rustc, fix_get_proxy_lock_addr, FixOptions}; pub use self::lockfile::{load_pkg_lockfile, resolve_to_string, write_pkg_lockfile}; -pub use self::registry::HttpTimeout; -pub use self::registry::{configure_http_handle, http_handle, http_handle_and_timeout}; -pub use self::registry::{modify_owners, yank, OwnersOptions, PublishOpts}; -pub use self::registry::{needs_custom_http_transport, registry_login, registry_logout, search}; -pub use self::registry::{publish, RegistryCredentialConfig}; +pub use self::registry::modify_owners; +pub use self::registry::publish; +pub use self::registry::registry_login; +pub use self::registry::registry_logout; +pub use self::registry::search; +pub use self::registry::yank; +pub use self::registry::OwnersOptions; +pub use self::registry::PublishOpts; +pub use self::registry::RegistryCredentialConfig; pub use self::resolve::{ add_overrides, get_resolved_packages, resolve_with_previous, resolve_ws, resolve_ws_with_opts, WorkspaceResolve, diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs index 5bc78c4d452d..ed26974659f1 100644 --- a/src/cargo/ops/registry/mod.rs +++ b/src/cargo/ops/registry/mod.rs @@ -13,22 +13,18 @@ use std::collections::HashSet; use std::path::PathBuf; use std::str; use std::task::Poll; -use std::time::Duration; use anyhow::{bail, format_err, Context as _}; use crates_io::{self, Registry}; -use curl::easy::{Easy, InfoType, SslOpt, SslVersion}; -use log::{log, Level}; use crate::core::source::Source; use crate::core::SourceId; use crate::sources::{RegistrySource, SourceConfigMap}; use crate::util::auth::{self, Secret}; -use crate::util::config::{Config, SslVersionConfig, SslVersionConfigRange}; +use crate::util::config::Config; use crate::util::errors::CargoResult; -use crate::util::network; +use crate::util::network::http::http_handle; use crate::util::IntoUrl; -use crate::version; pub use self::login::registry_login; pub use self::logout::registry_logout; @@ -158,204 +154,6 @@ fn registry( )) } -/// Creates a new HTTP handle with appropriate global configuration for cargo. -pub fn http_handle(config: &Config) -> CargoResult { - 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 { - Ok( - network::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 { - let http = config.http_config()?; - if let Some(proxy) = network::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 { - 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)?; - log::debug!("{:#?}", curl::Version::get()); - handle.debug_function(|kind, data| { - let (prefix, level) = match kind { - InfoType::Text => ("*", Level::Debug), - InfoType::HeaderIn => ("<", Level::Debug), - InfoType::HeaderOut => (">", Level::Debug), - InfoType::DataIn => ("{", Level::Trace), - InfoType::DataOut => ("}", Level::Trace), - InfoType::SslDataIn | InfoType::SslDataOut => return, - _ => return, - }; - let starts_with_ignore_case = |line: &str, text: &str| -> bool { - 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]"; - } - log!(level, "http-debug: {} {}", prefix, line); - } - } - Err(_) => { - log!( - level, - "http-debug: {} ({} bytes of data)", - prefix, - data.len() - ); - } - } - })?; - } - - HttpTimeout::new(config) -} - -#[must_use] -pub struct HttpTimeout { - pub dur: Duration, - pub low_speed_limit: u32, -} - -impl HttpTimeout { - pub fn new(config: &Config) -> CargoResult { - 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(()) - } -} - /// Gets the SourceId for an index or registry setting. /// /// The `index` and `reg` values are from the command-line or config settings. diff --git a/src/cargo/sources/git/oxide.rs b/src/cargo/sources/git/oxide.rs index 7f5298ef8466..e86c63e8ee96 100644 --- a/src/cargo/sources/git/oxide.rs +++ b/src/cargo/sources/git/oxide.rs @@ -1,7 +1,7 @@ //! This module contains all code sporting `gitoxide` for operations on `git` repositories and it mirrors //! `utils` closely for now. One day it can be renamed into `utils` once `git2` isn't required anymore. -use crate::ops::HttpTimeout; +use crate::util::network::http::HttpTimeout; use crate::util::{human_readable_bytes, network, MetricsCounter, Progress}; use crate::{CargoResult, Config}; use cargo_util::paths; diff --git a/src/cargo/sources/registry/http_remote.rs b/src/cargo/sources/registry/http_remote.rs index 96e8dd2d7b9d..c69ef8f9bed2 100644 --- a/src/cargo/sources/registry/http_remote.rs +++ b/src/cargo/sources/registry/http_remote.rs @@ -1,11 +1,11 @@ //! Access to a HTTP-based crate registry. See [`HttpRegistry`] for details. use crate::core::{PackageId, SourceId}; -use crate::ops; use crate::sources::registry::download; use crate::sources::registry::MaybeLock; use crate::sources::registry::{LoadResponse, RegistryConfig, RegistryData}; use crate::util::errors::{CargoResult, HttpNotSuccessful, DEBUG_HEADERS}; +use crate::util::network::http::http_handle; use crate::util::network::retry::{Retry, RetryResult}; use crate::util::network::sleep::SleepTracker; use crate::util::{auth, Config, Filesystem, IntoUrl, Progress, ProgressStyle}; @@ -610,7 +610,7 @@ impl<'cfg> RegistryData for HttpRegistry<'cfg> { // Looks like we're going to have to do a network request. self.start_fetch()?; - let mut handle = ops::http_handle(self.config)?; + let mut handle = http_handle(self.config)?; let full_url = self.full_url(path); debug!("fetch {}", full_url); handle.get(true)?; diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index dfd0bb198792..68f6f2139ebf 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -69,9 +69,11 @@ use self::ConfigValue as CV; use crate::core::compiler::rustdoc::RustdocExternMap; use crate::core::shell::Verbosity; use crate::core::{features, CliUnstable, Shell, SourceId, Workspace, WorkspaceRootConfig}; -use crate::ops::{self, RegistryCredentialConfig}; +use crate::ops::RegistryCredentialConfig; use crate::util::auth::Secret; use crate::util::errors::CargoResult; +use crate::util::network::http::configure_http_handle; +use crate::util::network::http::http_handle; use crate::util::CanonicalUrl; use crate::util::{internal, toml as cargo_toml}; use crate::util::{try_canonicalize, validate_package_name}; @@ -1706,11 +1708,11 @@ impl Config { pub fn http(&self) -> CargoResult<&RefCell> { let http = self .easy - .try_borrow_with(|| ops::http_handle(self).map(RefCell::new))?; + .try_borrow_with(|| http_handle(self).map(RefCell::new))?; { let mut http = http.borrow_mut(); http.reset(); - let timeout = ops::configure_http_handle(self, &mut http)?; + let timeout = configure_http_handle(self, &mut http)?; timeout.configure(&mut http)?; } Ok(http) diff --git a/src/cargo/util/network/http.rs b/src/cargo/util/network/http.rs new file mode 100644 index 000000000000..f077ce2b6409 --- /dev/null +++ b/src/cargo/util/network/http.rs @@ -0,0 +1,216 @@ +//! 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 log::log; +use log::Level; + +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 { + 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 { + 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 { + 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 { + 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)?; + log::debug!("{:#?}", curl::Version::get()); + handle.debug_function(|kind, data| { + let (prefix, level) = match kind { + InfoType::Text => ("*", Level::Debug), + InfoType::HeaderIn => ("<", Level::Debug), + InfoType::HeaderOut => (">", Level::Debug), + InfoType::DataIn => ("{", Level::Trace), + InfoType::DataOut => ("}", Level::Trace), + InfoType::SslDataIn | InfoType::SslDataOut => return, + _ => return, + }; + let starts_with_ignore_case = |line: &str, text: &str| -> bool { + 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]"; + } + log!(level, "http-debug: {} {}", prefix, line); + } + } + Err(_) => { + log!( + level, + "http-debug: {} ({} bytes of data)", + prefix, + data.len() + ); + } + } + })?; + } + + HttpTimeout::new(config) +} + +#[must_use] +pub struct HttpTimeout { + pub dur: Duration, + pub low_speed_limit: u32, +} + +impl HttpTimeout { + pub fn new(config: &Config) -> CargoResult { + 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(()) + } +} diff --git a/src/cargo/util/network/mod.rs b/src/cargo/util/network/mod.rs index d03a32fede7b..b078fa3527d2 100644 --- a/src/cargo/util/network/mod.rs +++ b/src/cargo/util/network/mod.rs @@ -2,6 +2,7 @@ use std::task::Poll; +pub mod http; pub mod proxy; pub mod retry; pub mod sleep;