Skip to main content

endhost_api_client/
client.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! # Endhost API client
15//!
16//! An [EndhostApiClient] provides the application with the information
17//! necessary to send and receive SCION-packets in the routing domain that is
18//! associated with the endhost API.
19//!
20//! The implementation [CrpcEndhostApiClient] is a concrete implementation
21//! following the current specification of the endhost-API.
22//!
23//! ## Example Usage
24//!
25//! ```no_run
26//! use std::{net::SocketAddr, str::FromStr};
27//!
28//! use endhost_api_client::client::{CrpcEndhostApiClient, EndhostApiClient};
29//! use sciparse::identifier::isd_asn::IsdAsn;
30//!
31//! pub async fn get_all_udp_sockaddrs() -> anyhow::Result<Vec<SocketAddr>> {
32//!     let crpc_client =
33//!         CrpcEndhostApiClient::new(&url::Url::parse("http://10.0.0.1:48080/").unwrap())?;
34//!
35//!     let res = crpc_client
36//!         .list_underlays(IsdAsn::from_str("1-ff00:0:110").unwrap())
37//!         .await?
38//!         .udp_underlay
39//!         .iter()
40//!         .map(|router| router.internal_interface)
41//!         .collect();
42//!
43//!     Ok(res)
44//! }
45//! ```
46use std::{ops::Deref, sync::Arc};
47
48use endhost_api::routes::{
49    ENDHOST_API_V1, LIST_SEGMENTS, LIST_UNDERLAYS, SEGMENTS_SERVICE, UNDERLAY_SERVICE,
50};
51use endhost_api_models::underlays::Underlays;
52use endhost_api_protobuf::v1::{
53    ListSegmentsRequest, ListSegmentsResponse, ListUnderlaysRequest, ListUnderlaysResponse,
54};
55use scion_sdk_reqwest_connect_rpc::{
56    client::{CrpcClient, CrpcClientError},
57    token_source::TokenSource,
58};
59use sciparse::{
60    identifier::isd_asn::IsdAsn,
61    segment::{SegmentsPage, rpc::InvalidSegmentError},
62};
63
64/// Endhost API client trait.
65///
66/// This allows for a client mock implementation in tests.
67#[async_trait::async_trait]
68pub trait EndhostApiClient: Send + Sync {
69    /// List the available underlays for a given ISD-AS.
70    ///
71    /// # Arguments
72    /// * `isd_as` - The ISD-AS to list the underlays for. For a wildcard ISD AS
73    ///   (`IsdAsn::WILDCARD`), all existing underlays will be returned.
74    ///
75    /// # Returns
76    /// A future that resolves to the list of underlays.
77    async fn list_underlays(&self, isd_as: IsdAsn) -> Result<Underlays, CrpcClientError>;
78    /// List the available segments between a source and destination ISD-AS.
79    ///
80    /// # Arguments
81    /// * `src` - The source ISD-AS.
82    /// * `dst` - The destination ISD-AS.
83    /// * `page_size` - The maximum number of segments to return.
84    /// * `page_token` - The token to use for pagination.
85    async fn list_segments(
86        &self,
87        src: IsdAsn,
88        dst: IsdAsn,
89        page_size: i32,
90        page_token: String,
91    ) -> Result<SegmentsPage, CrpcClientError>;
92}
93
94/// Connect RPC endhost API client.
95pub struct CrpcEndhostApiClient {
96    client: CrpcClient,
97}
98
99impl Deref for CrpcEndhostApiClient {
100    type Target = CrpcClient;
101
102    fn deref(&self) -> &Self::Target {
103        &self.client
104    }
105}
106
107impl CrpcEndhostApiClient {
108    /// Creates a new endhost API client from the given base URL.
109    pub fn new(base_url: &url::Url) -> anyhow::Result<Self> {
110        Ok(CrpcEndhostApiClient {
111            client: CrpcClient::new(base_url)?,
112        })
113    }
114
115    /// Creates a new endhost API client from the given base URL and [`reqwest::Client`].
116    pub fn new_with_client(base_url: &url::Url, client: reqwest::Client) -> anyhow::Result<Self> {
117        Ok(CrpcEndhostApiClient {
118            client: CrpcClient::new_with_client(base_url, client)?,
119        })
120    }
121
122    /// Uses the provided token source for authentication.
123    pub fn use_token_source(&mut self, token_source: Arc<dyn TokenSource>) -> &mut Self {
124        self.client.use_token_source(token_source);
125        self
126    }
127}
128
129#[async_trait::async_trait]
130impl EndhostApiClient for CrpcEndhostApiClient {
131    async fn list_underlays(&self, isd_as: IsdAsn) -> Result<Underlays, CrpcClientError> {
132        self.client
133            .unary_request::<ListUnderlaysRequest, ListUnderlaysResponse>(
134                &format!("{ENDHOST_API_V1}.{UNDERLAY_SERVICE}{LIST_UNDERLAYS}"),
135                ListUnderlaysRequest {
136                    isd_as: Some(isd_as.into()),
137                },
138            )
139            .await?
140            .try_into()
141            .map_err(|e: url::ParseError| {
142                CrpcClientError::DecodeError {
143                    context: "parsing underlay address as URL".into(),
144                    source: Some(e.into()),
145                    body: None,
146                }
147            })
148            .inspect(|resp| {
149                tracing::debug!(%resp, "Listed underlays");
150            })
151    }
152
153    async fn list_segments(
154        &self,
155        src: IsdAsn,
156        dst: IsdAsn,
157        page_size: i32,
158        page_token: String,
159    ) -> Result<SegmentsPage, CrpcClientError> {
160        self.client
161            .unary_request::<ListSegmentsRequest, ListSegmentsResponse>(
162                &format!("{ENDHOST_API_V1}.{SEGMENTS_SERVICE}{LIST_SEGMENTS}"),
163                ListSegmentsRequest {
164                    src_isd_as: src.0,
165                    dst_isd_as: dst.0,
166                    page_size,
167                    page_token,
168                },
169            )
170            .await?
171            .try_into()
172            .map_err(|e: InvalidSegmentError| {
173                CrpcClientError::DecodeError {
174                    context: "decoding segments".into(),
175                    source: Some(e.into()),
176                    body: None,
177                }
178            })
179            .inspect(|resp: &SegmentsPage| {
180                tracing::debug!(
181                    core=?resp.segments.core_segments.len(),
182                    down=?resp.segments.down_segments.len(),
183                    up=?resp.segments.up_segments.len(),
184                    "Listed segments"
185                );
186            })
187    }
188}