Skip to main content

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(debug)?;
137    }
138
139    HttpTimeout::new(gctx)
140}
141
142pub fn debug(kind: InfoType, data: &[u8]) {
143    enum LogLevel {
144        Debug,
145        Trace,
146    }
147    use LogLevel::*;
148    let (prefix, level) = match kind {
149        InfoType::Text => ("*", Debug),
150        InfoType::HeaderIn => ("<", Debug),
151        InfoType::HeaderOut => (">", Debug),
152        InfoType::DataIn => ("{", Trace),
153        InfoType::DataOut => ("}", Trace),
154        InfoType::SslDataIn | InfoType::SslDataOut => return,
155        _ => return,
156    };
157    let starts_with_ignore_case = |line: &str, text: &str| -> bool {
158        let line = line.as_bytes();
159        let text = text.as_bytes();
160        line[..line.len().min(text.len())].eq_ignore_ascii_case(text)
161    };
162    match str::from_utf8(data) {
163        Ok(s) => {
164            for mut line in s.lines() {
165                if starts_with_ignore_case(line, "authorization:") {
166                    line = "Authorization: [REDACTED]";
167                } else if starts_with_ignore_case(line, "h2h3 [authorization:") {
168                    line = "h2h3 [Authorization: [REDACTED]]";
169                } else if starts_with_ignore_case(line, "set-cookie") {
170                    line = "set-cookie: [REDACTED]";
171                }
172                match level {
173                    Debug => debug!(target: "network", "http-debug: {prefix} {line}"),
174                    Trace => trace!(target: "network", "http-debug: {prefix} {line}"),
175                }
176            }
177        }
178        Err(_) => {
179            let len = data.len();
180            match level {
181                Debug => {
182                    debug!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
183                }
184                Trace => {
185                    trace!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
186                }
187            }
188        }
189    }
190}
191
192#[must_use]
193pub struct HttpTimeout {
194    pub dur: Duration,
195    pub low_speed_limit: u32,
196}
197
198impl HttpTimeout {
199    pub fn new(gctx: &GlobalContext) -> CargoResult<HttpTimeout> {
200        let http_config = gctx.http_config()?;
201        let low_speed_limit = http_config.low_speed_limit.unwrap_or(10);
202        let seconds = http_config
203            .timeout
204            .or_else(|| {
205                gctx.get_env("HTTP_TIMEOUT")
206                    .ok()
207                    .and_then(|s| s.parse().ok())
208            })
209            .unwrap_or(30);
210        Ok(HttpTimeout {
211            dur: Duration::new(seconds, 0),
212            low_speed_limit,
213        })
214    }
215
216    pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
217        // The timeout option for libcurl by default times out the entire
218        // transfer, but we probably don't want this. Instead we only set
219        // timeouts for the connect phase as well as a "low speed" timeout so
220        // if we don't receive many bytes in a large-ish period of time then we
221        // time out.
222        handle.connect_timeout(self.dur)?;
223        handle.low_speed_time(self.dur)?;
224        handle.low_speed_limit(self.low_speed_limit)?;
225        Ok(())
226    }
227}