diff --git a/Cargo.lock b/Cargo.lock index d6cb27480..b92921242 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4664,6 +4664,7 @@ dependencies = [ "uv-publish", "uv-pypi-types", "uv-python", + "uv-redacted", "uv-requirements", "uv-requirements-txt", "uv-resolver", @@ -4950,6 +4951,7 @@ dependencies = [ "uv-pep508", "uv-platform-tags", "uv-pypi-types", + "uv-redacted", "uv-small-str", "uv-static", "uv-torch", @@ -5250,6 +5252,7 @@ dependencies = [ "uv-cache-key", "uv-fs", "uv-git-types", + "uv-redacted", "uv-static", "uv-version", "which", @@ -5263,6 +5266,7 @@ dependencies = [ "thiserror 2.0.12", "tracing", "url", + "uv-redacted", ] [[package]] @@ -5597,6 +5601,13 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "uv-redacted" +version = "0.0.1" +dependencies = [ + "url", +] + [[package]] name = "uv-requirements" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c430f5b87..09d782523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ uv-platform-tags = { path = "crates/uv-platform-tags" } uv-publish = { path = "crates/uv-publish" } uv-pypi-types = { path = "crates/uv-pypi-types" } uv-python = { path = "crates/uv-python" } +uv-redacted = { path = "crates/uv-redacted" } uv-requirements = { path = "crates/uv-requirements" } uv-requirements-txt = { path = "crates/uv-requirements-txt" } uv-resolver = { path = "crates/uv-resolver" } diff --git a/crates/uv-auth/src/cache.rs b/crates/uv-auth/src/cache.rs index 636ec4ec4..d61a6b880 100644 --- a/crates/uv-auth/src/cache.rs +++ b/crates/uv-auth/src/cache.rs @@ -259,6 +259,8 @@ impl From<(Realm, Username)> for RealmUsername { #[cfg(test)] mod tests { + use crate::credentials::Password; + use super::*; #[test] @@ -331,4 +333,23 @@ mod tests { let url = Url::parse("https://example.com/foobar").unwrap(); assert_eq!(trie.get(&url), None); } + + #[test] + fn test_url_with_credentials() { + let username = Username::new(Some(String::from("username"))); + let password = Password::new(String::from("password")); + let credentials = Arc::new(Credentials::Basic { + username: username.clone(), + password: Some(password), + }); + let cache = CredentialsCache::default(); + // Insert with URL with credentials and get with redacted URL. + let url = Url::parse("https://username:password@example.com/foobar").unwrap(); + cache.insert(&url, credentials.clone()); + assert_eq!(cache.get_url(&url, &username), Some(credentials.clone())); + // Insert with redacted URL and get with URL with credentials. + let url = Url::parse("https://username:password@second-example.com/foobar").unwrap(); + cache.insert(&url, credentials.clone()); + assert_eq!(cache.get_url(&url, &username), Some(credentials.clone())); + } } diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 64088676e..e86ab2084 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -24,6 +24,7 @@ uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-small-str = { workspace = true } +uv-redacted = { workspace = true } uv-static = { workspace = true } uv-torch = { workspace = true } uv-version = { workspace = true } diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index 74111bf5a..ca7faebaa 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -7,6 +7,7 @@ use url::Url; use uv_distribution_filename::{WheelFilename, WheelFilenameError}; use uv_normalize::PackageName; +use uv_redacted::redacted_url; use crate::middleware::OfflineError; use crate::{html, FlatIndexError}; @@ -197,10 +198,10 @@ pub enum ErrorKind { #[error("Failed to fetch: `{0}`")] WrappedReqwestError(Url, #[source] WrappedReqwestError), - #[error("Received some unexpected JSON from {url}")] + #[error("Received some unexpected JSON from {}", redacted_url(url))] BadJson { source: serde_json::Error, url: Url }, - #[error("Received some unexpected HTML from {url}")] + #[error("Received some unexpected HTML from {}", redacted_url(url))] BadHtml { source: html::Error, url: Url }, #[error("Failed to read zip with range requests: `{0}`")] diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index 1a4a4f8f9..93987be41 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -10,6 +10,7 @@ use uv_cache_key::cache_digest; use uv_distribution_filename::DistFilename; use uv_distribution_types::{File, FileLocation, IndexUrl, UrlString}; use uv_pypi_types::HashDigests; +use uv_redacted::redacted_url; use uv_small_str::SmallString; use crate::cached_client::{CacheControl, CachedClientError}; @@ -207,7 +208,7 @@ impl<'a> FlatIndexClient<'a> { Ok(file) => Some(file), Err(err) => { // Ignore files with unparsable version specifiers. - warn!("Skipping file in {url}: {err}"); + warn!("Skipping file in {}: {err}", redacted_url(&url)); None } } diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 8bdf2c065..f46b36fb4 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -31,6 +31,7 @@ use uv_pep440::Version; use uv_pep508::MarkerEnvironment; use uv_platform_tags::Platform; use uv_pypi_types::{ResolutionMetadata, SimpleJson}; +use uv_redacted::redacted_url; use uv_small_str::SmallString; use uv_torch::TorchStrategy; @@ -484,7 +485,10 @@ impl RegistryClient { // ref https://github.com/servo/rust-url/issues/333 .push(""); - trace!("Fetching metadata for {package_name} from {url}"); + trace!( + "Fetching metadata for {package_name} from {}", + redacted_url(&url) + ); let cache_entry = self.cache.entry( CacheBucket::Simple, diff --git a/crates/uv-git-types/Cargo.toml b/crates/uv-git-types/Cargo.toml index 98ef751c9..7158879de 100644 --- a/crates/uv-git-types/Cargo.toml +++ b/crates/uv-git-types/Cargo.toml @@ -20,3 +20,5 @@ serde = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } + +uv-redacted = { workspace = true } diff --git a/crates/uv-git-types/src/lib.rs b/crates/uv-git-types/src/lib.rs index 675ff8be1..f08a85988 100644 --- a/crates/uv-git-types/src/lib.rs +++ b/crates/uv-git-types/src/lib.rs @@ -5,6 +5,8 @@ pub use crate::reference::GitReference; use thiserror::Error; use url::Url; +use uv_redacted::redacted_url; + mod github; mod oid; mod reference; @@ -151,6 +153,6 @@ impl From for Url { impl std::fmt::Display for GitUrl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.repository) + write!(f, "{}", redacted_url(&self.repository)) } } diff --git a/crates/uv-git/Cargo.toml b/crates/uv-git/Cargo.toml index 600a19367..39c90849e 100644 --- a/crates/uv-git/Cargo.toml +++ b/crates/uv-git/Cargo.toml @@ -20,6 +20,7 @@ uv-auth = { workspace = true } uv-cache-key = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-git-types = { workspace = true } +uv-redacted = { workspace = true } uv-static = { workspace = true } uv-version = { workspace = true } diff --git a/crates/uv-git/src/credentials.rs b/crates/uv-git/src/credentials.rs index 1a583f51c..d8a67a250 100644 --- a/crates/uv-git/src/credentials.rs +++ b/crates/uv-git/src/credentials.rs @@ -4,6 +4,7 @@ use tracing::trace; use url::Url; use uv_auth::Credentials; use uv_cache_key::RepositoryUrl; +use uv_redacted::redacted_url; /// Global authentication cache for a uv invocation. /// @@ -31,7 +32,7 @@ impl GitStore { /// Returns `true` if the store was updated. pub fn store_credentials_from_url(url: &Url) -> bool { if let Some(credentials) = Credentials::from_url(url) { - trace!("Caching credentials for {url}"); + trace!("Caching credentials for {}", redacted_url(url)); GIT_STORE.insert(RepositoryUrl::new(url), credentials); true } else { diff --git a/crates/uv-git/src/source.rs b/crates/uv-git/src/source.rs index e76b44045..f9acb2848 100644 --- a/crates/uv-git/src/source.rs +++ b/crates/uv-git/src/source.rs @@ -13,6 +13,7 @@ use url::Url; use uv_cache_key::{cache_digest, RepositoryUrl}; use uv_git_types::GitUrl; +use uv_redacted::redacted_url; use crate::git::GitRemote; use crate::GIT_STORE; @@ -100,7 +101,10 @@ impl GitSource { // situation that we have a locked revision but the database // doesn't have it. (locked_rev, db) => { - debug!("Updating Git source `{}`", self.git.repository()); + debug!( + "Updating Git source `{}`", + redacted_url(self.git.repository()) + ); // Report the checkout operation to the reporter. let task = self.reporter.as_ref().map(|reporter| { diff --git a/crates/uv-redacted/Cargo.toml b/crates/uv-redacted/Cargo.toml new file mode 100644 index 000000000..bf337ad23 --- /dev/null +++ b/crates/uv-redacted/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "uv-redacted" +version = "0.0.1" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +url = { workspace = true } diff --git a/crates/uv-redacted/src/lib.rs b/crates/uv-redacted/src/lib.rs new file mode 100644 index 000000000..36ef5a46f --- /dev/null +++ b/crates/uv-redacted/src/lib.rs @@ -0,0 +1,72 @@ +use std::borrow::Cow; + +use url::Url; + +/// Return a version of the URL with redacted credentials, allowing the generic `git` username (without a password) +/// in SSH URLs, as in, `ssh://git@github.com/...`. +pub fn redacted_url(url: &Url) -> Cow<'_, Url> { + if url.username().is_empty() && url.password().is_none() { + return Cow::Borrowed(url); + } + if url.scheme() == "ssh" && url.username() == "git" && url.password().is_none() { + return Cow::Borrowed(url); + } + + let mut url = url.clone(); + let _ = url.set_username(""); + let _ = url.set_password(None); + Cow::Owned(url) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_url_no_credentials() { + let url = Url::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap(); + let redacted = redacted_url(&url); + assert_eq!(redacted.username(), ""); + assert!(redacted.password().is_none()); + assert_eq!( + format!("{redacted}"), + "https://pypi-proxy.fly.dev/basic-auth/simple" + ); + } + + #[test] + fn from_url_username_and_password() { + let url = Url::parse("https://user:pass@pypi-proxy.fly.dev/basic-auth/simple").unwrap(); + let redacted = redacted_url(&url); + assert_eq!(redacted.username(), ""); + assert!(redacted.password().is_none()); + assert_eq!( + format!("{redacted}"), + "https://pypi-proxy.fly.dev/basic-auth/simple" + ); + } + + #[test] + fn from_url_just_password() { + let url = Url::parse("https://:pass@pypi-proxy.fly.dev/basic-auth/simple").unwrap(); + let redacted = redacted_url(&url); + assert_eq!(redacted.username(), ""); + assert!(redacted.password().is_none()); + assert_eq!( + format!("{redacted}"), + "https://pypi-proxy.fly.dev/basic-auth/simple" + ); + } + + #[test] + fn from_url_just_username() { + let url = Url::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap(); + let redacted = redacted_url(&url); + assert_eq!(redacted.username(), ""); + assert!(redacted.password().is_none()); + assert_eq!( + format!("{redacted}"), + "https://pypi-proxy.fly.dev/basic-auth/simple" + ); + } +} diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 5247f5ca3..26291815e 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -1515,7 +1515,7 @@ impl std::fmt::Display for PubGrubHint { "hint".bold().cyan(), ":".bold(), name.cyan(), - found_index.cyan(), + found_index.redacted().cyan(), PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(), next_index.cyan(), "--index-strategy unsafe-best-match".green(), diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index eba275850..dc5444b2b 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -43,6 +43,7 @@ uv-platform-tags = { workspace = true } uv-publish = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true, features = ["schemars"] } +uv-redacted = { workspace = true } uv-requirements = { workspace = true } uv-requirements-txt = { workspace = true } uv-resolver = { workspace = true } diff --git a/crates/uv/src/commands/reporters.rs b/crates/uv/src/commands/reporters.rs index 70cd8fcec..08fd82191 100644 --- a/crates/uv/src/commands/reporters.rs +++ b/crates/uv/src/commands/reporters.rs @@ -8,6 +8,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use owo_colors::OwoColorize; use rustc_hash::FxHashMap; use url::Url; +use uv_redacted::redacted_url; use crate::commands::human_readable_bytes; use crate::printer::Printer; @@ -300,6 +301,7 @@ impl ProgressReporter { } fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + let url = redacted_url(url); let ProgressMode::Multi { multi_progress, state, @@ -330,6 +332,7 @@ impl ProgressReporter { } fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + let url = redacted_url(url); let ProgressMode::Multi { state, multi_progress,