Upgrade reqwest to v0.12.3 (#2817)

## Summary

Closes #2814.
This commit is contained in:
Charlie Marsh 2024-04-10 11:20:44 -04:00 committed by GitHub
parent d8323551f8
commit a01143980a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 312 additions and 450 deletions

511
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -55,7 +55,7 @@ anyhow = { version = "1.0.80" }
async-channel = { version = "2.2.0" }
async-compression = { version = "0.4.6" }
async-trait = { version = "0.1.78" }
async_http_range_reader = { version = "0.7.0" }
async_http_range_reader = { version = "0.7.1" }
async_zip = { git = "https://github.com/charliermarsh/rs-async-zip", rev = "1dcb40cfe1bf5325a6fd4bfcf9894db40241f585", features = ["deflate"] }
axoupdater = { version = "0.4.0", default-features = false }
backoff = { version = "0.4.0" }
@ -86,7 +86,7 @@ hex = { version = "0.4.3" }
hmac = { version = "0.12.1" }
home = { version = "0.5.9" }
html-escape = { version = "0.2.13" }
http = { version = "0.2.12" }
http = { version = "1.1.0" }
indexmap = { version = "2.2.5" }
indicatif = { version = "0.17.7" }
indoc = { version = "2.0.4" }
@ -107,9 +107,9 @@ rand = { version = "0.8.5" }
rayon = { version = "1.8.0" }
reflink-copy = { version = "0.1.15" }
regex = { version = "1.10.2" }
reqwest = { version = "0.11.23", default-features = false, features = ["json", "gzip", "brotli", "stream", "rustls-tls", "rustls-tls-native-roots"] }
reqwest-middleware = { version = "0.2.4" }
reqwest-retry = { version = "0.3.0" }
reqwest = { version = "0.12.3", default-features = false, features = ["json", "gzip", "brotli", "stream", "rustls-tls", "rustls-tls-native-roots"] }
reqwest-middleware = { version = "0.3.0" }
reqwest-retry = { version = "0.5.0" }
rkyv = { version = "0.7.43", features = ["strict", "validation"] }
rmp-serde = { version = "1.1.2" }
rust-netrc = { version = "0.1.1" }

View file

@ -7,15 +7,15 @@ edition = "2021"
async-trait = { workspace = true }
base64 = { workspace = true }
clap = { workspace = true, features = ["derive", "env"], optional = true }
http = { workspace = true }
once_cell = { workspace = true }
reqwest = { workspace = true }
reqwest-middleware = { workspace = true }
rust-netrc = { workspace = true }
task-local-extensions = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
once_cell = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View file

@ -1,9 +1,9 @@
use http::Extensions;
use std::path::Path;
use netrc::Netrc;
use reqwest::{header::HeaderValue, Request, Response};
use reqwest_middleware::{Middleware, Next};
use task_local_extensions::Extensions;
use tracing::{debug, warn};
use crate::{

View file

@ -48,13 +48,11 @@ tracing = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
# These must be kept in-sync with those used by `reqwest`.
rustls = { version = "0.21.10" }
rustls-native-certs = { version = "0.6.3" }
webpki-roots = { version = "0.25.4" }
[dev-dependencies]
anyhow = { workspace = true }
hyper = { version = "0.14.28", features = ["server", "http1"] }
http-body-util = { version = "0.1.0" }
hyper = { version = "1.2.0", features = ["server", "http1"] }
hyper-util = { version = "0.1.3", features = ["tokio"] }
insta = { version = "1.36.1" }
os_info = { version = "=3.7.0", default-features = false }
tokio = { workspace = true, features = ["fs", "macros"] }

View file

@ -16,8 +16,7 @@ use uv_warnings::warn_user_once;
use crate::linehaul::LineHaul;
use crate::middleware::OfflineMiddleware;
use crate::tls::Roots;
use crate::{tls, Connectivity};
use crate::Connectivity;
/// A builder for an [`BaseClient`].
#[derive(Debug, Clone)]
@ -140,19 +139,20 @@ impl<'a> BaseClientBuilder<'a> {
}
path_exists
});
// Load the TLS configuration.
let tls = tls::load(if self.native_tls || ssl_cert_file_exists {
Roots::Native
} else {
Roots::Webpki
})
.expect("Failed to load TLS configuration.");
// Configure the builder.
let client_core = ClientBuilder::new()
.user_agent(user_agent_string)
.pool_max_idle_per_host(20)
.timeout(std::time::Duration::from_secs(timeout))
.use_preconfigured_tls(tls);
.tls_built_in_root_certs(false);
// Configure TLS.
let client_core = if self.native_tls || ssl_cert_file_exists {
client_core.tls_built_in_native_certs(true)
} else {
client_core.tls_built_in_webpki_certs(true)
};
client_core.build().expect("Failed to build HTTP client.")
});

