From a824468c8b4055be54747e201a12f8f33e7db12d Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 27 Jun 2025 16:41:14 +0200 Subject: [PATCH] Respect URL-encoded credentials in redirect location (#14315) uv currently ignores URL-encoded credentials in a redirect location. This PR adds a check for these credentials to the redirect handling logic. If found, they are moved to the Authorization header in the redirect request. Closes #11097 --- crates/uv-client/src/base_client.rs | 11 +++++ crates/uv/tests/it/edit.rs | 63 ++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 85c384b0d..d62621863 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -25,6 +25,7 @@ use tracing::{debug, trace}; use url::ParseError; use url::Url; +use uv_auth::Credentials; use uv_auth::{AuthMiddleware, Indexes}; use uv_configuration::{KeyringProviderType, TrustedHost}; use uv_fs::Simplified; @@ -725,6 +726,16 @@ fn request_into_redirect( } } + // Check if there are credentials on the redirect location itself. + // If so, move them to Authorization header. + if !redirect_url.username().is_empty() { + if let Some(credentials) = Credentials::from_url(&redirect_url) { + let _ = redirect_url.set_username(""); + let _ = redirect_url.set_password(None); + headers.insert(AUTHORIZATION, credentials.to_header_value()); + } + } + std::mem::swap(req.headers_mut(), &mut headers); *req.url_mut() = Url::from(redirect_url); debug!( diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 2f9c91e84..3c26ab342 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -12018,6 +12018,61 @@ async fn add_redirect_cross_origin() -> Result<()> { Ok(()) } +/// If uv receives a 302 redirect to a cross-origin server with credentials +/// in the location, use those credentials for the redirect request. +#[tokio::test] +async fn add_redirect_cross_origin_credentials_in_location() -> Result<()> { + let context = TestContext::new("3.12"); + let filters = context + .filters() + .into_iter() + .chain([(r"127\.0\.0\.1:\d*", "[LOCALHOST]")]) + .collect::>(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = [] + "# + })?; + + let redirect_server = MockServer::start().await; + + Mock::given(method("GET")) + .respond_with(|req: &wiremock::Request| { + // Responds with credentials in the location + let redirect_url = redirect_url_to_base( + req, + "https://public:heron@pypi-proxy.fly.dev/basic-auth/simple/", + ); + ResponseTemplate::new(302).insert_header("Location", &redirect_url) + }) + .mount(&redirect_server) + .await; + + let redirect_url = Url::parse(&redirect_server.uri())?; + + uv_snapshot!(filters, context.add().arg("--default-index").arg(redirect_url.as_str()).arg("anyio"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + " + ); + + Ok(()) +} + /// uv currently fails to look up keyring credentials on a cross-origin redirect. #[tokio::test] async fn add_redirect_with_keyring_cross_origin() -> Result<()> { @@ -12145,14 +12200,18 @@ async fn pip_install_redirect_with_netrc_cross_origin() -> Result<()> { } fn redirect_url_to_pypi_proxy(req: &wiremock::Request) -> String { + redirect_url_to_base(req, "https://pypi-proxy.fly.dev/basic-auth/simple/") +} + +fn redirect_url_to_base(req: &wiremock::Request, base: &str) -> String { let last_path_segment = req .url .path_segments() .expect("path has segments") - .filter(|segment| !segment.is_empty()) // Filter out empty segments + .filter(|segment| !segment.is_empty()) .next_back() .expect("path has a package segment"); - format!("https://pypi-proxy.fly.dev/basic-auth/simple/{last_path_segment}/") + format!("{base}{last_path_segment}/") } /// Test the error message when adding a package with multiple existing references in