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