@@ -199,34 +199,27 @@ pub fn protocol_endpoint(uri: Uri) -> (String, String) {
199199}
200200
201201/// Error returned when a configured endpoint cannot be used as an absolute HTTP URL.
202+ ///
203+ /// Endpoint and path values are deliberately omitted so configuration errors
204+ /// cannot retain or display embedded credentials or tokens.
202205#[ derive( Debug , Snafu ) ]
203206pub enum HttpEndpointError {
204- #[ snafu( display( "endpoint `{endpoint}` is not a valid URI: {source}" ) ) ]
205- InvalidUri {
206- endpoint : String ,
207- source : http:: uri:: InvalidUri ,
208- } ,
209-
210- #[ snafu( display( "endpoint `{endpoint}` has an invalid path `{path}`: {source}" ) ) ]
211- InvalidPath {
212- endpoint : String ,
213- path : String ,
214- source : http:: uri:: InvalidUri ,
215- } ,
216-
217- #[ snafu( display( "endpoint `{endpoint}` cannot be reassembled from its parts: {source}" ) ) ]
218- InvalidUriParts {
219- endpoint : String ,
220- source : http:: uri:: InvalidUriParts ,
221- } ,
207+ #[ snafu( display( "endpoint is not a valid URI: {source}" ) ) ]
208+ InvalidUri { source : http:: uri:: InvalidUri } ,
209+
210+ #[ snafu( display( "endpoint has an invalid path: {source}" ) ) ]
211+ InvalidPath { source : http:: uri:: InvalidUri } ,
212+
213+ #[ snafu( display( "endpoint cannot be reassembled from its parts: {source}" ) ) ]
214+ InvalidUriParts { source : http:: uri:: InvalidUriParts } ,
222215
223216 #[ snafu( display(
224- "endpoint must be an absolute http(s) URL, for example `https://example.com`; got `{endpoint}` "
217+ "endpoint must be an absolute http(s) URL, for example `https://example.com`"
225218 ) ) ]
226- NotAbsoluteHttp { endpoint : String } ,
219+ NotAbsoluteHttp ,
227220
228- #[ snafu( display( "endpoint `{endpoint}` has an invalid port" ) ) ]
229- InvalidPort { endpoint : String } ,
221+ #[ snafu( display( "endpoint has an invalid port" ) ) ]
222+ InvalidPort ,
230223}
231224
232225/// A `Uri` proven to be an absolute `http`/`https` URL.
@@ -276,14 +269,10 @@ impl HttpEndpoint {
276269 let has_valid_scheme_and_host = matches ! ( uri. scheme_str( ) , Some ( "http" | "https" ) )
277270 && uri. host ( ) . is_some_and ( |host| !host. is_empty ( ) ) ;
278271 if !has_valid_scheme_and_host {
279- return Err ( HttpEndpointError :: NotAbsoluteHttp {
280- endpoint : redact_userinfo ( & uri. to_string ( ) ) ,
281- } ) ;
272+ return Err ( HttpEndpointError :: NotAbsoluteHttp ) ;
282273 }
283274 if authority_has_invalid_port ( & uri) {
284- return Err ( HttpEndpointError :: InvalidPort {
285- endpoint : redact_userinfo ( & uri. to_string ( ) ) ,
286- } ) ;
275+ return Err ( HttpEndpointError :: InvalidPort ) ;
287276 }
288277 Ok ( Self ( uri) )
289278 }
@@ -300,14 +289,10 @@ impl HttpEndpoint {
300289 // the scheme is added up front rather than relying on the parser to
301290 // accept authority-form input.
302291 let uri = if has_scheme ( endpoint) {
303- endpoint. parse :: < Uri > ( ) . context ( InvalidUriSnafu {
304- endpoint : redact_userinfo ( endpoint) ,
305- } ) ?
292+ endpoint. parse :: < Uri > ( ) . context ( InvalidUriSnafu ) ?
306293 } else {
307294 let endpoint = format ! ( "https://{endpoint}" ) ;
308- endpoint. parse :: < Uri > ( ) . context ( InvalidUriSnafu {
309- endpoint : redact_userinfo ( & endpoint) ,
310- } ) ?
295+ endpoint. parse :: < Uri > ( ) . context ( InvalidUriSnafu ) ?
311296 } ;
312297 Self :: new ( uri)
313298 }
@@ -375,13 +360,8 @@ impl HttpEndpoint {
375360 } else {
376361 format ! ( "{base_path}/{}" , path. strip_prefix( '/' ) . unwrap_or( path) )
377362 } ;
378- parts. path_and_query = Some ( joined. parse :: < PathAndQuery > ( ) . context ( InvalidPathSnafu {
379- endpoint : redact_userinfo ( & self . 0 . to_string ( ) ) ,
380- path : joined,
381- } ) ?) ;
382- let uri = Uri :: from_parts ( parts) . context ( InvalidUriPartsSnafu {
383- endpoint : redact_userinfo ( & self . 0 . to_string ( ) ) ,
384- } ) ?;
363+ parts. path_and_query = Some ( joined. parse :: < PathAndQuery > ( ) . context ( InvalidPathSnafu ) ?) ;
364+ let uri = Uri :: from_parts ( parts) . context ( InvalidUriPartsSnafu ) ?;
385365 Self :: new ( uri)
386366 }
387367
@@ -401,13 +381,8 @@ impl HttpEndpoint {
401381 . map ( PathAndQuery :: path)
402382 . unwrap_or_default ( ) ;
403383 let joined = format ! ( "{base_path}{suffix}" ) ;
404- parts. path_and_query = Some ( joined. parse :: < PathAndQuery > ( ) . context ( InvalidPathSnafu {
405- endpoint : redact_userinfo ( & self . 0 . to_string ( ) ) ,
406- path : joined,
407- } ) ?) ;
408- let uri = Uri :: from_parts ( parts) . context ( InvalidUriPartsSnafu {
409- endpoint : redact_userinfo ( & self . 0 . to_string ( ) ) ,
410- } ) ?;
384+ parts. path_and_query = Some ( joined. parse :: < PathAndQuery > ( ) . context ( InvalidPathSnafu ) ?) ;
385+ let uri = Uri :: from_parts ( parts) . context ( InvalidUriPartsSnafu ) ?;
411386 Self :: new ( uri)
412387 }
413388}
@@ -464,59 +439,6 @@ fn has_scheme(endpoint: &str) -> bool {
464439 && chars. all ( |c| c. is_ascii_alphanumeric ( ) || matches ! ( c, '+' | '-' | '.' ) )
465440}
466441
467- /// Redacts userinfo from an endpoint string for error messages, so embedded
468- /// basic-auth credentials are not echoed into logs or command output.
469- ///
470- /// Leading scheme prefixes are skipped before inspecting the authority-like
471- /// segment. Repeating this handles a defaulted `https://` prefix followed by a
472- /// malformed original prefix such as `http:/`. An `@` later in the path or
473- /// query is left untouched.
474- fn redact_userinfo ( endpoint : & str ) -> String {
475- let mut authority_start = 0 ;
476- while let Some ( rest) = endpoint. get ( authority_start..) {
477- let Some ( scheme_end) = rest. find ( ':' ) else {
478- break ;
479- } ;
480- let Some ( scheme) = rest. get ( ..scheme_end) else {
481- break ;
482- } ;
483- let mut chars = scheme. chars ( ) ;
484- let valid_scheme = matches ! ( chars. next( ) , Some ( c) if c. is_ascii_alphabetic( ) )
485- && chars. all ( |c| c. is_ascii_alphanumeric ( ) || matches ! ( c, '+' | '-' | '.' ) ) ;
486- if !valid_scheme {
487- break ;
488- }
489-
490- let after_colon = authority_start + scheme_end + 1 ;
491- let slash_count = endpoint
492- . get ( after_colon..)
493- . unwrap_or_default ( )
494- . bytes ( )
495- . take_while ( |byte| * byte == b'/' )
496- . count ( ) ;
497- let is_http = scheme. eq_ignore_ascii_case ( "http" ) || scheme. eq_ignore_ascii_case ( "https" ) ;
498- if slash_count == 0 && !is_http {
499- break ;
500- }
501- authority_start = after_colon + slash_count;
502- }
503-
504- let Some ( rest) = endpoint. get ( authority_start..) else {
505- return endpoint. to_string ( ) ;
506- } ;
507- // The authority ends at the first `/`, `?`, or `#`; `find` returns the
508- // byte offset of an ASCII delimiter, which is a valid char boundary.
509- let ( authority, tail) = match rest. find ( [ '/' , '?' , '#' ] ) {
510- Some ( i) => rest. split_at ( i) ,
511- None => ( rest, "" ) ,
512- } ;
513- let Some ( ( _, host_port) ) = authority. rsplit_once ( '@' ) else {
514- return endpoint. to_string ( ) ;
515- } ;
516- let prefix = endpoint. get ( ..authority_start) . unwrap_or_default ( ) ;
517- format ! ( "{prefix}<redacted>@{host_port}{tail}" )
518- }
519-
520442#[ cfg( test) ]
521443mod tests {
522444 use super :: * ;
@@ -669,10 +591,10 @@ mod tests {
669591 assert ! (
670592 matches!(
671593 HttpEndpoint :: parse( endpoint) ,
672- Err ( HttpEndpointError :: NotAbsoluteHttp { .. } )
594+ Err ( HttpEndpointError :: NotAbsoluteHttp )
673595 | Err ( HttpEndpointError :: InvalidUri { .. } )
674596 | Err ( HttpEndpointError :: InvalidUriParts { .. } )
675- | Err ( HttpEndpointError :: InvalidPort { .. } )
597+ | Err ( HttpEndpointError :: InvalidPort )
676598 ) ,
677599 "expected `{endpoint}` to be rejected"
678600 ) ;
@@ -693,7 +615,7 @@ mod tests {
693615 ] {
694616 assert ! ( matches!(
695617 HttpEndpoint :: parse( endpoint) ,
696- Err ( HttpEndpointError :: InvalidPort { .. } )
618+ Err ( HttpEndpointError :: InvalidPort )
697619 ) ) ;
698620 }
699621 }
@@ -704,16 +626,17 @@ mod tests {
704626 "http://user:secret@localhost:notaport" ,
705627 "user:secret@exa mple.com" ,
706628 "http:/user:secret@example.com" ,
629+ "http:://user:secret@example.com" ,
707630 ] {
708631 let error = HttpEndpoint :: parse ( endpoint) . expect_err ( "endpoint should be rejected" ) ;
709632 let message = error. to_string ( ) ;
710633 assert ! (
711- !message. contains( "secret" ) ,
712- "error must not echo the password : {message}"
634+ !message. contains( endpoint ) ,
635+ "error must not echo the endpoint : {message}"
713636 ) ;
714637 assert ! (
715- message. contains( "<redacted>@ " ) ,
716- "error should mark the redacted userinfo : {message}"
638+ ! message. contains( "secret " ) ,
639+ "error must not echo the password : {message}"
717640 ) ;
718641 }
719642 }
0 commit comments