View file

@ -20,4 +20,3 @@ mod middleware;
mod registry_client;
mod remote_metadata;
mod rkyvutil;
mod tls;

View file

@ -1,8 +1,8 @@
use http::Extensions;
use std::fmt::Debug;
use reqwest::{Request, Response};
use reqwest_middleware::{Middleware, Next};
use task_local_extensions::Extensions;
use url::Url;
/// A custom error type for the offline middleware.

View file

@ -1,102 +0,0 @@
use rustls::ClientConfig;
use tracing::warn;
#[derive(thiserror::Error, Debug)]
pub(crate) enum TlsError {
#[error(transparent)]
Rustls(#[from] rustls::Error),
#[error("zero valid certificates found in native root store")]
ZeroCertificates,
#[error("failed to load native root certificates")]
NativeCertificates(#[source] std::io::Error),
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Roots {
/// Use reqwest's `rustls-tls-webpki-roots` behavior for loading root certificates.
Webpki,
/// Use reqwest's `rustls-tls-native-roots` behavior for loading root certificates.
Native,
}
/// Initialize a TLS configuration for the client.
///
/// This is equivalent to the TLS initialization `reqwest` when `rustls-tls` is enabled,
/// with two notable changes:
///
/// 1. It enables _either_ the `webpki-roots` or the `native-certs` feature, but not both.
/// 2. It assumes the following builder settings (which match the defaults):
/// - `root_certs: vec![]`
/// - `min_tls_version: None`
/// - `max_tls_version: None`
/// - `identity: None`
/// - `certs_verification: false`
/// - `tls_sni: true`
/// - `http_version_pref: HttpVersionPref::All`
///
/// See: <https://github.com/seanmonstar/reqwest/blob/e3192638518d577759dd89da489175b8f992b12f/src/async_impl/client.rs#L498>
pub(crate) fn load(roots: Roots) -> Result<ClientConfig, TlsError> {
// Set root certificates.
let mut root_cert_store = rustls::RootCertStore::empty();
match roots {
Roots::Webpki => {
// Use `rustls-tls-webpki-roots`
use rustls::OwnedTrustAnchor;
let trust_anchors = webpki_roots::TLS_SERVER_ROOTS.iter().map(|trust_anchor| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
trust_anchor.subject,
trust_anchor.spki,
trust_anchor.name_constraints,
)
});
root_cert_store.add_trust_anchors(trust_anchors);
}
Roots::Native => {
// Use: `rustls-tls-native-roots`
let mut valid_count = 0;
let mut invalid_count = 0;
for cert in
rustls_native_certs::load_native_certs().map_err(TlsError::NativeCertificates)?
{
let cert = rustls::Certificate(cert.0);
// Continue on parsing errors, as native stores often include ancient or syntactically
// invalid certificates, like root certificates without any X509 extensions.
// Inspiration: https://github.com/rustls/rustls/blob/633bf4ba9d9521a95f68766d04c22e2b01e68318/rustls/src/anchors.rs#L105-L112
match root_cert_store.add(&cert) {
Ok(_) => valid_count += 1,
Err(err) => {
invalid_count += 1;
warn!(
"rustls failed to parse DER certificate {:?} {:?}",
&err, &cert
);
}
}
}
if valid_count == 0 && invalid_count > 0 {
return Err(TlsError::ZeroCertificates);
}
}
}
// Build TLS config
let config_builder = ClientConfig::builder()
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_protocol_versions(rustls::ALL_VERSIONS)?
.with_root_certificates(root_cert_store);
// Finalize TLS config
let mut tls = config_builder.with_no_client_auth();
// Enable SNI
tls.enable_sni = true;
// ALPN protocol
tls.alpn_protocols = vec!["h2".into(), "http/1.1".into()];
Ok(tls)
}

View file

@ -3,10 +3,13 @@ use std::io::Write;
use anyhow::Result;
use futures::future;
use hyper::header::AUTHORIZATION;
use hyper::server::conn::Http;
use http::header::AUTHORIZATION;
use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Body, Request, Response};
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tempfile::NamedTempFile;
use tokio::net::TcpListener;
@ -21,7 +24,7 @@ async fn test_client_with_netrc_credentials() -> Result<()> {
// Spawn the server loop in a background task
tokio::spawn(async move {
let svc = service_fn(move |req: Request<Body>| {
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
// Get User Agent Header and send it back in the response
let auth = req
.headers()
@ -29,16 +32,19 @@ async fn test_client_with_netrc_credentials() -> Result<()> {
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_default(); // Empty Default
future::ok::<_, hyper::Error>(Response::new(Body::from(auth)))
future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(auth))))
});
// Start Hyper Server
// Start Server (not wrapped in loop {} since we want a single response server)
// If you want server to accept multiple connections, wrap it in loop {}
let (socket, _) = listener.accept().await.unwrap();
Http::new()
.http1_keep_alive(false)
.serve_connection(socket, svc)
.with_upgrades()
.await
.expect("Server Started");
let socket = TokioIo::new(socket);
tokio::task::spawn(async move {
http1::Builder::new()
.serve_connection(socket, svc)
.with_upgrades()
.await
.expect("Server Started");
});
});
// Create a netrc file

