Skip to main content

cargo/util/network/
http.rs

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