diff --git a/Cargo.lock b/Cargo.lock index c9fa0ab2bf..0426268f6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3941,6 +3941,7 @@ dependencies = [ "log", "nym-client-core", "nym-explorer-client", + "nym-harbour-master-client", "nym-sdk", "nym-topology", "nym-validator-client", @@ -4018,6 +4019,17 @@ dependencies = [ "serde", ] +[[package]] +name = "nym-harbour-master-client" +version = "0.1.0" +dependencies = [ + "nym-http-api-client", + "reqwest", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "nym-http-api-client" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index bc700cc298..c30214dc96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/nym-connection-monitor", "crates/nym-gateway-directory", "crates/nym-gateway-probe", + "crates/nym-harbour-master-client", "crates/nym-vpn-proto", "nym-vpn-cli", "nym-vpn-lib", @@ -20,6 +21,7 @@ exclude = ["nym-vpn-desktop/src-tauri"] # nym-credential-storage = { path = "../nym/common/credential-storage" } # nym-crypto = { path = "../nym/common/crypto" } # nym-explorer-client = { path = "../nym/explorer-api/explorer-client" } +# nym-http-api-client = { path = "../nym/common/http-api-client" } # nym-id = { path = "../nym/common/nym-id" } # nym-ip-packet-requests = { path = "../nym/common/ip-packet-requests" } # nym-node-requests = { path = "../nym/nym-node/nym-node-requests" } @@ -83,6 +85,7 @@ nym-credential-storage = { git = "https://github.com/nymtech/nym", rev = "b8b66f nym-credentials = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } nym-crypto = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } nym-explorer-client = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } +nym-http-api-client = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } nym-id = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } nym-ip-packet-requests = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } nym-node-requests = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } @@ -90,4 +93,4 @@ nym-sdk = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } nym-task = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } nym-topology = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } nym-validator-client = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } -nym-wireguard-types = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } \ No newline at end of file +nym-wireguard-types = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" } diff --git a/crates/nym-gateway-directory/Cargo.toml b/crates/nym-gateway-directory/Cargo.toml index 4c5b4a3e94..60bdc86471 100644 --- a/crates/nym-gateway-directory/Cargo.toml +++ b/crates/nym-gateway-directory/Cargo.toml @@ -5,11 +5,13 @@ edition.workspace = true license.workspace = true [dependencies] +chrono = "0.4.37" hickory-resolver.workspace = true itertools.workspace = true log.workspace = true nym-client-core.workspace = true nym-explorer-client.workspace = true +nym-harbour-master-client = { path = "../nym-harbour-master-client" } nym-sdk.workspace = true nym-topology.workspace = true nym-validator-client.workspace = true @@ -18,4 +20,3 @@ serde.workspace = true thiserror.workspace = true tracing.workspace = true url.workspace = true -chrono = "0.4.37" diff --git a/crates/nym-gateway-directory/src/error.rs b/crates/nym-gateway-directory/src/error.rs index 3f6b52dcdc..c142782298 100644 --- a/crates/nym-gateway-directory/src/error.rs +++ b/crates/nym-gateway-directory/src/error.rs @@ -17,6 +17,12 @@ pub enum Error { #[error(transparent)] ExplorerApiError(#[from] nym_explorer_client::ExplorerApiError), + #[error(transparent)] + HarbourMasterError(#[from] nym_harbour_master_client::HarbourMasterError), + + #[error(transparent)] + HarbourMasterApiError(#[from] nym_harbour_master_client::HarbourMasterApiError), + #[error("failed to fetch location data from explorer-api: {error}")] FailedFetchLocationData { error: nym_explorer_client::ExplorerApiError, diff --git a/crates/nym-gateway-directory/src/gateway_client.rs b/crates/nym-gateway-directory/src/gateway_client.rs index 2f8384b32f..8801bce556 100644 --- a/crates/nym-gateway-directory/src/gateway_client.rs +++ b/crates/nym-gateway-directory/src/gateway_client.rs @@ -8,6 +8,9 @@ use crate::{ }; use itertools::Itertools; use nym_explorer_client::{ExplorerClient, Location, PrettyDetailedGatewayBond}; +use nym_harbour_master_client::{ + Gateway as HmGateway, HarbourMasterApiClientExt, PagedResult as HmPagedResult, +}; use nym_validator_client::{models::DescribedGateway, NymApiClient}; use std::net::IpAddr; use tracing::info; @@ -17,6 +20,7 @@ use url::Url; pub struct Config { pub api_url: Url, pub explorer_url: Option, + pub harbour_master_url: Option, } impl Default for Config { @@ -39,6 +43,7 @@ impl Default for Config { Config { api_url: default_api_url, explorer_url: default_explorer_url, + harbour_master_url: None, } } } @@ -61,11 +66,21 @@ impl Config { self.explorer_url = Some(explorer_url); self } + + pub fn harbour_master_url(&self) -> Option<&Url> { + self.harbour_master_url.as_ref() + } + + pub fn with_custom_harbour_master_url(mut self, harbour_master_url: Url) -> Self { + self.harbour_master_url = Some(harbour_master_url); + self + } } pub struct GatewayClient { api_client: NymApiClient, explorer_client: Option, + harbour_master_client: Option, } impl GatewayClient { @@ -76,10 +91,16 @@ impl GatewayClient { } else { None }; + let harbour_master_client = if let Some(url) = config.harbour_master_url { + Some(nym_harbour_master_client::Client::new_url(url, None)?) + } else { + None + }; Ok(GatewayClient { api_client, explorer_client, + harbour_master_client, }) } @@ -105,6 +126,20 @@ impl GatewayClient { } } + #[allow(unused)] + async fn lookup_gateways_in_harbour_master(&self) -> Option>> { + if let Some(harbour_master_client) = &self.harbour_master_client { + Some( + harbour_master_client + .get_gateways() + .await + .map_err(Error::HarbourMasterApiError), + ) + } else { + None + } + } + pub async fn lookup_described_gateways_with_location( &self, ) -> Result> { diff --git a/crates/nym-harbour-master-client/Cargo.toml b/crates/nym-harbour-master-client/Cargo.toml new file mode 100644 index 0000000000..bfad7b3ce6 --- /dev/null +++ b/crates/nym-harbour-master-client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nym-harbour-master-client" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +nym-http-api-client.workspace = true +reqwest = { workspace = true, features = ["rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +thiserror.workspace = true diff --git a/crates/nym-harbour-master-client/src/client.rs b/crates/nym-harbour-master-client/src/client.rs new file mode 100644 index 0000000000..7291448588 --- /dev/null +++ b/crates/nym-harbour-master-client/src/client.rs @@ -0,0 +1,24 @@ +use nym_http_api_client::{ApiClient, HttpClientError, NO_PARAMS}; + +pub use nym_http_api_client::Client; + +use crate::{ + responses::{Gateway, PagedResult}, + routes, +}; + +// This is largely lifted from mix-fetch. The future of harbourmaster is uncertain, but ideally +// these two should be merged so they both can depend on the same crate. + +pub type HarbourMasterApiError = HttpClientError; + +#[allow(async_fn_in_trait)] +pub trait HarbourMasterApiClientExt: ApiClient { + // TODO: paging + async fn get_gateways(&self) -> Result, HarbourMasterApiError> { + self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) + .await + } +} + +impl HarbourMasterApiClientExt for Client {} diff --git a/crates/nym-harbour-master-client/src/error.rs b/crates/nym-harbour-master-client/src/error.rs new file mode 100644 index 0000000000..0d514315e6 --- /dev/null +++ b/crates/nym-harbour-master-client/src/error.rs @@ -0,0 +1,9 @@ +use crate::client::HarbourMasterApiError; + +#[derive(Debug, thiserror::Error)] +pub enum HarbourMasterError { + #[error("api error: {0}")] + HarbourMasterApiError(#[from] HarbourMasterApiError), +} + +pub type Result = std::result::Result; diff --git a/crates/nym-harbour-master-client/src/helpers.rs b/crates/nym-harbour-master-client/src/helpers.rs new file mode 100644 index 0000000000..157bc7b8f8 --- /dev/null +++ b/crates/nym-harbour-master-client/src/helpers.rs @@ -0,0 +1,12 @@ +use crate::{ + error::Result, + responses::{Gateway, PagedResult}, + Client, HarbourMasterApiClientExt, +}; + +const HARBOUR_MASTER: &str = "https://harbourmaster.nymtech.net"; + +pub async fn get_gateways() -> Result> { + let client = Client::new_url(HARBOUR_MASTER, None)?; + Ok(client.get_gateways().await?) +} diff --git a/crates/nym-harbour-master-client/src/lib.rs b/crates/nym-harbour-master-client/src/lib.rs new file mode 100644 index 0000000000..e21de49fb0 --- /dev/null +++ b/crates/nym-harbour-master-client/src/lib.rs @@ -0,0 +1,10 @@ +mod client; +mod error; +mod helpers; +mod responses; +mod routes; + +pub use client::{Client, HarbourMasterApiClientExt, HarbourMasterApiError}; +pub use error::HarbourMasterError; +pub use helpers::get_gateways; +pub use responses::{Gateway, PagedResult}; diff --git a/crates/nym-harbour-master-client/src/responses.rs b/crates/nym-harbour-master-client/src/responses.rs new file mode 100644 index 0000000000..d0c3170480 --- /dev/null +++ b/crates/nym-harbour-master-client/src/responses.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +// TODO: these should have their own crate shared with the harbourmaster service + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PagedResult { + pub page: u32, + pub size: u32, + pub total: i32, + pub items: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Gateway { + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, + pub last_probe_result: Option, + pub last_probe_log: Option, + pub last_testrun_utc: Option, + pub last_updated_utc: String, + pub routing_score: f32, + pub config_score: u32, +} diff --git a/crates/nym-harbour-master-client/src/routes.rs b/crates/nym-harbour-master-client/src/routes.rs new file mode 100644 index 0000000000..e6b7e7b3d9 --- /dev/null +++ b/crates/nym-harbour-master-client/src/routes.rs @@ -0,0 +1,2 @@ +pub const API_VERSION: &str = "v2"; +pub const GATEWAYS: &str = "gateways"; diff --git a/nym-vpn-lib/src/platform/mod.rs b/nym-vpn-lib/src/platform/mod.rs index e982d09753..f788cfa4ff 100644 --- a/nym-vpn-lib/src/platform/mod.rs +++ b/nym-vpn-lib/src/platform/mod.rs @@ -201,6 +201,7 @@ async fn get_gateway_countries( let config = nym_gateway_directory::Config { api_url, explorer_url: Some(explorer_url), + harbour_master_url: None, }; let gateway_client = GatewayClient::new(config)?; @@ -225,6 +226,7 @@ async fn get_low_latency_entry_country( let config = nym_gateway_directory::Config { api_url, explorer_url: Some(explorer_url), + harbour_master_url: None, }; let gateway_client = GatewayClient::new(config)?; let described = gateway_client.lookup_low_latency_entry_gateway().await?;