cargo/util/network/
http.rs
1use 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
20pub 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 let mut handle = Easy::new();
40 let timeout = configure_http_handle(gctx, &mut handle)?;
41 Ok((handle, timeout))
42}
43
44pub 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
54pub 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 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 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 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(|kind, data| {
137 enum LogLevel {
138 Debug,
139 Trace,
140 }
141 use LogLevel::*;
142 let (prefix, level) = match kind {
143 InfoType::Text => ("*", Debug),
144 InfoType::HeaderIn => ("<", Debug),
145 InfoType::HeaderOut => (">", Debug),
146 InfoType::DataIn => ("{", Trace),
147 InfoType::DataOut => ("}", Trace),
148 InfoType::SslDataIn | InfoType::SslDataOut => return,
149 _ => return,
150 };
151 let starts_with_ignore_case = |line: &str, text: &str| -> bool {
152 let line = line.as_bytes();
153 let text = text.as_bytes();
154 line[..line.len().min(text.len())].eq_ignore_ascii_case(text)
155 };
156 match str::from_utf8(data) {
157 Ok(s) => {
158 for mut line in s.lines() {
159 if starts_with_ignore_case(line, "authorization:") {
160 line = "Authorization: [REDACTED]";
161 } else if starts_with_ignore_case(line, "h2h3 [authorization:") {
162 line = "h2h3 [Authorization: [REDACTED]]";
163 } else if starts_with_ignore_case(line, "set-cookie") {
164 line = "set-cookie: [REDACTED]";
165 }
166 match level {
167 Debug => debug!(target: "network", "http-debug: {prefix} {line}"),
168 Trace => trace!(target: "network", "http-debug: {prefix} {line}"),
169 }
170 }
171 }
172 Err(_) => {
173 let len = data.len();
174 match level {
175 Debug => {
176 debug!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
177 }
178 Trace => {
179 trace!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
180 }
181 }
182 }
183 }
184 })?;
185 }
186
187 HttpTimeout::new(gctx)
188}
189
190#[must_use]
191pub struct HttpTimeout {
192 pub dur: Duration,
193 pub low_speed_limit: u32,
194}
195
196impl HttpTimeout {
197 pub fn new(gctx: &GlobalContext) -> CargoResult<HttpTimeout> {
198 let http_config = gctx.http_config()?;
199 let low_speed_limit = http_config.low_speed_limit.unwrap_or(10);
200 let seconds = http_config
201 .timeout
202 .or_else(|| {
203 gctx.get_env("HTTP_TIMEOUT")
204 .ok()
205 .and_then(|s| s.parse().ok())
206 })
207 .unwrap_or(30);
208 Ok(HttpTimeout {
209 dur: Duration::new(seconds, 0),
210 low_speed_limit,
211 })
212 }
213
214 pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
215 handle.connect_timeout(self.dur)?;
221 handle.low_speed_time(self.dur)?;
222 handle.low_speed_limit(self.low_speed_limit)?;
223 Ok(())
224 }
225}