lib.rs (17074B)
1 #![no_std] 2 #![forbid(unsafe_code)] 3 #![cfg_attr(coverage_nightly, feature(coverage_attribute))] 4 5 extern crate alloc; 6 7 mod contract; 8 mod message; 9 10 pub use contract::{ 11 RETICULUM_V1_MAX_PAYLOAD_BYTES, ReticulumCapabilityReportV1, ReticulumDestinationV1, 12 ReticulumDuplicateFragmentBehaviorV1, ReticulumFragmentIntegrityV1, ReticulumFragmentPolicyV1, 13 ReticulumFragmentationModeV1, ReticulumGatewaySemanticsV1, ReticulumPayloadPolicyV1, 14 ReticulumPrivacySemanticsV1, ReticulumRoutingMetadataV1, 15 }; 16 pub use message::{ 17 RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, 18 RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, 19 }; 20 21 use alloc::borrow::ToOwned; 22 use alloc::boxed::Box; 23 use alloc::string::String; 24 use alloc::vec::Vec; 25 use core::fmt; 26 use radroots_transport::capability::{ 27 Availability, Maturity, SinkCapabilities, SourceCapabilities, 28 }; 29 use radroots_transport::outcome::{DeliveryOutcome, FetchTargetOutcome, FetchTargetState}; 30 use radroots_transport::sink::{ 31 DeliveryReceipt, DeliveryRequest, DeliveryTargetReceipt, EventSink, SinkStatus, 32 }; 33 use radroots_transport::source::{EventSource, FetchPage, FetchRequest, NextPage, SourceStatus}; 34 use radroots_transport::target::TargetScope; 35 use radroots_transport::{Error as TransportError, Target, TransportId}; 36 37 const DEFAULT_PROFILE_ID: &str = "transport.reticulum.default"; 38 const RETICULUM_AGENT_ENDPOINT_PREFIX: &str = "reticulum-agent:"; 39 const UNAVAILABLE_CODE: &str = "transport_unavailable"; 40 41 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 42 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 43 pub enum RadrootsReticulumBehavior { 44 #[default] 45 RejectDeliveryAttempts, 46 DeferDeliveryPlans, 47 } 48 49 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 50 #[derive(Clone, Debug, PartialEq, Eq)] 51 pub struct RadrootsReticulumEndpoint { 52 uri: String, 53 } 54 55 impl RadrootsReticulumEndpoint { 56 pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsReticulumError> { 57 let uri = raw.as_ref(); 58 if uri != RADROOTS_RETICULUM_ENDPOINT_URI { 59 return Err(RadrootsReticulumError::InvalidEndpoint); 60 } 61 Ok(Self { 62 uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), 63 }) 64 } 65 66 pub fn as_str(&self) -> &str { 67 self.uri.as_str() 68 } 69 70 pub fn into_string(self) -> String { 71 self.uri 72 } 73 } 74 75 impl Default for RadrootsReticulumEndpoint { 76 fn default() -> Self { 77 Self::parse(RADROOTS_RETICULUM_ENDPOINT_URI).expect("default Reticulum endpoint") 78 } 79 } 80 81 impl fmt::Display for RadrootsReticulumEndpoint { 82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 83 f.write_str(self.uri.as_str()) 84 } 85 } 86 87 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 88 #[derive(Clone, Debug, PartialEq, Eq)] 89 pub struct RadrootsReticulumAgentEndpoint { 90 uri: String, 91 } 92 93 impl RadrootsReticulumAgentEndpoint { 94 pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsReticulumError> { 95 let uri = raw.as_ref(); 96 if uri.is_empty() 97 || uri != uri.trim() 98 || uri 99 .chars() 100 .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace()) 101 || !uri.starts_with(RETICULUM_AGENT_ENDPOINT_PREFIX) 102 || uri.len() == RETICULUM_AGENT_ENDPOINT_PREFIX.len() 103 { 104 return Err(RadrootsReticulumError::InvalidAgentEndpoint); 105 } 106 Ok(Self { 107 uri: uri.to_owned(), 108 }) 109 } 110 111 pub fn as_str(&self) -> &str { 112 self.uri.as_str() 113 } 114 115 pub fn into_string(self) -> String { 116 self.uri 117 } 118 } 119 120 impl fmt::Display for RadrootsReticulumAgentEndpoint { 121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 122 f.write_str(self.uri.as_str()) 123 } 124 } 125 126 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 127 #[derive(Clone, Debug, PartialEq, Eq)] 128 pub struct RadrootsReticulumProfile { 129 profile_id: String, 130 endpoint: RadrootsReticulumEndpoint, 131 scope: TargetScope, 132 agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, 133 behavior: RadrootsReticulumBehavior, 134 destination: ReticulumDestinationV1, 135 capability_report: ReticulumCapabilityReportV1, 136 } 137 138 impl RadrootsReticulumProfile { 139 pub fn new( 140 profile_id: impl Into<String>, 141 endpoint: RadrootsReticulumEndpoint, 142 scope: TargetScope, 143 agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, 144 behavior: RadrootsReticulumBehavior, 145 ) -> Result<Self, RadrootsReticulumError> { 146 let profile_id = profile_id.into(); 147 if profile_id.trim().is_empty() || profile_id.chars().any(char::is_whitespace) { 148 return Err(RadrootsReticulumError::InvalidProfileId); 149 } 150 let destination = ReticulumDestinationV1::new(endpoint.as_str(), scope.clone(), None) 151 .map_err(|_| RadrootsReticulumError::InvalidEndpoint)?; 152 let capability_report = ReticulumCapabilityReportV1 { 153 destination: destination.clone(), 154 payload_policy: ReticulumPayloadPolicyV1::v1(), 155 ..ReticulumCapabilityReportV1::unavailable_local() 156 }; 157 Ok(Self { 158 profile_id, 159 endpoint, 160 scope, 161 agent_endpoint, 162 behavior, 163 destination, 164 capability_report, 165 }) 166 } 167 168 pub fn deferred_until_implemented() -> Self { 169 let capability_report = ReticulumCapabilityReportV1::unavailable_local(); 170 Self { 171 profile_id: DEFAULT_PROFILE_ID.to_owned(), 172 endpoint: RadrootsReticulumEndpoint::default(), 173 scope: TargetScope::parse(RADROOTS_RETICULUM_SCOPE_ID).expect("Reticulum scope"), 174 agent_endpoint: None, 175 behavior: RadrootsReticulumBehavior::RejectDeliveryAttempts, 176 destination: capability_report.destination.clone(), 177 capability_report, 178 } 179 } 180 181 pub fn with_behavior(mut self, behavior: RadrootsReticulumBehavior) -> Self { 182 self.behavior = behavior; 183 self 184 } 185 186 pub fn profile_id(&self) -> &str { 187 self.profile_id.as_str() 188 } 189 190 pub fn endpoint(&self) -> &RadrootsReticulumEndpoint { 191 &self.endpoint 192 } 193 194 pub fn scope(&self) -> &TargetScope { 195 &self.scope 196 } 197 198 pub fn agent_endpoint(&self) -> Option<&RadrootsReticulumAgentEndpoint> { 199 self.agent_endpoint.as_ref() 200 } 201 202 pub fn with_agent_endpoint(mut self, agent_endpoint: RadrootsReticulumAgentEndpoint) -> Self { 203 self.agent_endpoint = Some(agent_endpoint); 204 self 205 } 206 207 pub fn behavior(&self) -> RadrootsReticulumBehavior { 208 self.behavior 209 } 210 211 pub fn destination(&self) -> &ReticulumDestinationV1 { 212 &self.destination 213 } 214 215 pub fn capability_report(&self) -> &ReticulumCapabilityReportV1 { 216 &self.capability_report 217 } 218 } 219 220 impl Default for RadrootsReticulumProfile { 221 fn default() -> Self { 222 Self::deferred_until_implemented() 223 } 224 } 225 226 #[derive(Clone, Debug, PartialEq, Eq)] 227 pub struct RadrootsReticulumTransport { 228 profile: RadrootsReticulumProfile, 229 } 230 231 impl RadrootsReticulumTransport { 232 pub fn new(profile: RadrootsReticulumProfile) -> Self { 233 Self { profile } 234 } 235 236 pub fn profile(&self) -> &RadrootsReticulumProfile { 237 &self.profile 238 } 239 } 240 241 impl Default for RadrootsReticulumTransport { 242 fn default() -> Self { 243 Self::new(RadrootsReticulumProfile::default()) 244 } 245 } 246 247 impl EventSink for RadrootsReticulumTransport { 248 fn status(&self) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> { 249 Box::pin(async { 250 Ok(SinkStatus::new( 251 TransportId::RETICULUM, 252 true, 253 Maturity::Preview, 254 Availability::Unavailable, 255 SinkCapabilities::NONE, 256 RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, 257 )) 258 }) 259 } 260 261 fn deliver( 262 &self, 263 request: DeliveryRequest, 264 ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, radroots_transport::SinkFailure>> 265 { 266 Box::pin(async move { 267 ensure_reticulum_targets(request.target_set().targets()) 268 .map_err(|_| radroots_transport::SinkFailure::invalid_contract(&request))?; 269 let outcome = DeliveryOutcome::unavailable() 270 .with_detail(UNAVAILABLE_CODE, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE) 271 .map_err(|_| radroots_transport::SinkFailure::invalid_contract(&request))?; 272 let receipts = request 273 .target_set() 274 .targets() 275 .iter() 276 .cloned() 277 .map(|target| DeliveryTargetReceipt::skipped(target, outcome.clone())) 278 .collect::<Result<Vec<_>, _>>() 279 .map_err(|_| radroots_transport::SinkFailure::invalid_contract(&request))?; 280 DeliveryReceipt::for_request(&request, receipts) 281 .map_err(|_| radroots_transport::SinkFailure::invalid_contract(&request)) 282 }) 283 } 284 } 285 286 impl EventSource for RadrootsReticulumTransport { 287 fn status(&self) -> radroots_transport::BoxFuture<'_, Result<SourceStatus, TransportError>> { 288 Box::pin(async { 289 Ok(SourceStatus::new( 290 TransportId::RETICULUM, 291 true, 292 Maturity::Preview, 293 Availability::Unavailable, 294 SourceCapabilities::NONE, 295 RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, 296 )) 297 }) 298 } 299 300 fn fetch( 301 &self, 302 request: FetchRequest, 303 ) -> radroots_transport::BoxFuture<'_, Result<FetchPage, TransportError>> { 304 Box::pin(async move { 305 ensure_reticulum_targets(request.target_set().targets()) 306 .map_err(reticulum_error_to_transport_error)?; 307 let outcomes = request 308 .target_set() 309 .targets() 310 .iter() 311 .map(|target| { 312 FetchTargetOutcome::new( 313 target.fingerprint().clone(), 314 FetchTargetState::Unavailable, 315 ) 316 .with_message(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE) 317 }) 318 .collect(); 319 FetchPage::for_request(&request, Vec::new(), outcomes, NextPage::Complete) 320 }) 321 } 322 } 323 324 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 325 pub enum RadrootsReticulumError { 326 InvalidEndpoint, 327 InvalidAgentEndpoint, 328 InvalidProfileId, 329 NonReticulumTarget, 330 } 331 332 impl fmt::Display for RadrootsReticulumError { 333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 334 f.write_str(match self { 335 Self::InvalidEndpoint => "invalid Reticulum endpoint", 336 Self::InvalidAgentEndpoint => "invalid Reticulum agent endpoint", 337 Self::InvalidProfileId => "invalid Reticulum profile id", 338 Self::NonReticulumTarget => "Reticulum transport received a non-Reticulum target", 339 }) 340 } 341 } 342 343 fn reticulum_error_to_transport_error(error: RadrootsReticulumError) -> TransportError { 344 match error { 345 RadrootsReticulumError::InvalidEndpoint | RadrootsReticulumError::NonReticulumTarget => { 346 TransportError::InvalidTargetUri 347 } 348 RadrootsReticulumError::InvalidAgentEndpoint | RadrootsReticulumError::InvalidProfileId => { 349 TransportError::InvalidTransportKind 350 } 351 } 352 } 353 354 fn ensure_reticulum_targets(targets: &[Target]) -> Result<(), RadrootsReticulumError> { 355 for target in targets { 356 if target.kind() != &TransportId::RETICULUM { 357 return Err(RadrootsReticulumError::NonReticulumTarget); 358 } 359 if target.uri().as_str() != RADROOTS_RETICULUM_ENDPOINT_URI { 360 return Err(RadrootsReticulumError::InvalidEndpoint); 361 } 362 if target.scope().is_none() { 363 return Err(RadrootsReticulumError::InvalidEndpoint); 364 } 365 } 366 Ok(()) 367 } 368 369 #[cfg(test)] 370 #[cfg_attr(coverage_nightly, coverage(off))] 371 mod tests { 372 use super::*; 373 use alloc::{format, vec}; 374 use futures::executor::block_on; 375 use radroots_event::{SignedEvent, wire::Nip01EventWire}; 376 use radroots_transport::{ 377 DeliveryRequest, EventSink, EventSource, FetchRequest, TargetSet, 378 outcome::DeliveryOutcomeKind, 379 policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}, 380 sink::DeliveryPayload, 381 source::FetchBounds, 382 }; 383 384 fn target() -> Target { 385 ReticulumDestinationV1::local() 386 .transport_target() 387 .expect("Reticulum target") 388 } 389 390 fn signed_event() -> SignedEvent { 391 let mut wire = Nip01EventWire { 392 id: "0".repeat(64), 393 pubkey: "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df".to_owned(), 394 created_at: 1_800_000_100, 395 kind: 1, 396 tags: vec![], 397 content: "reticulum-preview".to_owned(), 398 sig: "42".repeat(64), 399 extra: Default::default(), 400 }; 401 wire.id = wire.computed_event_id().expect("event id").to_hex(); 402 let raw = format!( 403 "{{\"id\":\"{}\",\"pubkey\":\"{}\",\"created_at\":{},\"kind\":{},\"tags\":[],\"content\":\"{}\",\"sig\":\"{}\"}}", 404 wire.id, wire.pubkey, wire.created_at, wire.kind, wire.content, wire.sig 405 ); 406 SignedEvent::from_wire_verified_id(wire, raw).expect("signed event") 407 } 408 409 #[test] 410 fn endpoints_and_profiles_preserve_validated_reticulum_contracts() { 411 let endpoint = RadrootsReticulumEndpoint::default(); 412 assert_eq!(endpoint.as_str(), RADROOTS_RETICULUM_ENDPOINT_URI); 413 assert_eq!(format!("{endpoint}"), RADROOTS_RETICULUM_ENDPOINT_URI); 414 assert!(RadrootsReticulumEndpoint::parse("reticulum:other").is_err()); 415 416 let agent = 417 RadrootsReticulumAgentEndpoint::parse("reticulum-agent:local").expect("agent endpoint"); 418 let profile = RadrootsReticulumProfile::new( 419 "transport.reticulum.farm", 420 endpoint, 421 TargetScope::parse("farm.mesh").expect("scope"), 422 Some(agent.clone()), 423 RadrootsReticulumBehavior::DeferDeliveryPlans, 424 ) 425 .expect("profile"); 426 assert_eq!(profile.agent_endpoint(), Some(&agent)); 427 assert_eq!( 428 profile.behavior(), 429 RadrootsReticulumBehavior::DeferDeliveryPlans 430 ); 431 assert_eq!( 432 profile.destination(), 433 &profile.capability_report().destination 434 ); 435 } 436 437 #[test] 438 fn canonical_source_and_sink_fail_closed_with_request_bound_evidence() { 439 let transport = RadrootsReticulumTransport::default(); 440 let sink_status = block_on(EventSink::status(&transport)).expect("sink status"); 441 let source_status = block_on(EventSource::status(&transport)).expect("source status"); 442 assert_eq!(sink_status.transport_id(), TransportId::RETICULUM); 443 assert_eq!(source_status.transport_id(), TransportId::RETICULUM); 444 assert_eq!(sink_status.availability(), Availability::Unavailable); 445 assert_eq!(source_status.availability(), Availability::Unavailable); 446 447 let targets = TargetSet::new(vec![target()]).expect("targets"); 448 let delivery = DeliveryRequest::new( 449 "reticulum-delivery", 450 DeliveryPayload::new(signed_event()), 451 targets.clone(), 452 SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()), 453 1_800_000_200_000, 454 ) 455 .expect("delivery request"); 456 let receipt = block_on(EventSink::deliver(&transport, delivery.clone())) 457 .expect("unavailable receipt"); 458 assert_eq!(receipt.target_receipts().len(), 1); 459 assert!(!receipt.target_receipts()[0].was_attempted()); 460 assert_eq!( 461 receipt.target_receipts()[0].outcome().kind(), 462 DeliveryOutcomeKind::Unavailable 463 ); 464 receipt 465 .validate_for_request(&delivery) 466 .expect("request bound"); 467 468 let fetch = FetchRequest::new( 469 "reticulum-fetch", 470 targets, 471 FetchBounds::new(10, 1_800_000_200_000).expect("bounds"), 472 ) 473 .expect("fetch request"); 474 let page = block_on(EventSource::fetch(&transport, fetch.clone())).expect("fetch page"); 475 assert!(page.events().is_empty()); 476 page.validate_for_request(&fetch).expect("request bound"); 477 } 478 479 #[test] 480 fn non_reticulum_targets_are_rejected_before_adapter_effects() { 481 let target = Target::nostr_relay("wss://relay.example").expect("Nostr target"); 482 assert_eq!( 483 ensure_reticulum_targets(&[target]), 484 Err(RadrootsReticulumError::NonReticulumTarget) 485 ); 486 assert_eq!( 487 reticulum_error_to_transport_error(RadrootsReticulumError::NonReticulumTarget), 488 TransportError::InvalidTargetUri 489 ); 490 } 491 }