@@ -165,13 +165,23 @@ impl std::fmt::Debug for Network {
165165 . map ( |( k, _) | ( k. as_str ( ) , "<concealed>" ) )
166166 . collect ( ) ;
167167 f. debug_struct ( "Network" )
168- . field ( "rpc_url" , & self . rpc_url )
168+ . field ( "rpc_url" , & redact_rpc_url ( & self . rpc_url ) )
169169 . field ( "rpc_headers" , & concealed)
170170 . field ( "network_passphrase" , & self . network_passphrase )
171171 . finish ( )
172172 }
173173}
174174
175+ pub fn redact_rpc_url ( rpc_url : & str ) -> String {
176+ let Ok ( mut url) = Url :: parse ( rpc_url) else {
177+ return rpc_url. to_string ( ) ;
178+ } ;
179+ if url. password ( ) . is_some ( ) {
180+ let _ = url. set_password ( Some ( "redacted" ) ) ;
181+ }
182+ url. to_string ( )
183+ }
184+
175185fn parse_http_header ( header : & str ) -> Result < ( String , String ) , Error > {
176186 let header_components = header. splitn ( 2 , ':' ) ;
177187
@@ -664,4 +674,55 @@ mod tests {
664674 r#"Network { rpc_url: "http://localhost:8000/rpc", rpc_headers: [("Authorization", "<concealed>"), ("X-Api-Key", "<concealed>")], network_passphrase: "Test Network" }"#
665675 ) ;
666676 }
677+
678+ #[ test]
679+ fn test_debug_conceals_rpc_url_password ( ) {
680+ let network = Network {
681+ rpc_url : "https://alice:supersecret@rpc.example.com/soroban" . to_string ( ) ,
682+ network_passphrase : "Test Network" . to_string ( ) ,
683+ rpc_headers : Vec :: new ( ) ,
684+ } ;
685+ let rendered = format ! ( "{network:?}" ) ;
686+ assert ! (
687+ !rendered. contains( "supersecret" ) ,
688+ "password leaked into Debug output: {rendered}"
689+ ) ;
690+ assert ! (
691+ rendered. contains( "alice:redacted" ) ,
692+ "expected `alice:redacted` in Debug output: {rendered}"
693+ ) ;
694+ }
695+
696+ #[ test]
697+ fn redact_rpc_url_leaves_url_without_password_unchanged ( ) {
698+ let plain = "https://rpc.example.com/soroban" ;
699+ assert_eq ! ( redact_rpc_url( plain) , plain) ;
700+
701+ let user_only = "https://alice@rpc.example.com/soroban" ;
702+ assert_eq ! ( redact_rpc_url( user_only) , user_only) ;
703+ }
704+
705+ #[ test]
706+ fn redact_rpc_url_replaces_password_with_placeholder ( ) {
707+ let with_password = "https://alice:supersecret@rpc.example.com/soroban" ;
708+ let redacted = redact_rpc_url ( with_password) ;
709+ assert ! (
710+ !redacted. contains( "supersecret" ) ,
711+ "password leaked: {redacted}"
712+ ) ;
713+ assert ! (
714+ redacted. contains( "alice:redacted" ) ,
715+ "expected `alice:redacted`: {redacted}"
716+ ) ;
717+ assert ! (
718+ redacted. contains( "rpc.example.com/soroban" ) ,
719+ "expected host and path preserved: {redacted}"
720+ ) ;
721+ }
722+
723+ #[ test]
724+ fn redact_rpc_url_returns_input_when_unparseable ( ) {
725+ let bad = "not a url" ;
726+ assert_eq ! ( redact_rpc_url( bad) , bad) ;
727+ }
667728}
0 commit comments