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() -> Result<Vec<SocketAddr>, Box<dyn std::error::Error>> {
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 reqwest_connect_rpc::{
56 client::{CrpcClient, CrpcClientCreationError, CrpcClientError},
57 token_source::TokenSource,
58};
59use sciparse::{identifier::isd_asn::IsdAsn, rpc::FromRpcError, segment::SegmentsPage};
60
61/// Endhost API client trait.
62///
63/// This allows for a client mock implementation in tests.
64#[async_trait::async_trait]
65pub trait EndhostApiClient: Send + Sync {
66 /// List the available underlays for a given ISD-AS.
67 ///
68 /// # Arguments
69 /// * `isd_as` - The ISD-AS to list the underlays for. For a wildcard ISD AS
70 /// (`IsdAsn::WILDCARD`), all existing underlays will be returned.
71 ///
72 /// # Returns
73 /// A future that resolves to the list of underlays.
74 async fn list_underlays(&self, isd_as: IsdAsn) -> Result<Underlays, CrpcClientError>;
75 /// List the available segments between a source and destination ISD-AS.
76 ///
77 /// # Arguments
78 /// * `src` - The source ISD-AS.
79 /// * `dst` - The destination ISD-AS.
80 /// * `page_size` - The maximum number of segments to return.
81 /// * `page_token` - The token to use for pagination.
82 async fn list_segments(
83 &self,
84 src: IsdAsn,
85 dst: IsdAsn,
86 page_size: i32,
87 page_token: String,
88 ) -> Result<SegmentsPage, CrpcClientError>;
89}
90
91/// Connect RPC endhost API client.
92pub struct CrpcEndhostApiClient {
93 client: CrpcClient,
94}
95
96impl Deref for CrpcEndhostApiClient {
97 type Target = CrpcClient;
98
99 fn deref(&self) -> &Self::Target {
100 &self.client
101 }
102}
103
104impl CrpcEndhostApiClient {
105 /// Creates a new endhost API client from the given base URL.
106 pub fn new(base_url: &url::Url) -> Result<Self, CrpcClientCreationError> {
107 Ok(CrpcEndhostApiClient {
108 client: CrpcClient::new(base_url)?,
109 })
110 }
111
112 /// Creates a new endhost API client from the given base URL and [`reqwest::Client`].
113 pub fn new_with_client(
114 base_url: &url::Url,
115 client: reqwest::Client,
116 ) -> Result<Self, CrpcClientCreationError> {
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: FromRpcError| {
173 CrpcClientError::DecodeError {
174 context: "failed 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}