diff --git a/crates/uv-configuration/src/trusted_host.rs b/crates/uv-configuration/src/trusted_host.rs index 697f7afbf..326ef8602 100644 --- a/crates/uv-configuration/src/trusted_host.rs +++ b/crates/uv-configuration/src/trusted_host.rs @@ -2,34 +2,39 @@ use serde::{Deserialize, Deserializer}; use std::str::FromStr; use url::Url; -/// A trusted host, which could be a host or a host-port pair. +/// A host specification (wildcard, or host, with optional scheme and/or port) for which +/// certificates are not verified when making HTTPS requests. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct TrustedHost { - scheme: Option, - host: String, - port: Option, +pub enum TrustedHost { + Wildcard, + Host { + scheme: Option, + host: String, + port: Option, + }, } impl TrustedHost { /// Returns `true` if the [`Url`] matches this trusted host. pub fn matches(&self, url: &Url) -> bool { - if self - .scheme - .as_ref() - .is_some_and(|scheme| scheme != url.scheme()) - { - return false; - } + match self { + TrustedHost::Wildcard => true, + TrustedHost::Host { scheme, host, port } => { + if scheme.as_ref().is_some_and(|scheme| scheme != url.scheme()) { + return false; + } - if self.port.is_some_and(|port| url.port() != Some(port)) { - return false; - } + if port.is_some_and(|port| url.port() != Some(port)) { + return false; + } - if Some(self.host.as_ref()) != url.host_str() { - return false; - } + if Some(host.as_str()) != url.host_str() { + return false; + } - true + true + } + } } } @@ -48,7 +53,7 @@ impl<'de> Deserialize<'de> for TrustedHost { serde_untagged::UntaggedEnumVisitor::new() .string(|string| TrustedHost::from_str(string).map_err(serde::de::Error::custom)) .map(|map| { - map.deserialize::().map(|inner| TrustedHost { + map.deserialize::().map(|inner| TrustedHost::Host { scheme: inner.scheme, host: inner.host, port: inner.port, @@ -80,6 +85,10 @@ impl std::str::FromStr for TrustedHost { type Err = TrustedHostError; fn from_str(s: &str) -> Result { + if s == "*" { + return Ok(Self::Wildcard); + } + // Detect scheme. let (scheme, s) = if let Some(s) = s.strip_prefix("https://") { (Some("https".to_string()), s) @@ -105,20 +114,27 @@ impl std::str::FromStr for TrustedHost { .transpose() .map_err(|_| TrustedHostError::InvalidPort(s.to_string()))?; - Ok(Self { scheme, host, port }) + Ok(Self::Host { scheme, host, port }) } } impl std::fmt::Display for TrustedHost { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - if let Some(scheme) = &self.scheme { - write!(f, "{}://{}", scheme, self.host)?; - } else { - write!(f, "{}", self.host)?; - } + match self { + TrustedHost::Wildcard => { + write!(f, "*")?; + } + TrustedHost::Host { scheme, host, port } => { + if let Some(scheme) = &scheme { + write!(f, "{scheme}://{host}")?; + } else { + write!(f, "{host}")?; + } - if let Some(port) = self.port { - write!(f, ":{port}")?; + if let Some(port) = port { + write!(f, ":{port}")?; + } + } } Ok(()) diff --git a/crates/uv-configuration/src/trusted_host/tests.rs b/crates/uv-configuration/src/trusted_host/tests.rs index b24a254da..6eef745a9 100644 --- a/crates/uv-configuration/src/trusted_host/tests.rs +++ b/crates/uv-configuration/src/trusted_host/tests.rs @@ -1,8 +1,13 @@ #[test] fn parse() { + assert_eq!( + "*".parse::().unwrap(), + super::TrustedHost::Wildcard + ); + assert_eq!( "example.com".parse::().unwrap(), - super::TrustedHost { + super::TrustedHost::Host { scheme: None, host: "example.com".to_string(), port: None @@ -11,7 +16,7 @@ fn parse() { assert_eq!( "example.com:8080".parse::().unwrap(), - super::TrustedHost { + super::TrustedHost::Host { scheme: None, host: "example.com".to_string(), port: Some(8080) @@ -20,7 +25,7 @@ fn parse() { assert_eq!( "https://example.com".parse::().unwrap(), - super::TrustedHost { + super::TrustedHost::Host { scheme: Some("https".to_string()), host: "example.com".to_string(), port: None @@ -31,7 +36,7 @@ fn parse() { "https://example.com/hello/world" .parse::() .unwrap(), - super::TrustedHost { + super::TrustedHost::Host { scheme: Some("https".to_string()), host: "example.com".to_string(), port: None diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 75a327bea..db49421c1 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -14,11 +14,13 @@ use assert_fs::assert::PathAssert; use assert_fs::fixture::{ChildPath, PathChild, PathCopy, PathCreateDir, SymlinkToFile}; use base64::{prelude::BASE64_STANDARD as base64, Engine}; use etcetera::BaseStrategy; +use futures::StreamExt; use indoc::formatdoc; use itertools::Itertools; use predicates::prelude::predicate; use regex::Regex; +use tokio::io::AsyncWriteExt; use uv_cache::Cache; use uv_fs::Simplified; use uv_python::managed::ManagedPythonInstallations; @@ -1279,6 +1281,31 @@ pub fn decode_token(content: &[&str]) -> String { token } +/// Simulates `reqwest::blocking::get` but returns bytes directly, and disables +/// certificate verification, passing through the `BaseClient` +#[tokio::main(flavor = "current_thread")] +pub async fn download_to_disk(url: &str, path: &Path) { + let trusted_hosts: Vec<_> = std::env::var("UV_INSECURE_HOST") + .unwrap_or_default() + .split(' ') + .map(|h| uv_configuration::TrustedHost::from_str(h).unwrap()) + .collect(); + + let client = uv_client::BaseClientBuilder::new() + .allow_insecure_host(trusted_hosts) + .build(); + let url: reqwest::Url = url.parse().unwrap(); + let client = client.for_host(&url); + let response = client.request(http::Method::GET, url).send().await.unwrap(); + + let mut file = tokio::fs::File::create(path).await.unwrap(); + let mut stream = response.bytes_stream(); + while let Some(chunk) = stream.next().await { + file.write_all(&chunk.unwrap()).await.unwrap(); + } + file.sync_all().await.unwrap(); +} + /// Utility macro to return the name of the current function. /// /// https://stackoverflow.com/a/40234666/3549270 diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index a61fc9938..93b583818 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -7,7 +7,8 @@ use std::io::BufReader; use url::Url; use crate::common::{ - self, build_vendor_links_url, decode_token, packse_index_url, uv_snapshot, TestContext, + self, build_vendor_links_url, decode_token, download_to_disk, packse_index_url, uv_snapshot, + TestContext, }; use uv_fs::Simplified; @@ -7967,11 +7968,11 @@ fn lock_sources_archive() -> Result<()> { let context = TestContext::new("3.12"); // Download the source. - let response = - reqwest::blocking::get("https://github.com/user-attachments/files/16592193/workspace.zip")?; let workspace_archive = context.temp_dir.child("workspace.zip"); - let mut workspace_archive_file = fs_err::File::create(&*workspace_archive)?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut workspace_archive_file)?; + download_to_disk( + "https://github.com/user-attachments/files/16592193/workspace.zip", + &workspace_archive, + ); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(&formatdoc! { @@ -8106,11 +8107,11 @@ fn lock_sources_source_tree() -> Result<()> { let context = TestContext::new("3.12"); // Download the source. - let response = - reqwest::blocking::get("https://github.com/user-attachments/files/16592193/workspace.zip")?; let workspace_archive = context.temp_dir.child("workspace.zip"); - let mut workspace_archive_file = fs_err::File::create(&*workspace_archive)?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut workspace_archive_file)?; + download_to_disk( + "https://github.com/user-attachments/files/16592193/workspace.zip", + &workspace_archive, + ); // Unzip the file. let file = fs_err::File::open(&*workspace_archive)?; diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 3bff95204..5e0d26526 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -8,9 +8,9 @@ use anyhow::{bail, Context, Result}; use assert_fs::prelude::*; use indoc::indoc; use url::Url; -use uv_fs::Simplified; -use crate::common::{uv_snapshot, TestContext}; +use crate::common::{download_to_disk, uv_snapshot, TestContext}; +use uv_fs::Simplified; #[test] fn compile_requirements_in() -> Result<()> { @@ -2592,10 +2592,11 @@ fn compile_wheel_path_dependency() -> Result<()> { let context = TestContext::new("3.12"); // Download a wheel. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl")?; let flask_wheel = context.temp_dir.child("flask-3.0.0-py3-none-any.whl"); - let mut flask_wheel_file = fs::File::create(&flask_wheel)?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut flask_wheel_file)?; + download_to_disk( + "https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl", + &flask_wheel, + ); let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str(&format!( @@ -2842,10 +2843,11 @@ fn compile_wheel_path_dependency() -> Result<()> { fn compile_source_distribution_path_dependency() -> Result<()> { let context = TestContext::new("3.12"); // Download a source distribution. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/d8/09/c1a7354d3925a3c6c8cfdebf4245bae67d633ffda1ba415add06ffc839c5/flask-3.0.0.tar.gz")?; let flask_wheel = context.temp_dir.child("flask-3.0.0.tar.gz"); - let mut flask_wheel_file = std::fs::File::create(&flask_wheel)?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut flask_wheel_file)?; + download_to_disk( + "https://files.pythonhosted.org/packages/d8/09/c1a7354d3925a3c6c8cfdebf4245bae67d633ffda1ba415add06ffc839c5/flask-3.0.0.tar.gz", + &flask_wheel, + ); let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str(&format!( @@ -3517,10 +3519,11 @@ fn preserve_url() -> Result<()> { fn preserve_project_root() -> Result<()> { let context = TestContext::new("3.12"); // Download a wheel. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl")?; let flask_wheel = context.temp_dir.child("flask-3.0.0-py3-none-any.whl"); - let mut flask_wheel_file = std::fs::File::create(flask_wheel)?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut flask_wheel_file)?; + download_to_disk( + "https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl", + &flask_wheel, + ); let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str("flask @ file://${PROJECT_ROOT}/flask-3.0.0-py3-none-any.whl")?; @@ -3670,10 +3673,11 @@ fn error_missing_unnamed_env_var() -> Result<()> { fn respect_file_env_var() -> Result<()> { let context = TestContext::new("3.12"); // Download a wheel. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl")?; let flask_wheel = context.temp_dir.child("flask-3.0.0-py3-none-any.whl"); - let mut flask_wheel_file = std::fs::File::create(flask_wheel)?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut flask_wheel_file)?; + download_to_disk( + "https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl", + &flask_wheel, + ); let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str("flask @ ${FILE_PATH}")?; diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 8fec7ab0d..234a9a143 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -12,7 +12,8 @@ use predicates::Predicate; use url::Url; use crate::common::{ - copy_dir_all, site_packages_path, uv_snapshot, venv_to_interpreter, TestContext, + copy_dir_all, download_to_disk, site_packages_path, uv_snapshot, venv_to_interpreter, + TestContext, }; use uv_fs::Simplified; @@ -1069,10 +1070,8 @@ fn install_local_wheel() -> Result<()> { let context = TestContext::new("3.12"); // Download a wheel. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl")?; let archive = context.temp_dir.child("tomli-2.0.1-py3-none-any.whl"); - let mut archive_file = fs_err::File::create(archive.path())?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; + download_to_disk("https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", &archive); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str(&format!( @@ -1208,10 +1207,8 @@ fn mismatched_version() -> Result<()> { let context = TestContext::new("3.12"); // Download a wheel. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl")?; let archive = context.temp_dir.child("tomli-3.7.2-py3-none-any.whl"); - let mut archive_file = fs_err::File::create(archive.path())?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; + download_to_disk("https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", &archive); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str(&format!( @@ -1243,10 +1240,11 @@ fn mismatched_name() -> Result<()> { let context = TestContext::new("3.12"); // Download a wheel. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl")?; let archive = context.temp_dir.child("foo-2.0.1-py3-none-any.whl"); - let mut archive_file = fs_err::File::create(archive.path())?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; + download_to_disk( + "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", + &archive, + ); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str(&format!( @@ -1279,10 +1277,11 @@ fn install_local_source_distribution() -> Result<()> { let context = TestContext::new("3.12"); // Download a source distribution. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/b0/b4/bc2baae3970c282fae6c2cb8e0f179923dceb7eaffb0e76170628f9af97b/wheel-0.42.0.tar.gz")?; let archive = context.temp_dir.child("wheel-0.42.0.tar.gz"); - let mut archive_file = fs_err::File::create(archive.path())?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; + download_to_disk( + "https://files.pythonhosted.org/packages/b0/b4/bc2baae3970c282fae6c2cb8e0f179923dceb7eaffb0e76170628f9af97b/wheel-0.42.0.tar.gz", + &archive, + ); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str(&format!( @@ -1639,10 +1638,11 @@ fn install_path_source_dist_cached() -> Result<()> { let context = TestContext::new("3.12"); // Download a source distribution. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz")?; let archive = context.temp_dir.child("source_distribution-0.0.1.tar.gz"); - let mut archive_file = fs_err::File::create(archive.path())?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; + download_to_disk( + "https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz", + &archive, + ); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str(&format!( @@ -1734,10 +1734,11 @@ fn install_path_built_dist_cached() -> Result<()> { let context = TestContext::new("3.12"); // Download a wheel. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl")?; let archive = context.temp_dir.child("tomli-2.0.1-py3-none-any.whl"); - let mut archive_file = fs_err::File::create(archive.path())?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; + download_to_disk( + "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", + &archive, + ); let requirements_txt = context.temp_dir.child("requirements.txt"); let url = Url::from_file_path(archive.path()).unwrap(); diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 376b9f8bc..32c6a07f1 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -3552,12 +3552,12 @@ fn allow_insecure_host() -> anyhow::Result<()> { index_strategy: FirstIndex, keyring_provider: Disabled, allow_insecure_host: [ - TrustedHost { + Host { scheme: None, host: "google.com", port: None, }, - TrustedHost { + Host { scheme: None, host: "example.com", port: None, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index c117fd8d7..b7d834c7e 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -2,10 +2,11 @@ use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::{fixture::ChildPath, prelude::*}; use insta::assert_snapshot; + +use predicates::prelude::predicate; use tempfile::tempdir_in; -use crate::common::{uv_snapshot, venv_bin_path, TestContext}; -use predicates::prelude::predicate; +use crate::common::{download_to_disk, uv_snapshot, venv_bin_path, TestContext}; #[test] fn sync() -> Result<()> { @@ -2309,12 +2310,13 @@ fn sync_wheel_path_source_error() -> Result<()> { let context = TestContext::new("3.12"); // Download a wheel. - let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl")?; let archive = context .temp_dir .child("cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl"); - let mut archive_file = fs_err::File::create(archive.path())?; - std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; + download_to_disk( + "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", + &archive, + ); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(