mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 12:24:15 +00:00
parent
d8323551f8
commit
a01143980a
12 changed files with 312 additions and 450 deletions
511
Cargo.lock
generated
511
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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