1#![deny(unsafe_code)]
6
7pub mod client;
8pub mod endpoint;
9pub(crate) mod io;
10pub(crate) mod pool;
11pub mod registry;
12pub mod server;
13pub mod session;
14pub mod stream;
15
16pub use client::{fetch, raw_connect};
17#[cfg(feature = "compression")]
18pub use endpoint::CompressionOptions;
19pub use endpoint::{
20 parse_direct_addrs, ConnectionEvent, DiscoveryOptions, EndpointStats, IrohEndpoint,
21 NetworkingOptions, NodeAddrInfo, NodeOptions, PathInfo, PeerStats, PoolOptions,
22 StreamingOptions,
23};
24pub use registry::{get_endpoint, insert_endpoint, remove_endpoint};
25pub use server::respond;
26pub use server::serve;
27pub use server::serve_with_events;
28pub use server::ServeHandle;
29pub use server::ServerLimits;
30pub use session::{
31 session_accept, session_close, session_closed, session_connect, session_create_bidi_stream,
32 session_create_uni_stream, session_max_datagram_size, session_next_bidi_stream,
33 session_next_uni_stream, session_ready, session_recv_datagram, session_remote_id,
34 session_send_datagram, CloseInfo,
35};
36pub use stream::{BodyReader, HandleStore, StoreConfig};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum ErrorCode {
46 InvalidInput,
47 ConnectionFailed,
48 Timeout,
49 BodyTooLarge,
50 HeaderTooLarge,
51 PeerRejected,
52 Cancelled,
53 Internal,
54}
55
56#[derive(Debug, Clone)]
60pub struct CoreError {
61 pub code: ErrorCode,
62 pub message: String,
63}
64
65impl CoreError {
66 pub fn invalid_input(detail: impl std::fmt::Display) -> Self {
67 CoreError {
68 code: ErrorCode::InvalidInput,
69 message: detail.to_string(),
70 }
71 }
72 pub fn connection_failed(detail: impl std::fmt::Display) -> Self {
73 CoreError {
74 code: ErrorCode::ConnectionFailed,
75 message: detail.to_string(),
76 }
77 }
78 pub fn timeout(detail: impl std::fmt::Display) -> Self {
79 CoreError {
80 code: ErrorCode::Timeout,
81 message: detail.to_string(),
82 }
83 }
84 pub fn body_too_large(detail: impl std::fmt::Display) -> Self {
85 CoreError {
86 code: ErrorCode::BodyTooLarge,
87 message: detail.to_string(),
88 }
89 }
90 pub fn header_too_large(detail: impl std::fmt::Display) -> Self {
91 CoreError {
92 code: ErrorCode::HeaderTooLarge,
93 message: detail.to_string(),
94 }
95 }
96 pub fn peer_rejected(detail: impl std::fmt::Display) -> Self {
97 CoreError {
98 code: ErrorCode::PeerRejected,
99 message: detail.to_string(),
100 }
101 }
102 pub fn internal(detail: impl std::fmt::Display) -> Self {
103 CoreError {
104 code: ErrorCode::Internal,
105 message: detail.to_string(),
106 }
107 }
108 pub fn invalid_handle(handle: u64) -> Self {
109 CoreError {
110 code: ErrorCode::InvalidInput,
111 message: format!("unknown handle: {handle}"),
112 }
113 }
114 pub fn cancelled() -> Self {
115 CoreError {
116 code: ErrorCode::Cancelled,
117 message: "aborted".to_string(),
118 }
119 }
120}
121
122impl std::fmt::Display for CoreError {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 write!(f, "{:?}: {}", self.code, self.message)
125 }
126}
127
128impl std::error::Error for CoreError {}
129
130pub const ALPN: &[u8] = b"iroh-http/2";
134pub const ALPN_DUPLEX: &[u8] = b"iroh-http/2-duplex";
136
137pub(crate) type BoxBody =
141 http_body_util::combinators::BoxBody<bytes::Bytes, std::convert::Infallible>;
142
143pub(crate) fn box_body<B>(body: B) -> BoxBody
145where
146 B: http_body::Body<Data = bytes::Bytes, Error = std::convert::Infallible>
147 + Send
148 + Sync
149 + 'static,
150{
151 use http_body_util::BodyExt;
152 body.map_err(|_| unreachable!()).boxed()
153}
154
155pub fn secret_key_sign(secret_key_bytes: &[u8; 32], data: &[u8]) -> Result<[u8; 64], CoreError> {
160 std::panic::catch_unwind(|| {
161 let key = iroh::SecretKey::from_bytes(secret_key_bytes);
162 key.sign(data).to_bytes()
163 })
164 .map_err(|_| CoreError::internal("secret_key_sign panicked"))
165}
166
167pub fn public_key_verify(public_key_bytes: &[u8; 32], data: &[u8], sig_bytes: &[u8; 64]) -> bool {
170 std::panic::catch_unwind(|| {
171 let Ok(key) = iroh::PublicKey::from_bytes(public_key_bytes) else {
172 return false;
173 };
174 let sig = iroh::Signature::from_bytes(sig_bytes);
175 key.verify(data, &sig).is_ok()
176 })
177 .unwrap_or(false)
178}
179
180pub fn generate_secret_key() -> Result<[u8; 32], CoreError> {
182 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
183 iroh::SecretKey::generate(&mut rand::rng()).to_bytes()
184 }))
185 .map_err(|_| CoreError::internal("generate_secret_key panicked"))
186}
187
188pub fn base32_encode(bytes: &[u8]) -> String {
192 base32::encode(base32::Alphabet::Rfc4648Lower { padding: false }, bytes)
193}
194
195pub(crate) fn base32_decode(s: &str) -> Result<Vec<u8>, String> {
197 base32::decode(base32::Alphabet::Rfc4648Lower { padding: false }, s)
198 .ok_or_else(|| format!("invalid base32 string: {s}"))
199}
200
201pub(crate) fn parse_node_id(s: &str) -> Result<iroh::PublicKey, CoreError> {
203 let bytes = base32_decode(s).map_err(CoreError::invalid_input)?;
204 let arr: [u8; 32] = bytes
205 .try_into()
206 .map_err(|_| CoreError::invalid_input("node-id must be 32 bytes"))?;
207 iroh::PublicKey::from_bytes(&arr).map_err(|e| CoreError::invalid_input(e.to_string()))
208}
209
210pub fn node_ticket(ep: &IrohEndpoint) -> Result<String, CoreError> {
217 let info = ep.node_addr();
218 serde_json::to_string(&info)
219 .map_err(|e| CoreError::internal(format!("failed to serialize node ticket: {e}")))
220}
221
222pub struct ParsedNodeAddr {
224 pub node_id: iroh::PublicKey,
225 pub direct_addrs: Vec<std::net::SocketAddr>,
226}
227
228pub fn parse_node_addr(s: &str) -> Result<ParsedNodeAddr, CoreError> {
236 if let Ok(info) = serde_json::from_str::<NodeAddrInfo>(s) {
237 let node_id = parse_node_id(&info.id)?;
238 let mut direct_addrs = Vec::new();
239 for addr_str in &info.addrs {
240 if addr_str.contains("://") {
242 continue;
243 }
244 let addr = addr_str
245 .parse::<std::net::SocketAddr>()
246 .map_err(|_| CoreError::invalid_input(format!("malformed address: {addr_str}")))?;
247 direct_addrs.push(addr);
248 }
249 return Ok(ParsedNodeAddr {
250 node_id,
251 direct_addrs,
252 });
253 }
254 let node_id = parse_node_id(s)?;
255 Ok(ParsedNodeAddr {
256 node_id,
257 direct_addrs: Vec::new(),
258 })
259}
260
261#[derive(Debug, Clone)]
265pub struct FfiResponse {
266 pub status: u16,
267 pub headers: Vec<(String, String)>,
268 pub body_handle: u64,
270 pub url: String,
272 pub trailers_handle: u64,
274}
275
276#[derive(Debug)]
278pub struct RequestPayload {
279 pub req_handle: u64,
280 pub req_body_handle: u64,
281 pub res_body_handle: u64,
282 pub req_trailers_handle: u64,
283 pub res_trailers_handle: u64,
284 pub method: String,
285 pub url: String,
286 pub headers: Vec<(String, String)>,
287 pub remote_node_id: String,
288 pub is_bidi: bool,
289}
290
291#[derive(Debug)]
293pub struct FfiDuplexStream {
294 pub read_handle: u64,
295 pub write_handle: u64,
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn base32_round_trip() {
304 let original: Vec<u8> = (0..32).collect();
305 let encoded = base32_encode(&original);
306 let decoded = base32_decode(&encoded).unwrap();
307 assert_eq!(decoded, original);
308 }
309
310 #[test]
311 fn base32_empty() {
312 let encoded = base32_encode(&[]);
313 assert_eq!(encoded, "");
314 let decoded = base32_decode("").unwrap();
315 assert!(decoded.is_empty());
316 }
317
318 #[test]
319 fn base32_decode_invalid_char() {
320 let result = base32_decode("!!!invalid!!!");
321 assert!(result.is_err());
322 }
323
324 #[test]
325 fn parse_node_id_invalid_base32() {
326 let result = parse_node_id("!!!not-base32!!!");
327 assert!(result.is_err());
328 }
329
330 #[test]
331 fn parse_node_id_wrong_length() {
332 let result = parse_node_id("aa");
333 assert!(result.is_err());
334 }
335
336 #[test]
337 fn core_error_display() {
338 let e = CoreError::timeout("30s elapsed");
339 assert!(e.to_string().contains("Timeout"));
340 assert!(e.to_string().contains("30s elapsed"));
341 }
342}