diff --git a/Cargo.lock b/Cargo.lock index f5ab11636d..ff61dfcff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5136,6 +5136,7 @@ dependencies = [ "nym-explorer-client", "nym-gateway-client", "nym-gateway-requests", + "nym-http-api-client", "nym-id", "nym-metrics", "nym-mixnet-client", @@ -5747,6 +5748,7 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-gateway-requests", + "nym-http-api-client", "nym-network-defaults", "nym-pemstore", "nym-sphinx", diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index f6536b9762..94700eac42 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -40,6 +40,7 @@ nym-crypto = { path = "../crypto" } nym-explorer-client = { path = "../../explorer-api/explorer-client" } nym-gateway-client = { path = "../client-libs/gateway-client" } nym-gateway-requests = { path = "../gateway-requests" } +nym-http-api-client = { path = "../http-api-client" } nym-metrics = { path = "../nym-metrics" } nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" } nym-sphinx = { path = "../nymsphinx" } diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 6c434d1aad..624cbe6028 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -36,6 +36,13 @@ pub enum ClientCoreError { #[error("no gateway with id: {0}")] NoGatewayWithId(String), + #[error("Invalid URL: {0}")] + InvalidUrl(String), + + #[cfg(not(target_arch = "wasm32"))] + #[error("resolution failed: {0}")] + ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError), + #[error("no gateways on network")] NoGatewaysOnNetwork, diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 0e9dd38eb4..1dabaa9d31 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -15,6 +15,9 @@ use std::{sync::Arc, time::Duration}; use tungstenite::Message; use url::Url; +#[cfg(not(target_arch = "wasm32"))] +use crate::init::websockets::connect_async; + use nym_topology::NodeId; #[cfg(not(target_arch = "wasm32"))] use tokio::net::TcpStream; @@ -23,8 +26,6 @@ use tokio::time::sleep; #[cfg(not(target_arch = "wasm32"))] use tokio::time::Instant; #[cfg(not(target_arch = "wasm32"))] -use tokio_tungstenite::connect_async; -#[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; #[cfg(target_arch = "wasm32")] use wasm_utils::websocket::JSWebsocket; @@ -131,7 +132,7 @@ pub async fn gateways_for_init( async fn connect(endpoint: &str) -> Result { match tokio::time::timeout(CONN_TIMEOUT, connect_async(endpoint)).await { Err(_elapsed) => Err(ClientCoreError::GatewayConnectionTimeout), - Ok(Err(conn_failure)) => Err(conn_failure.into()), + Ok(Err(conn_failure)) => Err(conn_failure), Ok(Ok((stream, _))) => Ok(stream), } } diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 4954efbb3d..bbe27769ee 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -26,6 +26,8 @@ use serde::Serialize; pub mod helpers; pub mod types; +#[cfg(not(target_arch = "wasm32"))] +pub(crate) mod websockets; // helpers for error wrapping diff --git a/common/client-core/src/init/websockets.rs b/common/client-core/src/init/websockets.rs new file mode 100644 index 0000000000..6d09a325b0 --- /dev/null +++ b/common/client-core/src/init/websockets.rs @@ -0,0 +1,44 @@ +use crate::error::ClientCoreError; + +use nym_http_api_client::HickoryDnsResolver; +use tokio::net::TcpStream; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; +use tungstenite::handshake::client::Response; +use url::{Host, Url}; + +use std::net::SocketAddr; + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) async fn connect_async( + endpoint: &str, +) -> Result<(WebSocketStream>, Response), ClientCoreError> { + let resolver = HickoryDnsResolver::default(); + let uri = Url::parse(endpoint).map_err(|_| ClientCoreError::InvalidUrl(endpoint.to_owned()))?; + let port: u16 = uri.port_or_known_default().unwrap_or(443); + + let host = uri + .host() + .ok_or(ClientCoreError::InvalidUrl(endpoint.to_owned()))?; + + // Get address for tcp connection, if a domain is provided use our preferred resolver rather than + // the default std resolve + let sock_addrs: Vec = match host { + Host::Ipv4(addr) => vec![SocketAddr::new(addr.into(), port)], + Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)], + Host::Domain(domain) => { + // Do a DNS lookup for the domain using our custom DNS resolver + resolver + .resolve_str(domain) + .await? + .into_iter() + .map(|a| SocketAddr::new(a, port)) + .collect() + } + }; + + let stream = TcpStream::connect(&sock_addrs[..]).await?; + + tokio_tungstenite::client_async_tls(endpoint, stream) + .await + .map_err(Into::into) +} diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 6aa2795d3f..efc699473a 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -27,6 +27,7 @@ nym-credential-storage = { path = "../../credential-storage" } nym-credentials-interface = { path = "../../credentials-interface" } nym-crypto = { path = "../../crypto" } nym-gateway-requests = { path = "../../gateway-requests" } +nym-http-api-client = { path = "../../http-api-client" } nym-network-defaults = { path = "../../network-defaults" } nym-sphinx = { path = "../../nymsphinx" } nym-statistics-common = { path = "../../statistics" } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 3c2435121a..5b42fd7b96 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -40,8 +40,6 @@ use url::Url; use std::os::fd::RawFd; #[cfg(not(target_arch = "wasm32"))] use tokio::time::sleep; -#[cfg(not(target_arch = "wasm32"))] -use tokio_tungstenite::connect_async; #[cfg(not(unix))] use std::os::raw::c_int as RawFd; @@ -53,6 +51,11 @@ use zeroize::Zeroizing; pub mod config; +#[cfg(not(target_arch = "wasm32"))] +pub(crate) mod websockets; +#[cfg(not(target_arch = "wasm32"))] +use websockets::connect_async; + pub struct GatewayConfig { pub gateway_identity: identity::PublicKey, @@ -201,15 +204,7 @@ impl GatewayClient { "Attemting to establish connection to gateway at: {}", self.gateway_address ); - let ws_stream = match connect_async(&self.gateway_address).await { - Ok((ws_stream, _)) => ws_stream, - Err(error) => { - return Err(GatewayClientError::NetworkConnectionFailed { - address: self.gateway_address.clone(), - source: error, - }) - } - }; + let (ws_stream, _) = connect_async(&self.gateway_address).await?; self.connection = SocketState::Available(Box::new(ws_stream)); diff --git a/common/client-libs/gateway-client/src/client/websockets.rs b/common/client-libs/gateway-client/src/client/websockets.rs new file mode 100644 index 0000000000..32527571ec --- /dev/null +++ b/common/client-libs/gateway-client/src/client/websockets.rs @@ -0,0 +1,53 @@ +use crate::error::GatewayClientError; + +use nym_http_api_client::HickoryDnsResolver; +use tokio::net::TcpStream; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; +use tungstenite::handshake::client::Response; +use url::{Host, Url}; + +use std::net::SocketAddr; + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) async fn connect_async( + endpoint: &str, +) -> Result<(WebSocketStream>, Response), GatewayClientError> { + let resolver = HickoryDnsResolver::default(); + let uri = + Url::parse(endpoint).map_err(|_| GatewayClientError::InvalidUrl(endpoint.to_owned()))?; + let port: u16 = uri.port_or_known_default().unwrap_or(443); + + let host = uri + .host() + .ok_or(GatewayClientError::InvalidUrl(endpoint.to_owned()))?; + + // Get address for tcp connection, if a domain is provided use our preferred resolver rather than + // the default std resolve + let sock_addrs: Vec = match host { + Host::Ipv4(addr) => vec![SocketAddr::new(addr.into(), port)], + Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)], + Host::Domain(domain) => { + // Do a DNS lookup for the domain using our custom DNS resolver + resolver + .resolve_str(domain) + .await? + .into_iter() + .map(|a| SocketAddr::new(a, port)) + .collect() + } + }; + + let stream = TcpStream::connect(&sock_addrs[..]).await.map_err(|error| { + GatewayClientError::NetworkConnectionFailed { + address: endpoint.to_owned(), + source: error.into(), + } + })?; + + tokio_tungstenite::client_async_tls(endpoint, stream) + .await + .map_err(|error| GatewayClientError::NetworkConnectionFailed { + address: endpoint.to_owned(), + source: error, + }) +} diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index e1b581d30e..3432f43131 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -44,7 +44,11 @@ pub enum GatewayClientError { NetworkConnectionFailed { address: String, source: WsError }, #[error("Invalid URL: {0}")] - InvalidURL(String), + InvalidUrl(String), + + #[cfg(not(target_arch = "wasm32"))] + #[error("resolution failed: {0}")] + ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError), #[error("No shared key was provided or obtained")] NoSharedKeyAvailable, diff --git a/common/http-api-client/src/dns.rs b/common/http-api-client/src/dns.rs index 137809d6a7..d35d72487b 100644 --- a/common/http-api-client/src/dns.rs +++ b/common/http-api-client/src/dns.rs @@ -40,6 +40,7 @@ struct SocketAddrs { #[derive(Debug, thiserror::Error)] #[error("hickory-dns resolver error: {hickory_error}")] +/// Error occurring while resolving a hostname into an IP address. pub struct HickoryDnsError { #[from] hickory_error: ResolveError, diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index c75e7d64cd..bd359b199d 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -22,7 +22,7 @@ pub use user_agent::UserAgent; #[cfg(not(target_arch = "wasm32"))] mod dns; #[cfg(not(target_arch = "wasm32"))] -pub use dns::HickoryDnsResolver; +pub use dns::{HickoryDnsError, HickoryDnsResolver}; // The timeout is relatively high as we are often making requests over the mixnet, where latency is // high and chatty protocols take a while to complete.