hoprd_api/
peers.rs

1use axum::{
2    extract::{Json, Path, State},
3    http::status::StatusCode,
4    response::IntoResponse,
5};
6use serde::{Deserialize, Serialize};
7use serde_with::{serde_as, DisplayFromStr, DurationMilliSeconds};
8use std::sync::Arc;
9use tracing::debug;
10
11use hopr_lib::errors::{HoprLibError, HoprStatusError};
12use hopr_lib::{HoprTransportError, Multiaddr};
13
14use crate::{
15    types::{HoprIdentifier, PeerOrAddress},
16    ApiError, ApiErrorStatus, InternalState, BASE_PATH,
17};
18
19#[serde_as]
20#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
21#[schema(example = json!({
22    "announced": [
23        "/ip4/10.0.2.100/tcp/19093"
24    ],
25    "observed": [
26        "/ip4/10.0.2.100/tcp/19093"
27    ]
28}))]
29pub(crate) struct NodePeerInfoResponse {
30    #[serde_as(as = "Vec<DisplayFromStr>")]
31    #[schema(value_type = Vec<String>)]
32    announced: Vec<Multiaddr>,
33    #[serde_as(as = "Vec<DisplayFromStr>")]
34    #[schema(value_type = Vec<String>)]
35    observed: Vec<Multiaddr>,
36}
37
38#[serde_as]
39#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
40#[serde(rename_all = "camelCase")]
41pub(crate) struct DestinationParams {
42    #[serde_as(as = "DisplayFromStr")]
43    #[schema(value_type = String)]
44    destination: PeerOrAddress,
45}
46
47/// Returns transport-related information about the given peer.
48///
49/// This includes the peer ids that the given peer has `announced` on-chain
50/// and peer ids that are actually `observed` by the transport layer.
51#[utoipa::path(
52    get,
53    path = const_format::formatcp!("{BASE_PATH}/peers/{{destination}}"),
54    params(
55        ("destination" = String, Path, description = "PeerID or address of the requested peer")
56    ),
57    responses(
58        (status = 200, description = "Peer information fetched successfully.", body = NodePeerInfoResponse),
59        (status = 400, description = "Invalid destination", body = ApiError),
60        (status = 401, description = "Invalid authorization token.", body = ApiError),
61        (status = 422, description = "Unknown failure", body = ApiError)
62    ),
63    security(
64        ("api_token" = []),
65        ("bearer_token" = [])
66    ),
67    tag = "Peers",
68)]
69pub(super) async fn show_peer_info(
70    Path(DestinationParams { destination }): Path<DestinationParams>,
71    State(state): State<Arc<InternalState>>,
72) -> impl IntoResponse {
73    let hopr = state.hopr.clone();
74
75    match HoprIdentifier::new_with(destination, hopr.peer_resolver()).await {
76        Ok(destination) => Ok((
77            StatusCode::OK,
78            Json(NodePeerInfoResponse {
79                announced: hopr.multiaddresses_announced_on_chain(&destination.peer_id).await,
80                observed: hopr.network_observed_multiaddresses(&destination.peer_id).await,
81            }),
82        )),
83        Err(e) => Err(e.into_response()),
84    }
85}
86
87#[serde_as]
88#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
89#[schema(example = json!({
90    "latency": 200,
91    "reportedVersion": "2.1.0"
92}))]
93#[serde(rename_all = "camelCase")]
94pub(crate) struct PingResponse {
95    #[serde_as(as = "DurationMilliSeconds<u64>")]
96    #[schema(value_type = u64)]
97    latency: std::time::Duration,
98    reported_version: String,
99}
100
101/// Directly pings the given peer.
102#[utoipa::path(
103    post,
104    path = const_format::formatcp!("{BASE_PATH}/peers/{{destination}}/ping"),
105    params(
106        ("destination" = String, Path, description = "PeerID or address of the requested peer")
107    ),
108    responses(
109        (status = 200, description = "Ping successful", body = PingResponse),
110        (status = 400, description = "Invalid peer id", body = ApiError),
111        (status = 401, description = "Invalid authorization token.", body = ApiError),
112        (status = 404, description = "Peer id not found in the network.", body = ApiError),
113        (status = 408, description = "Peer timed out.", body = ApiError),
114        (status = 412, description = "The node is not ready."),
115        (status = 422, description = "Unknown failure", body = ApiError)
116    ),
117    security(
118        ("api_token" = []),
119        ("bearer_token" = [])
120    ),
121    tag = "Peers",
122)]
123pub(super) async fn ping_peer(
124    Path(DestinationParams { destination }): Path<DestinationParams>,
125    State(state): State<Arc<InternalState>>,
126) -> Result<impl IntoResponse, ApiError> {
127    debug!(%destination, "Manually ping peer");
128
129    let hopr = state.hopr.clone();
130
131    match HoprIdentifier::new_with(destination, hopr.peer_resolver()).await {
132        Ok(destination) => match hopr.ping(&destination.peer_id).await {
133            Ok((latency, status)) => {
134                let resp = Json(PingResponse {
135                    latency: latency / 2,
136                    reported_version: status.peer_version.unwrap_or("unknown".into()),
137                });
138                Ok((StatusCode::OK, resp).into_response())
139            }
140            Err(HoprLibError::TransportError(HoprTransportError::Protocol(hopr_lib::ProtocolError::Timeout))) => {
141                Ok((StatusCode::REQUEST_TIMEOUT, ApiErrorStatus::Timeout).into_response())
142            }
143            Err(HoprLibError::TransportError(HoprTransportError::NetworkError(
144                hopr_lib::NetworkingError::Timeout(_),
145            ))) => Ok((StatusCode::REQUEST_TIMEOUT, ApiErrorStatus::Timeout).into_response()),
146            Err(HoprLibError::TransportError(HoprTransportError::NetworkError(
147                hopr_lib::NetworkingError::PingerError(_, e),
148            ))) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::PingError(e)).into_response()),
149            Err(HoprLibError::TransportError(HoprTransportError::NetworkError(
150                hopr_lib::NetworkingError::NonExistingPeer,
151            ))) => Ok((StatusCode::NOT_FOUND, ApiErrorStatus::PeerNotFound).into_response()),
152            Err(HoprLibError::StatusError(HoprStatusError::NotThereYet(_, _))) => {
153                Ok((StatusCode::PRECONDITION_FAILED, ApiErrorStatus::NotReady).into_response())
154            }
155            Err(e) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response()),
156        },
157        Err(e) => Ok((StatusCode::UNPROCESSABLE_ENTITY, e).into_response()),
158    }
159}