1use std::time::Duration;
4
5use anyhow::{Context, Result, bail};
6use iroh::{
7 Endpoint, EndpointAddr, RelayMode,
8 endpoint::{Connection, presets},
9};
10use thread_api::{
11 Remote, contract::EndpointKind, credentials::Credentials, transport::IrohTransport,
12};
13
14use crate::{ConnectionOptions, descriptor_url, fetch_endpoint_descriptor};
15
16const ENDPOINT_CLOSE_TIMEOUT: Duration = Duration::from_secs(3);
17
18async fn close_endpoint(endpoint: &Endpoint) {
19 let _ = tokio::time::timeout(ENDPOINT_CLOSE_TIMEOUT, endpoint.close()).await;
20}
21
22pub struct HostedClient {
25 pub remote: Remote<IrohTransport<Credentials>>,
26 endpoint: Endpoint,
27 connection: Connection,
28}
29
30impl HostedClient {
31 pub async fn connect_server(server: &str, options: &ConnectionOptions) -> Result<Self> {
34 let descriptor = fetch_endpoint_descriptor(
35 &descriptor_url(server)?,
36 &options.trusted_descriptors,
37 &options.http_client,
38 )
39 .await?;
40 Self::connect_endpoint(
41 descriptor.endpoint_addr()?,
42 options.credential.clone(),
43 options.timeout,
44 options.tls_ca_certificate_pem.as_deref(),
45 )
46 .await
47 }
48
49 pub async fn connect_endpoint(
52 address: EndpointAddr,
53 credential: Credentials,
54 timeout: Duration,
55 tls_ca_certificate_pem: Option<&str>,
56 ) -> Result<Self> {
57 Self::connect_endpoint_with_description(
58 address,
59 credential,
60 timeout,
61 tls_ca_certificate_pem,
62 None,
63 )
64 .await
65 }
66
67 pub async fn connect_endpoint_with_description(
70 address: EndpointAddr,
71 credential: Credentials,
72 timeout: Duration,
73 tls_ca_certificate_pem: Option<&str>,
74 description: Option<thread_api::contract::DescribeEndpointResponse>,
75 ) -> Result<Self> {
76 if timeout.is_zero() {
77 bail!("hosted progress timeout must be positive");
78 }
79 let endpoint = Self::bind_endpoint(tls_ca_certificate_pem).await?;
80 Self::connect_bound_endpoint(
81 endpoint,
82 address,
83 credential,
84 timeout,
85 description,
86 tls_ca_certificate_pem,
87 )
88 .await
89 }
90
91 pub async fn bind_endpoint(tls_ca_certificate_pem: Option<&str>) -> Result<Endpoint> {
94 Self::bind_with_relays(RelayMode::Disabled, tls_ca_certificate_pem).await
95 }
96
97 async fn bind_with_relays(
98 relay_mode: RelayMode,
99 tls_ca_certificate_pem: Option<&str>,
100 ) -> Result<Endpoint> {
101 let mut builder = Endpoint::builder(presets::Minimal).relay_mode(relay_mode);
102 if let Some(pem) = tls_ca_certificate_pem {
103 builder = builder.ca_tls_config(crate::relay_tls::ca_tls_config_from_pem(pem)?);
104 }
105 builder.bind().await.context("bind hosted client endpoint")
106 }
107
108 pub async fn connect_bound_endpoint(
110 mut endpoint: Endpoint,
111 address: EndpointAddr,
112 credential: Credentials,
113 timeout: Duration,
114 description: Option<thread_api::contract::DescribeEndpointResponse>,
115 tls_ca_certificate_pem: Option<&str>,
116 ) -> Result<Self> {
117 if timeout.is_zero() {
118 close_endpoint(&endpoint).await;
119 bail!("hosted progress timeout must be positive");
120 }
121 let relays: Vec<_> = address.relay_urls().cloned().collect();
122 if !relays.is_empty() {
123 close_endpoint(&endpoint).await;
127 endpoint =
128 Self::bind_with_relays(RelayMode::custom(relays), tls_ca_certificate_pem).await?;
129 }
130 heddle_perf_contract::record_network_client_initialization();
131 let connection =
132 match tokio::time::timeout(timeout, endpoint.connect(address, api::HOSTED_ALPN_V1))
133 .await
134 .context("hosted connection deadline elapsed")
135 .and_then(|connection| connection.context("connect to hosted endpoint"))
136 {
137 Ok(connection) => connection,
138 Err(error) => {
139 close_endpoint(&endpoint).await;
140 return Err(error);
141 }
142 };
143 let result = async {
144 let transport = IrohTransport::new(
145 connection.clone(),
146 credential,
147 api::framing::MAX_CONTROL_BODY,
148 timeout,
149 )?;
150 if let Some(description) = description {
151 let key = connection.remote_id();
154 if description.endpoint.as_ref().is_some_and(|source| {
155 source.public_key == key.as_bytes() && source.kind == EndpointKind::Weft as i32
156 }) && description
157 .supported_packages
158 .iter()
159 .any(|package| package == "heddle.api.v1alpha2")
160 {
161 return Remote::from_description(
162 transport,
163 *key.as_bytes(),
164 EndpointKind::Weft,
165 description,
166 )
167 .context("reuse hosted v2 API");
168 }
169 }
170 Remote::discover(
171 transport,
172 *connection.remote_id().as_bytes(),
173 EndpointKind::Weft,
174 )
175 .await
176 .context("discover hosted v2 API")
177 }
178 .await;
179 match result {
180 Ok(remote) => Ok(Self {
181 remote,
182 endpoint,
183 connection,
184 }),
185 Err(error) => {
186 connection.close(0u32.into(), b"v2 discovery failed");
187 close_endpoint(&endpoint).await;
188 Err(error)
189 }
190 }
191 }
192
193 pub fn local_endpoint_id(&self) -> iroh::EndpointId {
194 self.endpoint.id()
195 }
196
197 pub fn endpoint(&self) -> &Endpoint {
198 &self.endpoint
199 }
200
201 pub fn connection(&self) -> &Connection {
202 &self.connection
203 }
204
205 pub fn into_parts(self) -> (Remote<IrohTransport<Credentials>>, Endpoint, Connection) {
209 let this = std::mem::ManuallyDrop::new(self);
210 unsafe {
213 (
214 std::ptr::read(&this.remote),
215 std::ptr::read(&this.endpoint),
216 std::ptr::read(&this.connection),
217 )
218 }
219 }
220
221 pub async fn close(&self) {
223 self.connection.close(0u32.into(), b"hosted client closed");
224 close_endpoint(&self.endpoint).await;
225 }
226}
227
228impl Drop for HostedClient {
229 fn drop(&mut self) {
230 self.connection.close(0u32.into(), b"hosted client dropped");
231 }
232}