View file

@ -1,12 +1,16 @@
use anyhow::Result;
use futures::future;
use http_body_util::Full;
use hyper::body::Bytes;
use hyper::header::USER_AGENT;
use hyper::server::conn::Http;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Body, Request, Response};
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use pep508_rs::{MarkerEnvironment, StringVersion};
use platform_tags::{Arch, Os, Platform};
use tokio::net::TcpListener;
use uv_cache::Cache;
use uv_client::LineHaul;
use uv_client::RegistryClientBuilder;
@ -19,8 +23,8 @@ async fn test_user_agent_has_version() -> Result<()> {
let addr = listener.local_addr()?;
// Spawn the server loop in a background task
tokio::spawn(async move {
let svc = service_fn(move |req: Request<Body>| {
let server_task = tokio::spawn(async move {
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
// Get User Agent Header and send it back in the response
let user_agent = req
.headers()
@ -28,16 +32,19 @@ async fn test_user_agent_has_version() -> Result<()> {
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_default(); // Empty Default
future::ok::<_, hyper::Error>(Response::new(Body::from(user_agent)))
future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(user_agent))))
});
// Start Hyper Server
// Start Server (not wrapped in loop {} since we want a single response server)
// If you want server to accept multiple connections, wrap it in loop {}
let (socket, _) = listener.accept().await.unwrap();
Http::new()
.http1_keep_alive(false)
.serve_connection(socket, svc)
.with_upgrades()
.await
.expect("Server Started");
let socket = TokioIo::new(socket);
tokio::task::spawn(async move {
http1::Builder::new()
.serve_connection(socket, svc)
.with_upgrades()
.await
.expect("Server Started");
});
});
// Initialize uv-client
@ -46,7 +53,8 @@ async fn test_user_agent_has_version() -> Result<()> {
// Send request to our dummy server
let res = client
.uncached_client()
.cached_client()
.uncached()
.get(format!("http://{addr}"))
.send()
.await?;
@ -60,6 +68,9 @@ async fn test_user_agent_has_version() -> Result<()> {
// Verify body matches regex
assert_eq!(body, format!("uv/{}", version()));
// Wait for the server task to complete, to be a good citizen.
server_task.await?;
Ok(())
}
@ -70,8 +81,8 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
let addr = listener.local_addr()?;
// Spawn the server loop in a background task
tokio::spawn(async move {
let svc = service_fn(move |req: Request<Body>| {
let server_task = tokio::spawn(async move {
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
// Get User Agent Header and send it back in the response
let user_agent = req
.headers()
@ -79,16 +90,19 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_default(); // Empty Default
future::ok::<_, hyper::Error>(Response::new(Body::from(user_agent)))
future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(user_agent))))
});
// Start Hyper Server
// Start Server (not wrapped in loop {} since we want a single response server)
// If you want server to accept multiple connections, wrap it in loop {}
let (socket, _) = listener.accept().await.unwrap();
Http::new()
.http1_keep_alive(false)
.serve_connection(socket, svc)
.with_upgrades()
.await
.expect("Server Started");
let socket = TokioIo::new(socket);
tokio::task::spawn(async move {
http1::Builder::new()
.serve_connection(socket, svc)
.with_upgrades()
.await
.expect("Server Started");
});
});
// Add some representative markers for an Ubuntu CI runner
@ -142,7 +156,8 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
// Send request to our dummy server
let res = client
.uncached_client()
.cached_client()
.uncached()
.get(format!("http://{addr}"))
.send()
.await?;
@ -153,6 +168,9 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
// Check User Agent
let body = res.text().await?;
// Wait for the server task to complete, to be a good citizen.
server_task.await?;
// Unpack User-Agent with linehaul
let (uv_version, uv_linehaul) = body
.split_once(' ')

View file

@ -79,7 +79,7 @@ indoc = { version = "2.0.4" }
insta = { version = "1.36.1", features = ["filters", "json"] }
predicates = { version = "3.0.4" }
regex = { version = "1.10.3" }
reqwest = { version = "0.11.23", features = ["blocking"], default-features = false }
reqwest = { workspace = true, features = ["blocking"], default-features = false }
[features]
default = ["flate2/zlib-ng", "python", "pypi", "git", "maturin", "python-patch"]