1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "client")]
pub use client_helpers::*;
#[cfg(feature = "client")]
mod client_helpers {
use crate::api::SignedHostInformation;
use crate::api::client::NymNodeApiClientExt;
use nym_http_api_client::UserAgent;
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use std::time::Duration;
/// Builder-style helper for obtaining a validated [`crate::api::Client`] for a nym-node.
///
/// On top of the basic port-probing performed by [`try_get_valid_nym_node_api_client`],
/// this struct optionally:
/// - verifies that the node's self-reported ed25519 identity matches an expected value
/// (e.g. the identity committed on-chain during bonding), and
/// - checks the cryptographic signature on the node's host information.
///
/// Both checks require an extra HTTP round-trip to the node's `/host-information` endpoint
/// and are skipped when neither option is enabled.
#[derive(Debug)]
pub struct NymNodeApiClientRetriever {
/// Expected (base58-encoded) ed25519 identity of the node.
/// used to check against data retrieved from the host information
expected_identity: Option<String>,
/// Custom port to use when attempting to query the node.
custom_port: Option<u16>,
/// User agent to use when attempting to query the node.
user_agent: UserAgent,
/// Specify whether the signature on the host information should be verified.
verify_host_information: bool,
}
impl NymNodeApiClientRetriever {
/// Creates a new retriever with the given user agent.
/// All optional checks (identity verification, host information signature)
/// are disabled by default — use the builder methods to enable them.
pub fn new(user_agent: impl Into<UserAgent>) -> Self {
Self {
expected_identity: None,
custom_port: None,
user_agent: user_agent.into(),
verify_host_information: false,
}
}
/// If set, the node's self-reported ed25519 identity (from its `/host-information`
/// endpoint) will be compared against this value. A mismatch produces
/// [`crate::error::Error::MismatchedIdentity`].
#[must_use]
pub fn with_expected_identity(mut self, expected_identity: Option<String>) -> Self {
self.expected_identity = expected_identity;
self
}
/// Prepend `http://<host>:<port>` to the list of addresses probed during
/// [`get_client`](Self::get_client), so it is tried before the standard ports.
#[must_use]
pub fn with_custom_port(mut self, port: Option<u16>) -> Self {
self.custom_port = port;
self
}
/// Enable cryptographic verification of the node's host information signature.
/// When enabled, [`get_client`](Self::get_client) will return
/// [`crate::error::Error::MissignedHostInformation`] if the signature is invalid.
#[must_use]
pub fn with_verify_host_information(mut self) -> Self {
self.verify_host_information = true;
self
}
/// Probe the node's HTTP API, perform any configured verification, and return the
/// client together with the [`SignedHostInformation`] if it was fetched.
///
/// The host information is only retrieved when identity verification or signature
/// checking is enabled. When neither is active, the returned
/// [`ApiClientWithHostInformation::host_information`] will be `None`.
pub async fn get_client(
self,
base_host: &str,
node_id: u32,
) -> Result<ApiClientWithHostInformation, crate::error::Error> {
let base_client = try_get_valid_nym_node_api_client(
base_host,
node_id,
self.custom_port,
self.user_agent,
)
.await?;
// no need to retrieve host information if we don't have to perform any verification
if !self.verify_host_information && self.expected_identity.is_none() {
return Ok(base_client.into());
}
let host_info = retrieve_validated_host_information(
&base_client,
node_id,
&self.expected_identity,
self.verify_host_information,
)
.await?;
Ok(ApiClientWithHostInformation::from(base_client).with_host_information(host_info))
}
}
/// Fetch a node's [`SignedHostInformation`] and optionally validate it.
///
/// This is the standalone equivalent of the checks performed inside
/// [`NymNodeApiClientRetriever::get_client`], useful when the caller already
/// holds a [`crate::api::Client`] and only needs the host information.
///
/// When `expected_ed25519_identity` is `Some`, the node's self-reported identity
/// is compared against it — a mismatch produces [`crate::error::Error::MismatchedIdentity`].
/// When `verify_host_information` is `true`, the cryptographic signature on the
/// host information is checked — an invalid signature produces
/// [`crate::error::Error::MissignedHostInformation`].
pub async fn retrieve_validated_host_information(
client: &crate::api::Client,
node_id: u32,
expected_ed25519_identity: &Option<String>,
verify_host_information: bool,
) -> Result<SignedHostInformation, crate::error::Error> {
let host_info = match client.get_host_information().await {
Ok(info) => info,
Err(err) => {
return Err(crate::error::Error::QueryFailure {
host: client.current_url().to_string(),
node_id,
source: Box::new(err),
});
}
};
if let Some(expected_identity) = expected_ed25519_identity {
// check if the identity key matches the information provided during bonding
if expected_identity.as_str() != host_info.keys.ed25519_identity.to_base58_string() {
return Err(crate::error::Error::MismatchedIdentity {
node_id,
expected: expected_identity.clone(),
got: host_info.keys.ed25519_identity.to_base58_string(),
});
}
}
// check if the host information has been signed with the node's key
if verify_host_information && !host_info.verify_host_information() {
return Err(crate::error::Error::MissignedHostInformation { node_id });
}
Ok(host_info)
}
/// A nym-node API client bundled with the node's [`SignedHostInformation`],
/// if it was retrieved during the connection/verification phase.
///
/// This avoids a redundant second call to the `/host-information` endpoint
/// when the caller also needs the host information after obtaining the client.
pub struct ApiClientWithHostInformation {
pub client: crate::api::Client,
pub host_information: Option<SignedHostInformation>,
}
impl ApiClientWithHostInformation {
fn with_host_information(self, host_information: SignedHostInformation) -> Self {
Self {
host_information: Some(host_information),
..self
}
}
}
impl From<crate::api::Client> for ApiClientWithHostInformation {
fn from(client: crate::api::Client) -> Self {
Self {
client,
host_information: None,
}
}
}
/// Probe a nym-node's HTTP API and return a connected [`crate::api::Client`].
///
/// `base_host` is a hostname (e.g. `nymtech.net`) or IP address (e.g. `127.0.0.1`).
/// The function tries the following addresses in order, returning the first one whose
/// `/health` endpoint reports an "up" status:
///
/// 1. `http://<host>:<custom_port>` (only when `custom_port` is `Some`)
/// 2. `http://<host>:8080` — the standard nym-node API port
/// 3. `https://<host>` — node behind an HTTPS reverse proxy (port 443)
/// 4. `http://<host>` — node behind an HTTP reverse proxy (port 80)
///
/// This function is intended for infrastructure binaries (nym-api, network monitor, etc.),
/// not regular clients, which is why hickory DNS is explicitly disabled.
pub async fn try_get_valid_nym_node_api_client(
base_host: &str,
node_id: u32,
custom_port: Option<u16>,
user_agent: impl Into<UserAgent>,
) -> Result<crate::api::Client, crate::error::Error> {
// first try the standard port in case the operator didn't put the node behind the proxy,
// then default https (443)
// finally default http (80)
let mut addresses_to_try = vec![
format!("http://{base_host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node
format!("https://{base_host}"), // node behind https proxy (443)
format!("http://{base_host}"), // node behind http proxy (80)
];
// if a custom port was provided, try to connect to it first
if let Some(port) = custom_port {
addresses_to_try.insert(0, format!("http://{base_host}:{port}"));
}
let user_agent = user_agent.into();
for address in addresses_to_try {
// if provided base_host was malformed, there's no point in continuing
let client = match crate::api::Client::builder(address).and_then(|b| {
b.with_timeout(Duration::from_secs(5))
.no_hickory_dns()
.with_user_agent(user_agent.clone())
.build()
}) {
Ok(client) => client,
Err(err) => {
return Err(crate::error::Error::MalformedHost {
host: base_host.to_string(),
node_id,
source: Box::new(err),
});
}
};
if let Ok(health) = client.get_health().await
&& health.status.is_up()
{
return Ok(client);
}
}
Err(crate::error::Error::NoHttpPortsAvailable {
host: base_host.to_string(),
node_id,
})
}
}