Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions crates/pixi/tests/integration_rust/pypi_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,108 @@ async fn test_allow_insecure_host() {
pixi.update_lock_file().await.unwrap();
}

#[tokio::test]
#[cfg_attr(not(feature = "slow_integration_tests"), ignore)]
async fn test_tls_no_verify_with_pypi_dependencies() {
let pixi = PixiControl::from_manifest(&format!(
r#"
[project]
name = "pypi-tls-test"
platforms = ["{platform}"]
channels = ["https://prefix.dev/conda-forge"]

[dependencies]
python = "~=3.12.0"

[pypi-dependencies]
sh = "*"

[pypi-options]
extra-index-urls = ["https://expired.badssl.com/"]"#,
platform = Platform::current(),
))
.unwrap();

// First verify that it fails with SSL errors when tls-no-verify is not set
assert!(
pixi.update_lock_file().await.is_err(),
"should fail with SSL error when tls-no-verify is not enabled"
);

// Now set tls-no-verify = true in the project config
let config_path = pixi.workspace().unwrap().pixi_dir().join("config.toml");
fs_err::create_dir_all(config_path.parent().unwrap()).unwrap();
let mut file = File::create(config_path).unwrap();
file.write_all(
r#"
tls-no-verify = true"#
.as_bytes(),
)
.unwrap();

// With tls-no-verify = true, this should now succeed or fail for non-SSL reasons
let result = pixi.update_lock_file().await;

// The test should succeed because tls-no-verify bypasses SSL verification
// If it fails, it should not be due to SSL certificate issues
match result {
Ok(_) => {
// Success - TLS verification was bypassed
}
Err(e) => {
let error_msg = format!("{e:?}");
// If it fails, it should NOT be due to SSL/TLS certificate issues
assert!(
!error_msg.to_lowercase().contains("certificate")
&& !error_msg.to_lowercase().contains("ssl")
&& !error_msg.to_lowercase().contains("tls"),
"Error should not be SSL/TLS related when tls-no-verify is enabled. Got: {error_msg}"
);
}
}
}

#[tokio::test]
#[cfg_attr(not(feature = "slow_integration_tests"), ignore)]
async fn test_tls_verify_still_fails_without_config() {
let pixi = PixiControl::from_manifest(&format!(
r#"
[project]
name = "pypi-tls-verify-test"
platforms = ["{platform}"]
channels = ["https://prefix.dev/conda-forge"]

[dependencies]
python = "~=3.12.0"

[pypi-dependencies]
sh = "*"

[pypi-options]
extra-index-urls = ["https://expired.badssl.com/"]"#,
platform = Platform::current(),
))
.unwrap();

// Without tls-no-verify, this should fail with SSL errors
let result = pixi.update_lock_file().await;
assert!(
result.is_err(),
"should fail with SSL error when tls-no-verify is not enabled"
);

let error = result.unwrap_err();
let error_msg = format!("{error:?}");
// The error should be SSL/TLS related
assert!(
error_msg.to_lowercase().contains("certificate")
|| error_msg.to_lowercase().contains("ssl")
|| error_msg.to_lowercase().contains("tls")
|| error_msg.contains("expired.badssl.com"),
"Error should be SSL/TLS related. Got: {error_msg}"
);
}

