use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; use std::str::FromStr; use url::Url; #[derive(Debug)] pub enum SensitiveError { InvalidUrl(String), ParseError(url::ParseError), RedactError(String), } impl fmt::Display for SensitiveError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) } } // Wrapper around Url which provides a custom `Display` implementation to protect user secrets. #[derive(Clone, PartialEq)] pub struct SensitiveUrl { pub full: Url, pub 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 { self.redacted.fmt(f) } } impl AsRef for SensitiveUrl { fn as_ref(&self) -> &str { self.redacted.as_str() } } impl Serialize for SensitiveUrl { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(self.full.as_ref()) } } 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 = SensitiveError; fn from_str(s: &str) -> Result { Self::parse(s) } } impl SensitiveUrl { pub fn parse(url: &str) -> Result { let surl = Url::parse(url).map_err(SensitiveError::ParseError)?; SensitiveUrl::new(surl) } fn new(full: Url) -> Result { let mut redacted = full.clone(); redacted .path_segments_mut() .map_err(|_| SensitiveError::InvalidUrl("URL cannot be a base.".to_string()))? .clear(); redacted.set_query(None); if redacted.has_authority() { redacted.set_username("").map_err(|_| { SensitiveError::RedactError("Unable to redact username.".to_string()) })?; redacted.set_password(None).map_err(|_| { SensitiveError::RedactError("Unable to redact password.".to_string()) })?; } Ok(Self { full, redacted: redacted.to_string(), }) } } #[cfg(test)] mod tests { use super::*; #[test] fn redact_remote_url() { let full = "https://project:secret@example.com/example?somequery"; let surl = SensitiveUrl::parse(full).unwrap(); assert_eq!(surl.to_string(), "https://example.com/"); assert_eq!(surl.full.to_string(), full); } #[test] fn redact_localhost_url() { let full = "http://localhost:5052/"; let surl = SensitiveUrl::parse(full).unwrap(); assert_eq!(surl.to_string(), "http://localhost:5052/"); assert_eq!(surl.full.to_string(), full); } }