diff --git a/Cargo.lock b/Cargo.lock index c8c14c7257..1d1108b1d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8449,9 +8449,10 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "sensitive_url" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b0221fa9905eec4163dbf7660b1876cc95663af1deddc3e19ebe49167c58c" dependencies = [ "serde", - "serde_json", "url", ] diff --git a/Cargo.toml b/Cargo.toml index d09b0fcd80..4d357816d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ members = [ "common/network_utils", "common/oneshot_broadcast", "common/pretty_reqwest_error", - "common/sensitive_url", "common/slot_clock", "common/system_health", "common/target_check", @@ -225,7 +224,7 @@ rpds = "0.11" rusqlite = { version = "0.28", features = ["bundled"] } rust_eth_kzg = "0.9" safe_arith = "0.1" -sensitive_url = { path = "common/sensitive_url", features = ["serde"] } +sensitive_url = { version = "0.1", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_repr = "0.1" diff --git a/common/sensitive_url/Cargo.toml b/common/sensitive_url/Cargo.toml deleted file mode 100644 index 3793cc5139..0000000000 --- a/common/sensitive_url/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "sensitive_url" -version = "0.1.0" -authors = ["Mac L "] -edition = { workspace = true } -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[features] -serde = ["dep:serde"] - -[dependencies] -serde = { workspace = true, optional = true } -url = { workspace = true } - -[dev-dependencies] -serde_json = { workspace = true } diff --git a/common/sensitive_url/src/lib.rs b/common/sensitive_url/src/lib.rs deleted file mode 100644 index 3f9240268d..0000000000 --- a/common/sensitive_url/src/lib.rs +++ /dev/null @@ -1,239 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; -use std::fmt; -use std::str::FromStr; -use url::Url; - -/// Errors that can occur when creating or parsing a `SensitiveUrl`. -#[derive(Debug)] -pub enum Error { - /// The URL cannot be used as a base URL. - InvalidUrl(String), - /// Failed to parse the URL string. - ParseError(url::ParseError), - /// Failed to redact sensitive information from the URL. - RedactError(String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg), - Error::ParseError(e) => write!(f, "Parse error: {}", e), - Error::RedactError(msg) => write!(f, "Redact error: {}", msg), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::ParseError(e) => Some(e), - _ => None, - } - } -} - -/// A URL wrapper that redacts sensitive information in `Display` and `Debug` output. -/// -/// This type stores both the full URL (with credentials, paths, and query parameters) -/// and a redacted version (containing only the scheme, host, and port). The redacted -/// version is used when displaying or debugging to prevent accidental leakage of -/// credentials in logs. -/// -/// Note that `SensitiveUrl` specifically does NOT implement `Deref`, meaning you cannot call -/// `Url` methods like `.password()` or `.scheme()` directly on `SensitiveUrl`. You must first -/// explicitly call `.expose_full()`. -/// -/// # Examples -/// -/// ``` -/// use sensitive_url::SensitiveUrl; -/// -/// let url = SensitiveUrl::parse("https://user:pass@example.com/api?token=secret").unwrap(); -/// -/// // Display shows only the redacted version: -/// assert_eq!(url.to_string(), "https://example.com/"); -/// -/// // But you can still access the full URL when needed: -/// let full = url.expose_full(); -/// assert_eq!(full.to_string(), "https://user:pass@example.com/api?token=secret"); -/// assert_eq!(full.password(), Some("pass")); -/// ``` -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct SensitiveUrl { - full: Url, - redacted: String, -} - -impl fmt::Display for SensitiveUrl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.redacted.fmt(f) - } -} - -impl fmt::Debug for SensitiveUrl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SensitiveUrl") - .field("redacted", &self.redacted) - // Maintains traditional `Debug` format but hides the 'full' field. - .finish_non_exhaustive() - } -} - -#[cfg(feature = "serde")] -impl Serialize for SensitiveUrl { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(self.full.as_ref()) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for SensitiveUrl { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s: String = Deserialize::deserialize(deserializer)?; - SensitiveUrl::parse(&s) - .map_err(|e| de::Error::custom(format!("Failed to deserialize sensitive URL {:?}", e))) - } -} - -impl FromStr for SensitiveUrl { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::parse(s) - } -} - -impl SensitiveUrl { - /// Attempts to parse a `&str` into a `SensitiveUrl`. - pub fn parse(url: &str) -> Result { - let surl = Url::parse(url).map_err(Error::ParseError)?; - SensitiveUrl::new(surl) - } - - /// Creates a `SensitiveUrl` from an existing `Url`. - pub fn new(full: Url) -> Result { - let mut redacted = full.clone(); - redacted - .path_segments_mut() - .map_err(|_| Error::InvalidUrl("URL cannot be a base.".to_string()))? - .clear(); - redacted.set_query(None); - - if redacted.has_authority() { - redacted - .set_username("") - .map_err(|_| Error::RedactError("Unable to redact username.".to_string()))?; - redacted - .set_password(None) - .map_err(|_| Error::RedactError("Unable to redact password.".to_string()))?; - } - - Ok(Self { - full, - redacted: redacted.to_string(), - }) - } - - /// Returns a reference to the full, unredacted URL. - pub fn expose_full(&self) -> &Url { - &self.full - } - - /// Returns the redacted URL as a `&str`. - pub fn redacted(&self) -> &str { - &self.redacted - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn redact_remote_url() { - let full = "https://user:pass@example.com/example?somequery"; - let surl = SensitiveUrl::parse(full).unwrap(); - assert_eq!(surl.to_string(), "https://example.com/"); - assert_eq!(surl.expose_full().to_string(), full); - } - - #[test] - fn redact_localhost_url() { - let full = "http://user:pass@localhost:5052/"; - let surl = SensitiveUrl::parse(full).unwrap(); - assert_eq!(surl.to_string(), "http://localhost:5052/"); - assert_eq!(surl.expose_full().to_string(), full); - } - - #[test] - fn test_no_credentials() { - let full = "https://example.com/path"; - let surl = SensitiveUrl::parse(full).unwrap(); - assert_eq!(surl.to_string(), "https://example.com/"); - assert_eq!(surl.expose_full().to_string(), full); - } - - #[test] - fn test_display() { - let full = "https://user:pass@example.com/api?token=secret"; - let surl = SensitiveUrl::parse(full).unwrap(); - - let display = surl.to_string(); - assert_eq!(display, "https://example.com/"); - } - - #[test] - fn test_debug() { - let full = "https://user:pass@example.com/api?token=secret"; - let surl = SensitiveUrl::parse(full).unwrap(); - - let debug = format!("{:?}", surl); - - assert_eq!( - debug, - "SensitiveUrl { redacted: \"https://example.com/\", .. }" - ); - } - - #[cfg(feature = "serde")] - mod serde_tests { - use super::*; - - #[test] - fn test_serialize() { - let full = "https://user:pass@example.com/api?token=secret"; - let surl = SensitiveUrl::parse(full).unwrap(); - - let json = serde_json::to_string(&surl).unwrap(); - assert_eq!(json, format!("\"{}\"", full)); - } - - #[test] - fn test_deserialize() { - let full = "https://user:pass@example.com/api?token=secret"; - let json = format!("\"{}\"", full); - - let surl: SensitiveUrl = serde_json::from_str(&json).unwrap(); - assert_eq!(surl.expose_full().as_str(), full); - } - - #[test] - fn test_roundtrip() { - let full = "https://user:pass@example.com/api?token=secret"; - let original = SensitiveUrl::parse(full).unwrap(); - - let json = serde_json::to_string(&original).unwrap(); - let deserialized: SensitiveUrl = serde_json::from_str(&json).unwrap(); - - assert_eq!(deserialized.expose_full(), original.expose_full()); - } - } -} diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index cd23138a1c..54035f2e82 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -15,7 +15,7 @@ logging = { workspace = true } node_test_rig = { path = "../node_test_rig" } parking_lot = { workspace = true } rayon = { workspace = true } -sensitive_url = { path = "../../common/sensitive_url" } +sensitive_url = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true }