#[tokio::test]
#[cfg_attr(not(feature = "slow_integration_tests"), ignore)]
async fn test_indexes_are_passed_when_solving_build_pypi_dependencies() {
Expand Down
68 changes: 63 additions & 5 deletions crates/pixi_core/src/lock_file/resolve/pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ use pixi_pypi_spec::PixiPypiSpec;
use pixi_record::{LockedGitUrl, PixiRecord};
use pixi_reporters::{UvReporter, UvReporterOptions};
use pixi_uv_conversions::{
ConversionError, as_uv_req, convert_uv_requirements_to_pep508, into_pinned_git_spec,
pypi_options_to_build_options, pypi_options_to_index_locations, to_exclude_newer,
to_index_strategy, to_normalize, to_requirements, to_uv_normalize, to_uv_version,
to_version_specifiers,
ConversionError, as_uv_req, configure_insecure_hosts_for_tls_bypass,
convert_uv_requirements_to_pep508, into_pinned_git_spec, pypi_options_to_build_options,
pypi_options_to_index_locations, to_exclude_newer, to_index_strategy, to_normalize,
to_requirements, to_uv_normalize, to_uv_version, to_version_specifiers,
};
use pypi_modifiers::{
pypi_marker_env::determine_marker_environment,
Expand Down Expand Up @@ -408,8 +408,15 @@ pub async fn resolve_pypi(
// TODO: create a cached registry client per index_url set?
let index_strategy = to_index_strategy(pypi_options.index_strategy.as_ref());

// Configure insecure hosts for TLS verification bypass
let allow_insecure_hosts = configure_insecure_hosts_for_tls_bypass(
context.allow_insecure_host.clone(),
context.tls_no_verify,
&index_locations,
);

let base_client_builder = BaseClientBuilder::default()
.allow_insecure_host(context.allow_insecure_host.clone())
.allow_insecure_host(allow_insecure_hosts)
.markers(&marker_environment)
.keyring(context.keyring_provider)
.connectivity(Connectivity::Online)
Expand Down Expand Up @@ -1195,6 +1202,57 @@ mod tests {
assert_eq!(path.as_str(), "./b/c");
}

#[test]
fn test_tls_no_verify_host_conversion() {
use pixi_uv_conversions::to_uv_trusted_host;
// Test the logic for converting hosts to trusted hosts when tls_no_verify is enabled
let test_hosts = vec![
"pypi.org",
"test-index.example.com",
"another-index.example.org",
];

let mut allow_insecure_hosts = vec![];
let tls_no_verify = true;

if tls_no_verify {
for host in &test_hosts {
if let Ok(trusted_host) = to_uv_trusted_host(host) {
allow_insecure_hosts.push(trusted_host);
}
}
}

assert_eq!(allow_insecure_hosts.len(), 3);

let host_names: Vec<String> = allow_insecure_hosts.iter().map(|h| h.to_string()).collect();

assert!(host_names.contains(&"pypi.org".to_string()));
assert!(host_names.contains(&"test-index.example.com".to_string()));
assert!(host_names.contains(&"another-index.example.org".to_string()));
}

#[test]
fn test_tls_verify_enabled_preserves_empty_list() {
use pixi_uv_conversions::to_uv_trusted_host;
// Test that when tls_no_verify is false, no hosts are added
let test_hosts = vec!["pypi.org", "test-index.example.com"];

let mut allow_insecure_hosts = vec![];
let tls_no_verify = false;

if tls_no_verify {
// This should not execute
for host in &test_hosts {
if let Ok(trusted_host) = to_uv_trusted_host(host) {
allow_insecure_hosts.push(trusted_host);
}
}
}

assert_eq!(allow_insecure_hosts.len(), 0);
}

// In this case we want to make the path relative to the project_root or lock
// file path
#[cfg(target_os = "windows")]
Expand Down
12 changes: 9 additions & 3 deletions crates/pixi_install_pypi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ use pixi_reporters::{UvReporter, UvReporterOptions};
use pixi_utils::prefix::Prefix;
use pixi_uv_context::UvResolutionContext;
use pixi_uv_conversions::{
BuildIsolation, locked_indexes_to_index_locations, pypi_options_to_build_options,
to_exclude_newer, to_index_strategy,
BuildIsolation, configure_insecure_hosts_for_tls_bypass, locked_indexes_to_index_locations,
pypi_options_to_build_options, to_exclude_newer, to_index_strategy,
};
use plan::{InstallPlanner, InstallReason, NeedReinstall, PyPIInstallationPlan};
use pypi_modifiers::{
Expand Down Expand Up @@ -341,8 +341,14 @@ impl<'a> PyPIEnvironmentUpdater<'a> {

let index_strategy = to_index_strategy(self.build_config.index_strategy);

let allow_insecure_hosts = configure_insecure_hosts_for_tls_bypass(
self.context_config.uv_context.allow_insecure_host.clone(),
self.context_config.uv_context.tls_no_verify,
&index_locations,
);

let base_client_builder = BaseClientBuilder::default()
.allow_insecure_host(self.context_config.uv_context.allow_insecure_host.clone())
.allow_insecure_host(allow_insecure_hosts)
.keyring(self.context_config.uv_context.keyring_provider)
.connectivity(Connectivity::Online)
.extra_middleware(self.context_config.uv_context.extra_middleware.clone());
Expand Down
2 changes: 2 additions & 0 deletions crates/pixi_uv_context/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct UvResolutionContext {
pub shared_state: SharedState,
pub extra_middleware: ExtraMiddleware,
pub proxies: Vec<reqwest::Proxy>,
pub tls_no_verify: bool,
pub package_config_settings: PackageConfigSettings,
pub extra_build_requires: ExtraBuildRequires,
pub extra_build_variables: ExtraBuildVariables,
Expand Down Expand Up @@ -85,6 +86,7 @@ impl UvResolutionContext {
shared_state: SharedState::default(),
extra_middleware: ExtraMiddleware(uv_middlewares(config)),
proxies: config.get_proxies().into_diagnostic()?,
tls_no_verify: config.tls_no_verify(),
package_config_settings: PackageConfigSettings::default(),
extra_build_requires: ExtraBuildRequires::default(),
extra_build_variables: ExtraBuildVariables::default(),
Expand Down
Loading
Loading