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 scion_proto::address::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//! ```
46
47use std::{ops::Deref, sync::Arc};
48
49use endhost_api::routes::{
50 ENDHOST_API_V1, LIST_PATHS, LIST_UNDERLAYS, PATH_SERVICE, UNDERLAY_SERVICE,
51};
52use endhost_api_models::underlays::Underlays;
53use endhost_api_protobuf::endhost::api_service::v1::{
54 ListSegmentsRequest, ListSegmentsResponse, ListUnderlaysRequest, ListUnderlaysResponse,
55};
56use scion_proto::{address::IsdAsn, path::segment::Segments};
57use scion_sdk_reqwest_connect_rpc::{
58 client::{CrpcClient, CrpcClientError},
59 token_source::TokenSource,
60};
61
62/// Endhost API client trait.
63///
64/// This allows for a client mock implementation in tests.
65#[async_trait::async_trait]
66pub trait EndhostApiClient: Send + Sync {
67 /// List the available underlays for a given ISD-AS.
68 ///
69 /// # Arguments
70 /// * `isd_as` - The ISD-AS to list the underlays for. For a wildcard ISD AS
71 /// (`IsdAsn::WILDCARD`), all existing underlays will be returned.
72 ///
73 /// # Returns
74 /// A future that resolves to the list of underlays.
75 async fn list_underlays(&self, isd_as: IsdAsn) -> Result<Underlays, CrpcClientError>;
76 /// List the available segments between a source and destination ISD-AS.
77 ///
78 /// # Arguments
79 /// * `src` - The source ISD-AS.
80 /// * `dst` - The destination ISD-AS.
81 /// * `page_size` - The maximum number of segments to return.
82 /// * `page_token` - The token to use for pagination.
83 async fn list_segments(
84 &self,
85 src: IsdAsn,
86 dst: IsdAsn,
87 page_size: i32,
88 page_token: String,
89 ) -> Result<Segments, CrpcClientError>;
90}
91
92/// Connect RPC endhost API client.
93pub struct CrpcEndhostApiClient {
94 client: CrpcClient,
95}
96
97impl Deref for CrpcEndhostApiClient {
98 type Target = CrpcClient;
99
100 fn deref(&self) -> &Self::Target {
101 &self.client
102 }
103}
104
105impl CrpcEndhostApiClient {
106 /// Creates a new endhost API client from the given base URL.
107 pub fn new(base_url: &url::Url) -> anyhow::Result<Self> {
108 Ok(CrpcEndhostApiClient {
109 client: CrpcClient::new(base_url)?,
110 })
111 }
112
113 /// Creates a new endhost API client from the given base URL and [`reqwest::Client`].
114 pub fn new_with_client(base_url: &url::Url, client: reqwest::Client) -> anyhow::Result<Self> {
115 Ok(CrpcEndhostApiClient {
116 client: CrpcClient::new_with_client(base_url, client)?,
117 })
118 }
119
120 /// Uses the provided token source for authentication.
121 pub fn use_token_source(&mut self, token_source: Arc<dyn TokenSource>) -> &mut Self {
122 self.client.use_token_source(token_source);
123 self
124 }
125}
126
127#[async_trait::async_trait]
128impl EndhostApiClient for CrpcEndhostApiClient {
129 async fn list_underlays(&self, isd_as: IsdAsn) -> Result<Underlays, CrpcClientError> {
130 self.client
131 .unary_request::<ListUnderlaysRequest, ListUnderlaysResponse>(
132 &format!("{ENDHOST_API_V1}.{UNDERLAY_SERVICE}{LIST_UNDERLAYS}"),
133 ListUnderlaysRequest {
134 isd_as: Some(isd_as.into()),
135 },
136 )
137 .await?
138 .try_into()
139 .map_err(|e: url::ParseError| {
140 CrpcClientError::DecodeError {
141 context: "parsing underlay address as URL".into(),
142 source: e.into(),
143 body: None,
144 }
145 })
146 .inspect(|resp| {
147 tracing::debug!(%resp, "Listed underlays");
148 })
149 }
150
151 async fn list_segments(
152 &self,
153 src: IsdAsn,
154 dst: IsdAsn,
155 page_size: i32,
156 page_token: String,
157 ) -> Result<Segments, CrpcClientError> {
158 self.client
159 .unary_request::<ListSegmentsRequest, ListSegmentsResponse>(
160 &format!("{ENDHOST_API_V1}.{PATH_SERVICE}{LIST_PATHS}"),
161 ListSegmentsRequest {
162 src_isd_as: src.0,
163 dst_isd_as: dst.0,
164 page_size,
165 page_token,
166 },
167 )
168 .await?
169 .try_into()
170 .map_err(
171 |e: scion_proto::path::convert::segment::InvalidSegmentError| {
172 CrpcClientError::DecodeError {
173 context: "decoding segments".into(),
174 source: e.into(),
175 body: None,
176 }
177 },
178 )
179 .inspect(|resp| {
180 tracing::debug!(%resp, "Listed segments");
181 })
182 }
183}