1#![deny(unsafe_code)]
16
17pub mod endpoint;
18
19pub(crate) mod ffi;
20pub(crate) mod http;
21
22pub mod events {
24 pub use crate::http::events::*;
25}
26pub mod registry {
27 pub use crate::ffi::registry::*;
28}
29
30pub use http::body::{Body, BoxError};
32pub use http::client::{fetch_request, FetchError};
33pub use http::server::{serve, serve_with_events, RemoteNodeId, ServeHandle, ServeOptions};
34
35pub use ffi::dispatcher::{ffi_serve, ffi_serve_with_callback, respond};
37pub use ffi::fetch::fetch;
38#[allow(clippy::disallowed_types)] pub use ffi::handles::{
40 make_body_channel, BodyReader, HandleStore, ResponseHeadEntry, StoreConfig,
41};
42pub use ffi::session::{CloseInfo, Session};
43#[allow(clippy::disallowed_types)] pub use ffi::types::{FfiDuplexStream, FfiResponse, RequestPayload};
45
46pub use endpoint::{
48 parse_direct_addrs, ConnectionEvent, DiscoveryOptions, EndpointStats, IrohEndpoint,
49 NetworkingOptions, NodeAddrInfo, NodeOptions, PathInfo, PeerStats, PoolOptions,
50 StreamingOptions,
51};
52pub use events::TransportEvent;
53pub use http::server::stack::{CompressionOptions, StackConfig};
54pub use registry::{get_endpoint, insert_endpoint, remove_endpoint};
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum ErrorCode {
64 InvalidInput,
65 ConnectionFailed,
66 Timeout,
67 BodyTooLarge,
68 HeaderTooLarge,
69 PeerRejected,
70 Cancelled,
71 Internal,
72}
73
74#[derive(Debug, Clone, thiserror::Error)]
78#[error("{code:?}: {message}")]
79pub struct CoreError {
80 pub code: ErrorCode,
81 pub message: String,
82}
83
84impl CoreError {
85 pub fn invalid_input(detail: impl std::fmt::Display) -> Self {
86 CoreError {
87 code: ErrorCode::InvalidInput,
88 message: detail.to_string(),
89 }
90 }
91 pub fn connection_failed(detail: impl std::fmt::Display) -> Self {
92 CoreError {
93 code: ErrorCode::ConnectionFailed,
94 message: detail.to_string(),
95 }
96 }
97 pub fn timeout(detail: impl std::fmt::Display) -> Self {
98 CoreError {
99 code: ErrorCode::Timeout,
100 message: detail.to_string(),
101 }
102 }
103 pub fn body_too_large(detail: impl std::fmt::Display) -> Self {
104 CoreError {
105 code: ErrorCode::BodyTooLarge,
106 message: detail.to_string(),
107 }
108 }
109 pub fn header_too_large(detail: impl std::fmt::Display) -> Self {
110 CoreError {
111 code: ErrorCode::HeaderTooLarge,
112 message: detail.to_string(),
113 }
114 }
115 pub fn peer_rejected(detail: impl std::fmt::Display) -> Self {
116 CoreError {
117 code: ErrorCode::PeerRejected,
118 message: detail.to_string(),
119 }
120 }
121 pub fn internal(detail: impl std::fmt::Display) -> Self {
122 CoreError {
123 code: ErrorCode::Internal,
124 message: detail.to_string(),
125 }
126 }
127 pub fn invalid_handle(handle: u64) -> Self {
128 CoreError {
129 code: ErrorCode::InvalidInput,
130 message: format!("unknown handle: {handle}"),
131 }
132 }
133 pub fn cancelled() -> Self {
134 CoreError {
135 code: ErrorCode::Cancelled,
136 message: "aborted".to_string(),
137 }
138 }
139}
140
141pub const ALPN: &[u8] = b"iroh-http/2";
145pub const ALPN_DUPLEX: &[u8] = b"iroh-http/2-duplex";
147
148pub const ALPN_STR: &str = "iroh-http/2";
150pub const ALPN_DUPLEX_STR: &str = "iroh-http/2-duplex";
152
153pub const KNOWN_ALPNS: &[&str] = &[ALPN_STR, ALPN_DUPLEX_STR];
155
156pub fn secret_key_sign(secret_key_bytes: &[u8; 32], data: &[u8]) -> Result<[u8; 64], CoreError> {
161 std::panic::catch_unwind(|| {
162 let key = iroh::SecretKey::from_bytes(secret_key_bytes);
163 key.sign(data).to_bytes()
164 })
165 .map_err(|_| CoreError::internal("secret_key_sign panicked"))
166}
167
168pub fn public_key_verify(public_key_bytes: &[u8; 32], data: &[u8], sig_bytes: &[u8; 64]) -> bool {
171 std::panic::catch_unwind(|| {
172 let Ok(key) = iroh::PublicKey::from_bytes(public_key_bytes) else {
173 return false;
174 };
175 let sig = iroh::Signature::from_bytes(sig_bytes);
176 key.verify(data, &sig).is_ok()
177 })
178 .unwrap_or(false)
179}
180
181pub fn generate_secret_key() -> Result<[u8; 32], CoreError> {
183 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
184 iroh::SecretKey::generate().to_bytes()
185 }))
186 .map_err(|_| CoreError::internal("generate_secret_key panicked"))
187}
188
189pub fn base32_encode(bytes: &[u8]) -> String {
193 base32::encode(base32::Alphabet::Rfc4648Lower { padding: false }, bytes)
194}
195
196pub(crate) fn base32_decode(s: &str) -> Result<Vec<u8>, String> {
198 base32::decode(base32::Alphabet::Rfc4648Lower { padding: false }, s)
199 .ok_or_else(|| format!("invalid base32 string: {s}"))
200}
201
202pub(crate) fn parse_node_id(s: &str) -> Result<iroh::PublicKey, CoreError> {
204 let bytes = base32_decode(s).map_err(CoreError::invalid_input)?;
205 let arr: [u8; 32] = bytes
206 .try_into()
207 .map_err(|_| CoreError::invalid_input("node-id must be 32 bytes"))?;
208 iroh::PublicKey::from_bytes(&arr).map_err(|e| CoreError::invalid_input(e.to_string()))
209}
210
211pub fn node_ticket(ep: &IrohEndpoint) -> Result<String, CoreError> {
218 let info = ep.node_addr();
219 serde_json::to_string(&info)
220 .map_err(|e| CoreError::internal(format!("failed to serialize node ticket: {e}")))
221}
222
223pub struct ParsedNodeAddr {
225 pub node_id: iroh::PublicKey,
226 pub direct_addrs: Vec<std::net::SocketAddr>,
227}
228
229pub fn parse_node_addr(s: &str) -> Result<ParsedNodeAddr, CoreError> {
237 if let Ok(info) = serde_json::from_str::<NodeAddrInfo>(s) {
238 let node_id = parse_node_id(&info.id)?;
239 let mut direct_addrs = Vec::new();
240 for addr_str in &info.addrs {
241 if addr_str.contains("://") {
243 continue;
244 }
245 let addr = addr_str
246 .parse::<std::net::SocketAddr>()
247 .map_err(|_| CoreError::invalid_input(format!("malformed address: {addr_str}")))?;
248 direct_addrs.push(addr);
249 }
250 return Ok(ParsedNodeAddr {
251 node_id,
252 direct_addrs,
253 });
254 }
255 let node_id = parse_node_id(s)?;
256 Ok(ParsedNodeAddr {
257 node_id,
258 direct_addrs: Vec::new(),
259 })
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn base32_round_trip() {
268 let original: Vec<u8> = (0..32).collect();
269 let encoded = base32_encode(&original);
270 let decoded = base32_decode(&encoded).unwrap();
271 assert_eq!(decoded, original);
272 }
273
274 #[test]
275 fn base32_empty() {
276 let encoded = base32_encode(&[]);
277 assert_eq!(encoded, "");
278 let decoded = base32_decode("").unwrap();
279 assert!(decoded.is_empty());
280 }
281
282 #[test]
283 fn base32_decode_invalid_char() {
284 let result = base32_decode("!!!invalid!!!");
285 assert!(result.is_err());
286 }
287
288 #[test]
289 fn parse_node_id_invalid_base32() {
290 let result = parse_node_id("!!!not-base32!!!");
291 assert!(result.is_err());
292 }
293
294 #[test]
295 fn parse_node_id_wrong_length() {
296 let result = parse_node_id("aa");
297 assert!(result.is_err());
298 }
299
300 #[test]
301 fn core_error_display() {
302 let e = CoreError::timeout("30s elapsed");
303 assert!(e.to_string().contains("Timeout"));
304 assert!(e.to_string().contains("30s elapsed"));
305 }
306}