cargo/util/network/
http.rs1use 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 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 let mut handle = Easy::new();
46 let timeout = configure_http_handle(gctx, &mut handle)?;
47 Ok((handle, timeout))
48}
49
50pub 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
60pub 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 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 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 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}