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