mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 05:03:46 +00:00
parent
d8323551f8
commit
a01143980a
12 changed files with 312 additions and 450 deletions
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,4 +20,3 @@ mod middleware;
|
|||
mod registry_client;
|
||||
mod remote_metadata;
|
||||
mod rkyvutil;
|
||||
mod tls;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(' ')
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue