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(check) = http.check_revoke {
65 handle.ssl_options(SslOpt::new().no_revoke(!check))?;
66 }
67
68 if let Some(user_agent) = &http.user_agent {
69 handle.useragent(user_agent)?;
70 } else {
71 handle.useragent(&format!("cargo/{}", version()))?;
72 }
73
74 fn to_ssl_version(s: &str) -> CargoResult<SslVersion> {
75 let version = match s {
76 "default" => SslVersion::Default,
77 "tlsv1" => SslVersion::Tlsv1,
78 "tlsv1.0" => SslVersion::Tlsv10,
79 "tlsv1.1" => SslVersion::Tlsv11,
80 "tlsv1.2" => SslVersion::Tlsv12,
81 "tlsv1.3" => SslVersion::Tlsv13,
82 _ => bail!(
83 "Invalid ssl version `{s}`,\
84 choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'."
85 ),
86 };
87 Ok(version)
88 }
89
90 handle.accept_encoding("")?;
92 if let Some(ssl_version) = &http.ssl_version {
93 match ssl_version {
94 SslVersionConfig::Single(s) => {
95 let version = to_ssl_version(s.as_str())?;
96 handle.ssl_version(version)?;
97 }
98 SslVersionConfig::Range(SslVersionConfigRange { min, max }) => {
99 let min_version = min
100 .as_ref()
101 .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
102 let max_version = max
103 .as_ref()
104 .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
105 handle.ssl_min_max_version(min_version, max_version)?;
106 }
107 }
108 } else if cfg!(windows) {
109 handle.ssl_min_max_version(SslVersion::Default, SslVersion::Tlsv12)?;
126 }
127
128 if let Some(true) = http.debug {
129 handle.verbose(true)?;
130 tracing::debug!(target: "network", "{:#?}", curl::Version::get());
131 handle.debug_function(|kind, data| {
132 enum LogLevel {
133 Debug,
134 Trace,
135 }
136 use LogLevel::*;
137 let (prefix, level) = match kind {
138 InfoType::Text => ("*", Debug),
139 InfoType::HeaderIn => ("<", Debug),
140 InfoType::HeaderOut => (">", Debug),
141 InfoType::DataIn => ("{", Trace),
142 InfoType::DataOut => ("}", Trace),
143 InfoType::SslDataIn | InfoType::SslDataOut => return,
144 _ => return,
145 };
146 let starts_with_ignore_case = |line: &str, text: &str| -> bool {
147 let line = line.as_bytes();
148 let text = text.as_bytes();
149 line[..line.len().min(text.len())].eq_ignore_ascii_case(text)
150 };
151 match str::from_utf8(data) {
152 Ok(s) => {
153 for mut line in s.lines() {
154 if starts_with_ignore_case(line, "authorization:") {
155 line = "Authorization: [REDACTED]";
156 } else if starts_with_ignore_case(line, "h2h3 [authorization:") {
157 line = "h2h3 [Authorization: [REDACTED]]";
158 } else if starts_with_ignore_case(line, "set-cookie") {
159 line = "set-cookie: [REDACTED]";
160 }
161 match level {
162 Debug => debug!(target: "network", "http-debug: {prefix} {line}"),
163 Trace => trace!(target: "network", "http-debug: {prefix} {line}"),
164 }
165 }
166 }
167 Err(_) => {
168 let len = data.len();
169 match level {
170 Debug => {
171 debug!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
172 }
173 Trace => {
174 trace!(target: "network", "http-debug: {prefix} ({len} bytes of data)")
175 }
176 }
177 }
178 }
179 })?;
180 }
181
182 HttpTimeout::new(gctx)
183}
184
185#[must_use]
186pub struct HttpTimeout {
187 pub dur: Duration,
188 pub low_speed_limit: u32,
189}
190
191impl HttpTimeout {
192 pub fn new(gctx: &GlobalContext) -> CargoResult<HttpTimeout> {
193 let http_config = gctx.http_config()?;
194 let low_speed_limit = http_config.low_speed_limit.unwrap_or(10);
195 let seconds = http_config
196 .timeout
197 .or_else(|| {
198 gctx.get_env("HTTP_TIMEOUT")
199 .ok()
200 .and_then(|s| s.parse().ok())
201 })
202 .unwrap_or(30);
203 Ok(HttpTimeout {
204 dur: Duration::new(seconds, 0),
205 low_speed_limit,
206 })
207 }
208
209 pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
210 handle.connect_timeout(self.dur)?;
216 handle.low_speed_time(self.dur)?;
217 handle.low_speed_limit(self.low_speed_limit)?;
218 Ok(())
219 }
220}