272 lines
8.5 KiB
Rust
272 lines
8.5 KiB
Rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
use crate::nym_contract_cache::cache::NymContractCache;
|
|
use crate::support::caching::cache::{SharedCache, UninitialisedCache};
|
|
use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher};
|
|
use crate::support::config;
|
|
use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE;
|
|
use futures_util::{stream, StreamExt};
|
|
use nym_api_requests::models::{
|
|
IpPacketRouterDetails, NetworkRequesterDetails, NymNodeDescription,
|
|
};
|
|
use nym_config::defaults::{mainnet, DEFAULT_NYM_NODE_HTTP_PORT};
|
|
use nym_contracts_common::IdentityKey;
|
|
use nym_mixnet_contract_common::Gateway;
|
|
use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt};
|
|
use std::collections::HashMap;
|
|
use thiserror::Error;
|
|
|
|
// type alias for ease of use
|
|
pub type DescribedNodes = HashMap<IdentityKey, NymNodeDescription>;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum NodeDescribeCacheError {
|
|
#[error("contract cache hasn't been initialised")]
|
|
UninitialisedContractCache {
|
|
#[from]
|
|
source: UninitialisedCache,
|
|
},
|
|
|
|
#[error("gateway {gateway} has provided malformed host information ({host}: {source}")]
|
|
MalformedHost {
|
|
host: String,
|
|
|
|
gateway: IdentityKey,
|
|
|
|
#[source]
|
|
source: NymNodeApiClientError,
|
|
},
|
|
|
|
#[error("gateway '{gateway}' with host '{host}' doesn't seem to expose any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)]
|
|
NoHttpPortsAvailable { host: String, gateway: IdentityKey },
|
|
|
|
#[error("failed to query gateway '{gateway}': {source}")]
|
|
ApiFailure {
|
|
gateway: IdentityKey,
|
|
|
|
#[source]
|
|
source: NymNodeApiClientError,
|
|
},
|
|
|
|
// TODO: perhaps include more details here like whether key/signature/payload was malformed
|
|
#[error("could not verify signed host information for gateway '{gateway}'")]
|
|
MissignedHostInformation { gateway: IdentityKey },
|
|
}
|
|
|
|
pub struct NodeDescriptionProvider {
|
|
// for now we only care about gateways, nothing more
|
|
// network_gateways: SharedCache<Vec<GatewayBond>>,
|
|
contract_cache: NymContractCache,
|
|
|
|
batch_size: usize,
|
|
}
|
|
|
|
impl NodeDescriptionProvider {
|
|
pub(crate) fn new(contract_cache: NymContractCache) -> NodeDescriptionProvider {
|
|
NodeDescriptionProvider {
|
|
contract_cache,
|
|
batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub(crate) fn with_batch_size(mut self, batch_size: usize) -> Self {
|
|
self.batch_size = batch_size;
|
|
self
|
|
}
|
|
}
|
|
|
|
async fn try_get_client(
|
|
gateway: &Gateway,
|
|
) -> Result<nym_node_requests::api::Client, NodeDescribeCacheError> {
|
|
let gateway_host = &gateway.host;
|
|
|
|
// 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 addresses_to_try = vec![
|
|
format!("http://{gateway_host}:{DEFAULT_NYM_NODE_HTTP_PORT}"),
|
|
format!("https://{gateway_host}"),
|
|
format!("http://{gateway_host}"),
|
|
];
|
|
|
|
for address in addresses_to_try {
|
|
// if provided host was malformed, no point in continuing
|
|
let client = match nym_node_requests::api::Client::new_url(address, None) {
|
|
Ok(client) => client,
|
|
Err(err) => {
|
|
return Err(NodeDescribeCacheError::MalformedHost {
|
|
host: gateway_host.clone(),
|
|
gateway: gateway.identity_key.clone(),
|
|
source: err,
|
|
});
|
|
}
|
|
};
|
|
if let Ok(health) = client.get_health().await {
|
|
if health.status.is_up() {
|
|
return Ok(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(NodeDescribeCacheError::NoHttpPortsAvailable {
|
|
host: gateway_host.clone(),
|
|
gateway: gateway.identity_key.clone(),
|
|
})
|
|
}
|
|
|
|
async fn get_gateway_description(
|
|
gateway: Gateway,
|
|
) -> Result<(IdentityKey, NymNodeDescription), NodeDescribeCacheError> {
|
|
let client = try_get_client(&gateway).await?;
|
|
|
|
let host_info =
|
|
client
|
|
.get_host_information()
|
|
.await
|
|
.map_err(|err| NodeDescribeCacheError::ApiFailure {
|
|
gateway: gateway.identity_key.clone(),
|
|
source: err,
|
|
})?;
|
|
|
|
if !host_info.verify_host_information() {
|
|
return Err(NodeDescribeCacheError::MissignedHostInformation {
|
|
gateway: gateway.identity_key,
|
|
});
|
|
}
|
|
|
|
let build_info =
|
|
client
|
|
.get_build_information()
|
|
.await
|
|
.map_err(|err| NodeDescribeCacheError::ApiFailure {
|
|
gateway: gateway.identity_key.clone(),
|
|
source: err,
|
|
})?;
|
|
|
|
let websockets =
|
|
client
|
|
.get_mixnet_websockets()
|
|
.await
|
|
.map_err(|err| NodeDescribeCacheError::ApiFailure {
|
|
gateway: gateway.identity_key.clone(),
|
|
source: err,
|
|
})?;
|
|
|
|
let network_requester =
|
|
if let Ok(nr) = client.get_network_requester().await {
|
|
let exit_policy = client.get_exit_policy().await.map_err(|err| {
|
|
NodeDescribeCacheError::ApiFailure {
|
|
gateway: gateway.identity_key.clone(),
|
|
source: err,
|
|
}
|
|
})?;
|
|
let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL;
|
|
|
|
Some(NetworkRequesterDetails {
|
|
address: nr.address,
|
|
uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy,
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let ip_packet_router = if let Ok(ipr) = client.get_ip_packet_router().await {
|
|
Some(IpPacketRouterDetails {
|
|
address: ipr.address,
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let description = NymNodeDescription {
|
|
host_information: host_info.data,
|
|
build_information: build_info,
|
|
network_requester,
|
|
ip_packet_router,
|
|
mixnet_websockets: websockets,
|
|
};
|
|
|
|
Ok((gateway.identity_key, description))
|
|
}
|
|
|
|
#[async_trait]
|
|
impl CacheItemProvider for NodeDescriptionProvider {
|
|
type Item = HashMap<IdentityKey, NymNodeDescription>;
|
|
type Error = NodeDescribeCacheError;
|
|
|
|
async fn wait_until_ready(&self) {
|
|
self.contract_cache.wait_for_initial_values().await
|
|
}
|
|
|
|
async fn try_refresh(&self) -> Result<Self::Item, Self::Error> {
|
|
let gateways = self.contract_cache.gateways_all().await;
|
|
|
|
// let guard = self.network_gateways.get().await?;
|
|
// let gateways = &*guard;
|
|
|
|
if gateways.is_empty() {
|
|
return Ok(HashMap::new());
|
|
}
|
|
|
|
// TODO: somehow bypass the 'higher-ranked lifetime error' and remove that redundant clone
|
|
let websockets = stream::iter(
|
|
gateways
|
|
// .deref()
|
|
// .clone()
|
|
.into_iter()
|
|
.map(|bond| bond.gateway)
|
|
.map(get_gateway_description),
|
|
)
|
|
.buffer_unordered(self.batch_size)
|
|
.filter_map(|res| async move {
|
|
match res {
|
|
Ok((identity, description)) => Some((identity, description)),
|
|
Err(err) => {
|
|
debug!("{err}");
|
|
None
|
|
}
|
|
}
|
|
})
|
|
.collect::<HashMap<_, _>>()
|
|
.await;
|
|
|
|
Ok(websockets)
|
|
}
|
|
}
|
|
|
|
// currently dead code : (
|
|
#[allow(dead_code)]
|
|
pub(crate) fn new_refresher(
|
|
config: &config::TopologyCacher,
|
|
contract_cache: NymContractCache,
|
|
// hehe. we can't do that yet
|
|
// network_gateways: SharedCache<Vec<GatewayBond>>,
|
|
) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> {
|
|
CacheRefresher::new(
|
|
Box::new(
|
|
NodeDescriptionProvider::new(contract_cache)
|
|
.with_batch_size(config.debug.node_describe_batch_size),
|
|
),
|
|
config.debug.node_describe_caching_interval,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn new_refresher_with_initial_value(
|
|
config: &config::TopologyCacher,
|
|
contract_cache: NymContractCache,
|
|
// hehe. we can't do that yet
|
|
// network_gateways: SharedCache<Vec<GatewayBond>>,
|
|
initial: SharedCache<DescribedNodes>,
|
|
) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> {
|
|
CacheRefresher::new_with_initial_value(
|
|
Box::new(
|
|
NodeDescriptionProvider::new(contract_cache)
|
|
.with_batch_size(config.debug.node_describe_batch_size),
|
|
),
|
|
config.debug.node_describe_caching_interval,
|
|
initial,
|
|
)
|
|
}
|