Support for wildcard in UV_INSECURE_HOST (#8052)

Allow '*' as a value to match all hosts, and provide
`reqwest_blocking_get` for uv tests, so that they also respect
UV_INSECURE_HOST (since they respect `ALL_PROXY`).

This lets those tests pass with a forward proxy - we can think about
setting a root certificate later so that we don't need to disable
certificate verification at all.

---

I tested this locally by running:

```bash
GIT_SSL_NO_VERIFY=true ALL_PROXY=localhost:8080 UV_INSECURE_HOST="*" cargo nextest run sync_wheel_path_source_error
```

With my forward proxy showing:

```
2024-10-09T18:20:16.300188Z  INFO fopro: Proxied GET cc2fedbd88/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl (headers 480.024958ms + body 92.345666ms)
2024-10-09T18:20:16.913298Z  INFO fopro: Proxied GET https://pypi.org/simple/pycparser/ (headers 509.664834ms + body 269.291µs)
2024-10-09T18:20:17.383975Z  INFO fopro: Proxied GET 5f610ebe42/pycparser-2.21-py2.py3-none-any.whl.metadata (headers 443.184208ms + body 2.094792ms)
```
This commit is contained in:
Amos Wenger 2024-10-12 14:55:26 +02:00 committed by GitHub
parent 9351652e32
commit a3b11dacb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 137 additions and 81 deletions

View file

@ -2,35 +2,40 @@ 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 {
pub enum TrustedHost {
Wildcard,
Host {
scheme: Option<String>,
host: String,
port: Option<u16>,
},
}
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())
{
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)) {
if port.is_some_and(|port| url.port() != Some(port)) {
return false;
}
if Some(self.host.as_ref()) != url.host_str() {
if Some(host.as_str()) != url.host_str() {
return false;
}
true
}
}
}
}
impl<'de> Deserialize<'de> for TrustedHost {
@ -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::<Inner>().map(|inner| TrustedHost {
map.deserialize::<Inner>().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<Self, Self::Err> {
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,21 +114,28 @@ 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)?;
match self {
TrustedHost::Wildcard => {
write!(f, "*")?;
}
TrustedHost::Host { scheme, host, port } => {
if let Some(scheme) = &scheme {
write!(f, "{scheme}://{host}")?;
} else {
write!(f, "{}", self.host)?;
write!(f, "{host}")?;
}
if let Some(port) = self.port {
if let Some(port) = port {
write!(f, ":{port}")?;
}
}
}
Ok(())
}

View file

@ -1,8 +1,13 @@
#[test]
fn parse() {
assert_eq!(
"*".parse::<super::TrustedHost>().unwrap(),
super::TrustedHost::Wildcard
);
assert_eq!(
"example.com".parse::<super::TrustedHost>().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::<super::TrustedHost>().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::<super::TrustedHost>().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::<super::TrustedHost>()
.unwrap(),
super::TrustedHost {
super::TrustedHost::Host {
scheme: Some("https".to_string()),
host: "example.com".to_string(),
port: None

View file

@ -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

View file

@ -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)?;

View file

@ -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}")?;

View file

@ -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();

View file

@ -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,

View file

@ -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(