1#![allow(clippy::disallowed_types)]
13use iroh::endpoint::Connection;
20use serde::Serialize;
21
22use crate::{
23 ffi::handles::{HandleStore, SessionEntry},
24 ffi::pumps::{pump_body_to_quic_send, pump_quic_recv_to_body},
25 parse_node_addr, CoreError, FfiDuplexStream, IrohEndpoint, ALPN_DUPLEX,
26};
27
28fn is_connection_closed(err: &iroh::endpoint::ConnectionError) -> bool {
31 use iroh::endpoint::ConnectionError::*;
32 matches!(
33 err,
34 ApplicationClosed(_) | ConnectionClosed(_) | Reset | TimedOut | LocallyClosed
35 )
36}
37
38#[derive(Debug, Clone, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct CloseInfo {
42 pub close_code: u64,
43 pub reason: String,
44}
45
46#[derive(Clone)]
52pub struct Session {
53 endpoint: IrohEndpoint,
54 handle: u64,
55}
56
57impl Session {
58 pub fn from_handle(endpoint: IrohEndpoint, handle: u64) -> Self {
64 Self { endpoint, handle }
65 }
66
67 pub fn handle(&self) -> u64 {
69 self.handle
70 }
71
72 pub async fn connect(
80 endpoint: IrohEndpoint,
81 remote_node_id: &str,
82 direct_addrs: Option<&[std::net::SocketAddr]>,
83 ) -> Result<Self, CoreError> {
84 let parsed = parse_node_addr(remote_node_id)?;
85 let node_id = parsed.node_id;
86 let mut addr = iroh::EndpointAddr::new(node_id);
87 for relay in parsed.relay_urls {
88 addr = addr.with_relay_url(relay);
89 }
90 for a in &parsed.direct_addrs {
91 addr = addr.with_ip_addr(*a);
92 }
93 if let Some(addrs) = direct_addrs {
94 for a in addrs {
95 addr = addr.with_ip_addr(*a);
96 }
97 }
98
99 let conn = endpoint
100 .raw()
101 .connect(addr, ALPN_DUPLEX)
102 .await
103 .map_err(|e| CoreError::connection_failed(format!("connect session: {e}")))?;
104
105 let handle = endpoint.handles().insert_session(SessionEntry { conn })?;
106
107 Ok(Self { endpoint, handle })
108 }
109
110 pub async fn accept(endpoint: IrohEndpoint) -> Result<Option<Self>, CoreError> {
115 let incoming = match endpoint.raw().accept().await {
116 Some(inc) => inc,
117 None => return Ok(None),
118 };
119
120 let conn = incoming
121 .await
122 .map_err(|e| CoreError::connection_failed(format!("accept session: {e}")))?;
123
124 let alpn = conn.alpn();
128 if alpn != ALPN_DUPLEX {
129 let detail = format!(
130 "accept session: unexpected ALPN {:?}",
131 String::from_utf8_lossy(alpn)
132 );
133 conn.close(0u32.into(), b"unexpected ALPN");
134 return Err(CoreError::connection_failed(detail));
135 }
136
137 let handle = endpoint.handles().insert_session(SessionEntry { conn })?;
138
139 Ok(Some(Self { endpoint, handle }))
140 }
141
142 fn conn(&self) -> Result<Connection, CoreError> {
143 self.endpoint
144 .handles()
145 .lookup_session(self.handle)
146 .map(|s| s.conn.clone())
147 .ok_or_else(|| CoreError::invalid_handle(self.handle))
148 }
149
150 pub fn remote_id(&self) -> Result<iroh::PublicKey, CoreError> {
152 self.conn().map(|c| c.remote_id())
153 }
154
155 pub async fn create_bidi_stream(&self) -> Result<FfiDuplexStream, CoreError> {
160 let conn = self.conn()?;
161
162 let (send, recv) = conn
163 .open_bi()
164 .await
165 .map_err(|e| CoreError::connection_failed(format!("open_bi: {e}")))?;
166
167 wrap_bidi_stream(self.endpoint.handles(), send, recv)
168 }
169
170 pub async fn next_bidi_stream(&self) -> Result<Option<FfiDuplexStream>, CoreError> {
175 let conn = self.conn()?;
176
177 match conn.accept_bi().await {
178 Ok((send, recv)) => Ok(Some(wrap_bidi_stream(self.endpoint.handles(), send, recv)?)),
179 Err(e) if is_connection_closed(&e) => Ok(None),
180 Err(e) => Err(CoreError::connection_failed(format!("accept_bi: {e}"))),
181 }
182 }
183
184 pub fn close(&self, close_code: u64, reason: &str) -> Result<(), CoreError> {
194 let conn = self.conn()?;
195 let code = iroh::endpoint::VarInt::from_u64(close_code).map_err(|_| {
196 CoreError::invalid_input(format!(
197 "close_code {close_code} exceeds QUIC VarInt max (2^62 - 1)"
198 ))
199 })?;
200 conn.close(code, reason.as_bytes());
201 Ok(())
202 }
203
204 pub async fn ready(&self) -> Result<(), CoreError> {
208 let _conn = self.conn()?;
210 Ok(())
213 }
214
215 pub async fn closed(&self) -> Result<CloseInfo, CoreError> {
223 let conn = match self.conn() {
224 Ok(c) => c,
225 Err(_) => {
226 return Ok(CloseInfo {
229 close_code: 0,
230 reason: String::new(),
231 });
232 }
233 };
234 let err = conn.closed().await;
235 self.endpoint.handles().remove_session(self.handle);
237 let (close_code, reason) = parse_connection_error(&err);
238 Ok(CloseInfo { close_code, reason })
239 }
240
241 pub async fn create_uni_stream(&self) -> Result<u64, CoreError> {
245 let conn = self.conn()?;
246 let send = conn
247 .open_uni()
248 .await
249 .map_err(|e| CoreError::connection_failed(format!("open_uni: {e}")))?;
250
251 let handles = self.endpoint.handles();
252 let (send_writer, send_reader) = handles.make_body_channel();
253 let write_handle = handles.insert_writer(send_writer)?;
254 tokio::spawn(pump_body_to_quic_send(send_reader, send));
255
256 Ok(write_handle)
257 }
258
259 pub async fn next_uni_stream(&self) -> Result<Option<u64>, CoreError> {
263 let conn = self.conn()?;
264
265 match conn.accept_uni().await {
266 Ok(recv) => {
267 let handles = self.endpoint.handles();
268 let (recv_writer, recv_reader) = handles.make_body_channel();
269 let read_handle = handles.insert_reader(recv_reader)?;
270 tokio::spawn(pump_quic_recv_to_body(recv, recv_writer));
271 Ok(Some(read_handle))
272 }
273 Err(e) if is_connection_closed(&e) => Ok(None),
274 Err(e) => Err(CoreError::connection_failed(format!("accept_uni: {e}"))),
275 }
276 }
277
278 pub fn send_datagram(&self, data: &[u8]) -> Result<(), CoreError> {
282 let conn = self.conn()?;
283 conn.send_datagram(bytes::Bytes::copy_from_slice(data))
284 .map_err(|e| match e {
285 iroh::endpoint::SendDatagramError::TooLarge => CoreError::body_too_large(
286 "datagram exceeds path MTU; check Session::max_datagram_size()",
287 ),
288 _ => CoreError::internal(format!("send_datagram: {e}")),
289 })
290 }
291
292 pub async fn recv_datagram(&self) -> Result<Option<Vec<u8>>, CoreError> {
296 let conn = self.conn()?;
297 match conn.read_datagram().await {
298 Ok(data) => Ok(Some(data.to_vec())),
299 Err(e) if is_connection_closed(&e) => Ok(None),
300 Err(e) => Err(CoreError::connection_failed(format!("recv_datagram: {e}"))),
301 }
302 }
303
304 pub fn max_datagram_size(&self) -> Result<Option<usize>, CoreError> {
308 let conn = self.conn()?;
309 Ok(conn.max_datagram_size())
310 }
311}
312
313fn wrap_bidi_stream(
317 handles: &HandleStore,
318 send: iroh::endpoint::SendStream,
319 recv: iroh::endpoint::RecvStream,
320) -> Result<FfiDuplexStream, CoreError> {
321 let mut guard = handles.insert_guard();
322
323 let (recv_writer, recv_reader) = handles.make_body_channel();
325 let read_handle = guard.insert_reader(recv_reader)?;
326 tokio::spawn(pump_quic_recv_to_body(recv, recv_writer));
327
328 let (send_writer, send_reader) = handles.make_body_channel();
330 let write_handle = guard.insert_writer(send_writer)?;
331 tokio::spawn(pump_body_to_quic_send(send_reader, send));
332
333 guard.commit();
334 Ok(FfiDuplexStream {
335 read_handle,
336 write_handle,
337 })
338}
339
340fn parse_connection_error(err: &iroh::endpoint::ConnectionError) -> (u64, String) {
342 match err {
343 iroh::endpoint::ConnectionError::ApplicationClosed(info) => {
344 let code: u64 = info.error_code.into();
345 let reason = String::from_utf8_lossy(&info.reason).into_owned();
346 (code, reason)
347 }
348 other => (0, other.to_string()),
349 }
350}