From 2d1885bb55900934362d07441c9470bb4274fd2b Mon Sep 17 00:00:00 2001 From: Kewe63 <86300262+Kewe63@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:17:30 +0300 Subject: [PATCH 1/2] fix: validate sync endpoint URL roundtrips --- crates/types/src/rpc_sync.rs | 82 ++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/crates/types/src/rpc_sync.rs b/crates/types/src/rpc_sync.rs index 77a61015..9829956b 100644 --- a/crates/types/src/rpc_sync.rs +++ b/crates/types/src/rpc_sync.rs @@ -19,7 +19,7 @@ use core::fmt; use std::str::FromStr; -use url::Url; +use url::{Host, Url}; /// A parsed endpoint URL for RPC synchronization. /// @@ -105,6 +105,21 @@ fn validate_ws_scheme(scheme: &str) -> Result<(), eyre::Report> { Ok(()) } +fn validate_derived_ws_port(http: &Url, has_ws_override: bool) -> Result<(), eyre::Report> { + if has_ws_override { + return Ok(()); + } + + if matches!(http.port(), Some(u16::MAX)) { + return Err(eyre::eyre!( + "Invalid HTTP URL port '{}': derived WebSocket port would overflow.", + u16::MAX + )); + } + + Ok(()) +} + /// Parses a WebSocket override in the format `=`. /// /// The value after `=` can be: @@ -142,6 +157,7 @@ impl FromStr for SyncEndpointUrl { Url::parse(http_part).map_err(|e| eyre::eyre!("Failed to parse HTTP URL: {e}"))?; validate_http_scheme(http.scheme())?; + validate_derived_ws_port(&http, ws_part.is_some())?; let ws = ws_part .map(|part| parse_ws_override(part, &http)) @@ -153,17 +169,23 @@ impl FromStr for SyncEndpointUrl { impl fmt::Display for SyncEndpointUrl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let host = self.http.host_str().expect("validated host"); + let host = host_for_display(&self.http); let http_port = self.http.port_or_known_default().expect("validated port"); let ws_url = self.websocket(); - let ws_host = ws_url.host_str().expect("validated host"); + let ws_host = host_for_display(&ws_url); - write!( - f, - "{}://{host}:{http_port},{}=", - self.http.scheme(), - ws_url.scheme() - )?; + write!(f, "{}://{host}:{http_port}", self.http.scheme())?; + let http_path = self.http.path(); + if http_path != "/" { + write!(f, "{http_path}")?; + } + if let Some(query) = self.http.query() { + write!(f, "?{query}")?; + } + if let Some(fragment) = self.http.fragment() { + write!(f, "#{fragment}")?; + } + write!(f, ",{}=", ws_url.scheme())?; let ws_path = ws_url.path(); let has_path = ws_path != "/"; @@ -189,6 +211,13 @@ impl fmt::Display for SyncEndpointUrl { } } +fn host_for_display(url: &Url) -> String { + match url.host().expect("validated host") { + Host::Ipv6(addr) => format!("[{addr}]"), + host => host.to_string(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -349,6 +378,41 @@ mod tests { assert_eq!(url.websocket().as_str(), "wss://ws.example.com:1212/"); } + #[test] + fn parse_rejects_http_port_that_would_overflow_derived_websocket_port() { + let err = "http://localhost:65535" + .parse::() + .unwrap_err(); + + assert!(err + .to_string() + .contains("derived WebSocket port would overflow")); + } + + #[test] + fn display_preserves_http_path_and_query() { + let endpoint: SyncEndpointUrl = + "https://rpc.example.com/api/v1?key=value,wss=ws.example.com/websocket" + .parse() + .unwrap(); + + assert_eq!( + endpoint.to_string(), + "https://rpc.example.com:443/api/v1?key=value,wss=ws.example.com/websocket" + ); + let reparsed: SyncEndpointUrl = endpoint.to_string().parse().unwrap(); + assert_eq!(endpoint, reparsed); + } + + #[test] + fn display_brackets_ipv6_hosts() { + let endpoint: SyncEndpointUrl = "http://[::1]:8545,ws=8546".parse().unwrap(); + + assert_eq!(endpoint.to_string(), "http://[::1]:8545,ws=8546"); + let reparsed: SyncEndpointUrl = endpoint.to_string().parse().unwrap(); + assert_eq!(endpoint, reparsed); + } + #[test] fn parse_wss_with_host_port_and_path_override() { let url: SyncEndpointUrl = "https://example.com,wss=ws.example.com:8546/websocket" From 0e628fcb50ab24d8956d3bcbbad24ee962a8b075 Mon Sep 17 00:00:00 2001 From: Kewe63 <86300262+Kewe63@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:26:56 +0300 Subject: [PATCH 2/2] fix(types): address sync endpoint review feedback Drop the custom IPv6 host formatter and rely on url's canonical host serialization, which already preserves IPv6 brackets. Preserve WebSocket override query and fragment components in Display output, and extend the round-trip regression to cover HTTP and WebSocket path/query/fragment components together. --- crates/types/src/rpc_sync.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/types/src/rpc_sync.rs b/crates/types/src/rpc_sync.rs index 9829956b..e476b345 100644 --- a/crates/types/src/rpc_sync.rs +++ b/crates/types/src/rpc_sync.rs @@ -19,7 +19,7 @@ use core::fmt; use std::str::FromStr; -use url::{Host, Url}; +use url::Url; /// A parsed endpoint URL for RPC synchronization. /// @@ -169,10 +169,10 @@ impl FromStr for SyncEndpointUrl { impl fmt::Display for SyncEndpointUrl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let host = host_for_display(&self.http); + let host = self.http.host_str().expect("validated host"); let http_port = self.http.port_or_known_default().expect("validated port"); let ws_url = self.websocket(); - let ws_host = host_for_display(&ws_url); + let ws_host = ws_url.host_str().expect("validated host"); write!(f, "{}://{host}:{http_port}", self.http.scheme())?; let http_path = self.http.path(); @@ -189,10 +189,11 @@ impl fmt::Display for SyncEndpointUrl { let ws_path = ws_url.path(); let has_path = ws_path != "/"; + let has_suffix = has_path || ws_url.query().is_some() || ws_url.fragment().is_some(); - if ws_host != host || has_path { - // Include the host when it differs or when a path is present - // (a bare port + path like `443/websocket` mis-parses as a hostname) + if ws_host != host || has_suffix { + // Include the host when it differs or when extra URL components are + // present (a bare port plus path/query/fragment mis-parses as a host). write!(f, "{ws_host}")?; if let Some(ws_port) = ws_url.port() { write!(f, ":{ws_port}")?; @@ -206,18 +207,17 @@ impl fmt::Display for SyncEndpointUrl { if has_path { write!(f, "{ws_path}")?; } + if let Some(query) = ws_url.query() { + write!(f, "?{query}")?; + } + if let Some(fragment) = ws_url.fragment() { + write!(f, "#{fragment}")?; + } Ok(()) } } -fn host_for_display(url: &Url) -> String { - match url.host().expect("validated host") { - Host::Ipv6(addr) => format!("[{addr}]"), - host => host.to_string(), - } -} - #[cfg(test)] mod tests { use super::*; @@ -390,15 +390,15 @@ mod tests { } #[test] - fn display_preserves_http_path_and_query() { + fn display_preserves_http_and_websocket_path_query_and_fragment() { let endpoint: SyncEndpointUrl = - "https://rpc.example.com/api/v1?key=value,wss=ws.example.com/websocket" + "https://rpc.example.com/api/v1?key=value#http-fragment,wss=ws.example.com/websocket?token=abc#ws-fragment" .parse() .unwrap(); assert_eq!( endpoint.to_string(), - "https://rpc.example.com:443/api/v1?key=value,wss=ws.example.com/websocket" + "https://rpc.example.com:443/api/v1?key=value#http-fragment,wss=ws.example.com/websocket?token=abc#ws-fragment" ); let reparsed: SyncEndpointUrl = endpoint.to_string().parse().unwrap(); assert_eq!(endpoint, reparsed);