cargo/util/network/
http.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
//! 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::context::SslVersionConfig;
use crate::util::context::SslVersionConfigRange;
use crate::version;
use crate::CargoResult;
use crate::GlobalContext;

/// Creates a new HTTP handle with appropriate global configuration for cargo.
pub fn http_handle(gctx: &GlobalContext) -> CargoResult<Easy> {
    let (mut handle, timeout) = http_handle_and_timeout(gctx)?;
    timeout.configure(&mut handle)?;
    Ok(handle)
}

pub fn http_handle_and_timeout(gctx: &GlobalContext) -> CargoResult<(Easy, HttpTimeout)> {
    if gctx.frozen() {
        bail!(
            "attempting to make an HTTP request, but --frozen was \
             specified"
        )
    }
    if gctx.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(gctx, &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(gctx: &GlobalContext) -> CargoResult<bool> {
    Ok(super::proxy::http_proxy_exists(gctx.http_config()?, gctx)
        || *gctx.http_config()? != Default::default()
        || gctx.get_env_os("HTTP_TIMEOUT").is_some())
}

/// Configure a libcurl http handle with the defaults options for Cargo
pub fn configure_http_handle(gctx: &GlobalContext, handle: &mut Easy) -> CargoResult<HttpTimeout> {
    let http = gctx.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(gctx);
        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!(target: "network", "{:#?}", 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!(target: "network", "http-debug: {prefix} {line}"),
                            Trace => trace!(target: "network", "http-debug: {prefix} {line}"),
                        }
                    }
                }
                Err(_) => {
                    let len = data.len();
                    match level {
                        Debug => {
                            debug!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
                        }
                        Trace => {
                            trace!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
                        }
                    }
                }
            }
        })?;
    }

    HttpTimeout::new(gctx)
}

#[must_use]
pub struct HttpTimeout {
    pub dur: Duration,
    pub low_speed_limit: u32,
}

impl HttpTimeout {
    pub fn new(gctx: &GlobalContext) -> CargoResult<HttpTimeout> {
        let http_config = gctx.http_config()?;
        let low_speed_limit = http_config.low_speed_limit.unwrap_or(10);
        let seconds = http_config
            .timeout
            .or_else(|| {
                gctx.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(())
    }
}