1use std::collections::HashMap;
46use std::sync::{Arc, Mutex};
47use std::time::{Duration, Instant};
48
49use coap_lite::{CoapOption, MessageClass, MessageType, Packet, RequestType};
50pub use pamoja_core::Message;
51use pamoja_core::{Error, Receive, Result, Transport};
52use tokio::net::UdpSocket;
53use tokio::sync::{mpsc, oneshot};
54use tokio::task::JoinHandle;
55
56mod server;
57
58pub use server::{CoapPublisher, CoapServer};
59
60type PendingAcks = Arc<Mutex<HashMap<u16, oneshot::Sender<Answer>>>>;
62
63type Observations = Arc<Mutex<HashMap<Vec<u8>, Observation>>>;
65
66enum Answer {
68 Acknowledged(Reply),
70 Reset,
72}
73
74struct Reply {
76 code: MessageClass,
78 payload: Vec<u8>,
80 observe: Option<u32>,
82}
83
84struct Observation {
86 path: String,
88 freshest: Option<(u32, Instant)>,
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum Reliability {
97 NonConfirmable,
99 Confirmable,
101}
102
103#[derive(Clone, Debug)]
108pub struct CoapConfig {
109 host: String,
110 port: u16,
111 bind: String,
112 reliability: Reliability,
113 ack_timeout: Duration,
114 max_retransmits: u32,
115}
116
117impl CoapConfig {
118 pub fn new(host: impl Into<String>, port: u16) -> Self {
131 Self {
132 host: host.into(),
133 port,
134 bind: "0.0.0.0:0".to_owned(),
135 reliability: Reliability::Confirmable,
136 ack_timeout: Duration::from_secs(2),
137 max_retransmits: 4,
138 }
139 }
140
141 pub fn bind(mut self, addr: impl Into<String>) -> Self {
152 self.bind = addr.into();
153 self
154 }
155
156 pub fn reliability(mut self, reliability: Reliability) -> Self {
166 self.reliability = reliability;
167 self
168 }
169
170 pub fn ack_timeout(mut self, timeout: Duration) -> Self {
185 self.ack_timeout = timeout;
186 self
187 }
188
189 pub fn max_retransmits(mut self, count: u32) -> Self {
199 self.max_retransmits = count;
200 self
201 }
202}
203
204pub struct CoapTransport {
211 config: CoapConfig,
212 socket: Option<Arc<UdpSocket>>,
213 incoming: Option<mpsc::UnboundedReceiver<Message>>,
214 pending: PendingAcks,
215 observations: Observations,
216 pump: Option<JoinHandle<()>>,
217 next_id: u16,
218}
219
220impl CoapTransport {
221 pub fn new(config: CoapConfig) -> Self {
231 Self {
232 config,
233 socket: None,
234 incoming: None,
235 pending: Arc::new(Mutex::new(HashMap::new())),
236 observations: Arc::new(Mutex::new(HashMap::new())),
237 pump: None,
238 next_id: random() as u16,
239 }
240 }
241
242 pub fn is_connected(&self) -> bool {
249 self.socket.is_some()
250 }
251
252 pub async fn disconnect(&mut self) -> Result<()> {
264 if let Some(pump) = self.pump.take() {
265 pump.abort();
266 }
267 self.socket = None;
268 self.incoming = None;
269 self.observations.lock().expect("observations lock").clear();
270 Ok(())
271 }
272
273 fn next_message_id(&mut self) -> u16 {
279 let id = self.next_id;
280 self.next_id = self.next_id.wrapping_add(1);
281 id
282 }
283
284 fn next_request_token(&mut self) -> Vec<u8> {
290 random().to_be_bytes()[..TOKEN_LENGTH].to_vec()
291 }
292
293 async fn exchange(&mut self, id: u16, bytes: &[u8], socket: &UdpSocket) -> Result<Reply> {
300 let spread = (random() >> 11) as f64 / (1u64 << 53) as f64;
301 let mut timeout = self
302 .config
303 .ack_timeout
304 .mul_f64(1.0 + spread * (ACK_RANDOM_FACTOR - 1.0));
305 for _ in 0..=self.config.max_retransmits {
306 let (tx, rx) = oneshot::channel();
307 self.pending.lock().expect("pending lock").insert(id, tx);
308 if let Err(error) = socket.send(bytes).await {
309 if !refused(&error) {
310 return Err(Error::Transport(error.to_string()));
311 }
312 }
313 match tokio::time::timeout(timeout, rx).await {
314 Ok(Ok(Answer::Acknowledged(reply))) => return Ok(reply),
315 Ok(Ok(Answer::Reset)) => {
316 return Err(Error::Transport(
317 "the server reset the request: it arrived, but the server could not \
318 process it"
319 .into(),
320 ))
321 }
322 Ok(Err(_)) => return Err(Error::Closed),
323 Err(_) => {
324 self.pending.lock().expect("pending lock").remove(&id);
325 timeout = timeout.saturating_mul(2);
326 }
327 }
328 }
329 let sent = self.config.max_retransmits + 1;
330 let times = if sent == 1 {
331 "transmission"
332 } else {
333 "transmissions"
334 };
335 Err(Error::Transport(format!(
336 "no acknowledgment after {sent} {times}"
337 )))
338 }
339}
340
341const TOKEN_LENGTH: usize = 4;
343
344const ACK_RANDOM_FACTOR: f64 = 1.5;
346
347fn refused(error: &std::io::Error) -> bool {
355 matches!(
356 error.kind(),
357 std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::ConnectionReset
358 )
359}
360
361fn random() -> u64 {
366 use std::hash::{BuildHasher, RandomState};
367 RandomState::new().hash_one(std::time::SystemTime::now())
368}
369
370fn answered(code: MessageClass, diagnostic: &[u8]) -> Result<()> {
376 let raw = u8::from(code);
377 let class = raw >> 5;
378 if class != 4 && class != 5 {
379 return Ok(());
380 }
381 let detail = raw & 0x1F;
382 let name = response_name(class, detail)
383 .map(|name| format!(" {name}"))
384 .unwrap_or_default();
385 let reason = match core::str::from_utf8(diagnostic) {
386 Ok(text) if !text.is_empty() => format!(": {text}"),
387 _ => String::new(),
388 };
389 Err(Error::Transport(format!(
390 "the server answered {class}.{detail:02}{name}{reason}"
391 )))
392}
393
394fn response_name(class: u8, detail: u8) -> Option<&'static str> {
396 Some(match (class, detail) {
397 (4, 0) => "Bad Request",
398 (4, 1) => "Unauthorized",
399 (4, 2) => "Bad Option",
400 (4, 3) => "Forbidden",
401 (4, 4) => "Not Found",
402 (4, 5) => "Method Not Allowed",
403 (4, 6) => "Not Acceptable",
404 (4, 12) => "Precondition Failed",
405 (4, 13) => "Request Entity Too Large",
406 (4, 15) => "Unsupported Content-Format",
407 (5, 0) => "Internal Server Error",
408 (5, 1) => "Not Implemented",
409 (5, 2) => "Bad Gateway",
410 (5, 3) => "Service Unavailable",
411 (5, 4) => "Gateway Timeout",
412 (5, 5) => "Proxying Not Supported",
413 _ => return None,
414 })
415}
416
417impl Transport for CoapTransport {
418 async fn connect(&mut self) -> Result<()> {
419 let server = tokio::net::lookup_host((self.config.host.as_str(), self.config.port))
420 .await
421 .map_err(|err| Error::Transport(err.to_string()))?
422 .next()
423 .ok_or_else(|| Error::Transport(format!("could not resolve {}", self.config.host)))?;
424
425 let socket = UdpSocket::bind(&self.config.bind)
426 .await
427 .map_err(|err| Error::Transport(err.to_string()))?;
428 socket
429 .connect(server)
430 .await
431 .map_err(|err| Error::Transport(err.to_string()))?;
432 let socket = Arc::new(socket);
433
434 let (tx, rx) = mpsc::unbounded_channel();
435 let pending = Arc::clone(&self.pending);
436 let observations = Arc::clone(&self.observations);
437 let pump_socket = Arc::clone(&socket);
438 let pump = tokio::spawn(async move {
439 let mut buf = vec![0u8; 1500];
440 loop {
441 let len = match pump_socket.recv(&mut buf).await {
442 Ok(len) => len,
443 Err(error) if refused(&error) => continue,
444 Err(_) => break,
445 };
446 let Ok(packet) = Packet::from_bytes(&buf[..len]) else {
447 continue;
448 };
449 if !dispatch(packet, &pending, &observations, &tx, &pump_socket).await {
450 break;
451 }
452 }
453 });
454
455 self.socket = Some(socket);
456 self.incoming = Some(rx);
457 self.pump = Some(pump);
458 Ok(())
459 }
460
461 async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
462 let socket = self.socket.clone().ok_or(Error::Closed)?;
463 let id = self.next_message_id();
464 let token = self.next_request_token();
465
466 let mut packet = Packet::new();
467 packet.header.set_version(1);
468 packet
469 .header
470 .set_type(message_type(self.config.reliability));
471 packet.header.code = MessageClass::Request(RequestType::Put);
472 packet.header.message_id = id;
473 packet.set_token(token);
474 add_path(&mut packet, topic);
475 packet.payload = payload.to_vec();
476
477 let bytes = packet
478 .to_bytes()
479 .map_err(|err| Error::Codec(err.to_string()))?;
480
481 match self.config.reliability {
482 Reliability::NonConfirmable => match socket.send(&bytes).await {
483 Err(error) if !refused(&error) => Err(Error::Transport(error.to_string())),
484 _ => Ok(()),
485 },
486 Reliability::Confirmable => {
487 let reply = self.exchange(id, &bytes, &socket).await?;
488 answered(reply.code, &reply.payload)
489 }
490 }
491 }
492
493 async fn subscribe(&mut self, topic: &str) -> Result<()> {
513 let socket = self.socket.clone().ok_or(Error::Closed)?;
514 let id = self.next_message_id();
515 let path = topic.trim_matches('/');
516 let observed = self
517 .observations
518 .lock()
519 .expect("observations lock")
520 .iter()
521 .find(|(_, observation)| observation.path == path)
522 .map(|(token, _)| token.clone());
523 let token = observed.unwrap_or_else(|| self.next_request_token());
524
525 let mut packet = Packet::new();
526 packet.header.set_version(1);
527 packet.header.set_type(MessageType::Confirmable);
528 packet.header.code = MessageClass::Request(RequestType::Get);
529 packet.header.message_id = id;
530 packet.set_token(token.clone());
531 packet.add_option(CoapOption::Observe, Vec::new());
533 add_path(&mut packet, topic);
534
535 let bytes = packet
536 .to_bytes()
537 .map_err(|err| Error::Codec(err.to_string()))?;
538
539 self.observations.lock().expect("observations lock").insert(
540 token.clone(),
541 Observation {
542 path: topic.trim_matches('/').to_owned(),
543 freshest: None,
544 },
545 );
546 let registered = match self.exchange(id, &bytes, &socket).await {
547 Ok(reply) => answered(reply.code, &reply.payload).and_then(|()| {
548 if reply.code == MessageClass::Empty || reply.observe.is_some() {
549 Ok(())
550 } else {
551 Err(Error::Transport(format!(
552 "the server answered {topic} without registering an observation, so \
553 no changes will follow"
554 )))
555 }
556 }),
557 Err(error) => Err(error),
558 };
559 if registered.is_err() {
560 self.observations
561 .lock()
562 .expect("observations lock")
563 .remove(&token);
564 }
565 registered
566 }
567}
568
569impl Receive for CoapTransport {
570 async fn recv(&mut self) -> Result<Option<Message>> {
581 let incoming = self.incoming.as_mut().ok_or(Error::Closed)?;
582 Ok(incoming.recv().await)
583 }
584}
585
586fn message_type(reliability: Reliability) -> MessageType {
588 match reliability {
589 Reliability::NonConfirmable => MessageType::NonConfirmable,
590 Reliability::Confirmable => MessageType::Confirmable,
591 }
592}
593
594fn add_path(packet: &mut Packet, topic: &str) {
596 for segment in topic.split('/').filter(|segment| !segment.is_empty()) {
597 packet.add_option(CoapOption::UriPath, segment.as_bytes().to_vec());
598 }
599}
600
601fn path_from_packet(packet: &Packet) -> String {
603 match packet.get_option(CoapOption::UriPath) {
604 Some(segments) => segments
605 .iter()
606 .map(|segment| String::from_utf8_lossy(segment).into_owned())
607 .collect::<Vec<_>>()
608 .join("/"),
609 None => String::new(),
610 }
611}
612
613async fn dispatch(
620 packet: Packet,
621 pending: &PendingAcks,
622 observations: &Observations,
623 tx: &mpsc::UnboundedSender<Message>,
624 socket: &UdpSocket,
625) -> bool {
626 let observe = observe_value(&packet);
627 match packet.header.get_type() {
628 MessageType::Acknowledgement => {
629 let message_id = packet.header.message_id;
630 let reply = Reply {
631 code: packet.header.code,
632 payload: packet.payload.clone(),
633 observe,
634 };
635 let delivered = match observe {
636 Some(sequence) if is_success(reply.code) => {
637 notify(packet, sequence, observations, tx)
638 }
639 _ => Delivered::Taken,
640 };
641 if let Some(waiter) = pending.lock().expect("pending lock").remove(&message_id) {
642 let _ = waiter.send(Answer::Acknowledged(reply));
643 }
644 delivered != Delivered::Gone
645 }
646 MessageType::Reset => {
647 if let Some(waiter) = pending
648 .lock()
649 .expect("pending lock")
650 .remove(&packet.header.message_id)
651 {
652 let _ = waiter.send(Answer::Reset);
653 }
654 true
655 }
656 kind => {
657 let confirmable = kind == MessageType::Confirmable;
658 let message_id = packet.header.message_id;
659 let delivered = match (packet.header.code, observe) {
660 (MessageClass::Response(_), Some(sequence)) if is_success(packet.header.code) => {
661 notify(packet, sequence, observations, tx)
662 }
663 (MessageClass::Response(_), _) => end(&packet, observations),
664 _ => Delivered::Unknown,
665 };
666 match delivered {
667 Delivered::Unknown if confirmable => reset(message_id, socket).await,
668 Delivered::Taken if confirmable => acknowledge(message_id, socket).await,
669 _ => {}
670 }
671 delivered != Delivered::Gone
672 }
673 }
674}
675
676fn end(packet: &Packet, observations: &Observations) -> Delivered {
682 match observations
683 .lock()
684 .expect("observations lock")
685 .remove(packet.get_token())
686 {
687 Some(_) => Delivered::Taken,
688 None => Delivered::Unknown,
689 }
690}
691
692#[derive(PartialEq, Eq)]
694enum Delivered {
695 Taken,
697 Unknown,
699 Gone,
701}
702
703fn notify(
708 packet: Packet,
709 sequence: u32,
710 observations: &Observations,
711 tx: &mpsc::UnboundedSender<Message>,
712) -> Delivered {
713 let path = {
714 let mut observations = observations.lock().expect("observations lock");
715 let Some(observation) = observations.get_mut(packet.get_token()) else {
716 return Delivered::Unknown;
717 };
718 let now = Instant::now();
719 if !fresher(observation.freshest, sequence, now) {
720 return Delivered::Taken;
721 }
722 observation.freshest = Some((sequence, now));
723 observation.path.clone()
724 };
725 match tx.send(Message::new(path, packet.payload)) {
726 Ok(()) => Delivered::Taken,
727 Err(_) => Delivered::Gone,
728 }
729}
730
731fn fresher(freshest: Option<(u32, Instant)>, incoming: u32, now: Instant) -> bool {
734 const HALF: u32 = 1 << 23;
735 let Some((known, at)) = freshest else {
736 return true;
737 };
738 (known < incoming && incoming - known < HALF)
739 || (known > incoming && known - incoming > HALF)
740 || now > at + Duration::from_secs(128)
741}
742
743fn observe_value(packet: &Packet) -> Option<u32> {
745 let value = packet.get_option(CoapOption::Observe)?.front()?;
746 Some(
747 value
748 .iter()
749 .fold(0u32, |sum, byte| (sum << 8) | u32::from(*byte)),
750 )
751}
752
753fn is_success(code: MessageClass) -> bool {
755 u8::from(code) >> 5 == 2
756}
757
758async fn acknowledge(message_id: u16, socket: &UdpSocket) {
760 empty(MessageType::Acknowledgement, message_id, socket).await;
761}
762
763async fn reset(message_id: u16, socket: &UdpSocket) {
766 empty(MessageType::Reset, message_id, socket).await;
767}
768
769async fn empty(kind: MessageType, message_id: u16, socket: &UdpSocket) {
771 let mut packet = Packet::new();
772 packet.header.set_version(1);
773 packet.header.set_type(kind);
774 packet.header.code = MessageClass::Empty;
775 packet.header.message_id = message_id;
776 if let Ok(bytes) = packet.to_bytes() {
777 let _ = socket.send(&bytes).await;
778 }
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784
785 #[test]
786 fn reliability_defaults_to_confirmable() {
787 let config = CoapConfig::new("localhost", 5683);
788 assert_eq!(config.reliability, Reliability::Confirmable);
789 }
790
791 #[test]
792 fn setters_update_the_configuration() {
793 let config = CoapConfig::new("localhost", 5683)
794 .reliability(Reliability::NonConfirmable)
795 .ack_timeout(Duration::from_millis(250))
796 .max_retransmits(1)
797 .bind("127.0.0.1:0");
798 assert_eq!(config.reliability, Reliability::NonConfirmable);
799 assert_eq!(config.ack_timeout, Duration::from_millis(250));
800 assert_eq!(config.max_retransmits, 1);
801 assert_eq!(config.bind, "127.0.0.1:0");
802 }
803
804 #[test]
805 fn a_notification_is_fresher_by_the_rule_of_rfc_7641_section_3_4() {
806 let then = Instant::now();
807 let soon = then + Duration::from_secs(1);
808 assert!(
809 fresher(None, 0, then),
810 "the first notification is always fresh"
811 );
812 assert!(fresher(Some((1, then)), 2, soon));
813 assert!(
814 !fresher(Some((2, then)), 1, soon),
815 "an older number is stale"
816 );
817 assert!(!fresher(Some((2, then)), 2, soon), "a repeat is stale");
818 assert!(
819 fresher(Some((0x00FF_FFFF, then)), 0, soon),
820 "the 24-bit number wraps"
821 );
822 assert!(
823 !fresher(Some((0, then)), 0x0080_0001, soon),
824 "a jump past half the space reads as older"
825 );
826 assert!(
827 fresher(Some((5, then)), 1, then + Duration::from_secs(129)),
828 "after 128 seconds any notification is fresher"
829 );
830 }
831
832 #[test]
833 fn an_observe_value_is_a_big_endian_integer_of_up_to_three_bytes() {
834 let mut packet = Packet::new();
835 packet.add_option(CoapOption::Observe, vec![0x01, 0x02, 0x03]);
836 assert_eq!(observe_value(&packet), Some(0x0001_0203));
837 let mut empty = Packet::new();
838 empty.add_option(CoapOption::Observe, Vec::new());
839 assert_eq!(
840 observe_value(&empty),
841 Some(0),
842 "a zero-length value is zero"
843 );
844 assert_eq!(observe_value(&Packet::new()), None);
845 }
846
847 #[test]
848 fn path_round_trips_through_uri_path_options() {
849 let mut packet = Packet::new();
850 add_path(&mut packet, "sensors/1/temperature");
851 assert_eq!(path_from_packet(&packet), "sensors/1/temperature");
852 }
853
854 #[test]
855 fn leading_and_repeated_slashes_are_ignored() {
856 let mut packet = Packet::new();
857 add_path(&mut packet, "/sensors//1/");
858 assert_eq!(path_from_packet(&packet), "sensors/1");
859 }
860
861 #[tokio::test]
862 async fn send_before_connect_reports_closed() {
863 let mut transport = CoapTransport::new(CoapConfig::new("localhost", 5683));
864 assert!(matches!(
865 transport.send("t", b"x").await,
866 Err(Error::Closed)
867 ));
868 }
869
870 #[tokio::test]
871 async fn subscribe_before_connect_reports_closed() {
872 let mut transport = CoapTransport::new(CoapConfig::new("localhost", 5683));
873 assert!(matches!(transport.subscribe("t").await, Err(Error::Closed)));
874 }
875
876 #[tokio::test]
877 async fn recv_before_connect_reports_closed() {
878 let mut transport = CoapTransport::new(CoapConfig::new("localhost", 5683));
879 assert!(matches!(transport.recv().await, Err(Error::Closed)));
880 }
881
882 #[test]
883 fn a_registration_queues_its_state_before_the_request_wakes() {
884 let pending: PendingAcks = Arc::default();
885 let observations: Observations = Arc::default();
886 let (waiter, mut answer) = oneshot::channel();
887 pending.lock().expect("pending lock").insert(9, waiter);
888 observations.lock().expect("observations lock").insert(
889 vec![1, 2],
890 Observation {
891 path: "commands/valve".to_owned(),
892 freshest: None,
893 },
894 );
895 let mut ack = Packet::new();
896 ack.header.set_version(1);
897 ack.header.set_type(MessageType::Acknowledgement);
898 ack.header.code = MessageClass::Response(coap_lite::ResponseType::Content);
899 ack.header.message_id = 9;
900 ack.set_token(vec![1, 2]);
901 ack.add_option(CoapOption::Observe, vec![5]);
902 ack.payload = b"closed".to_vec();
903 let (tx, mut queue) = mpsc::unbounded_channel();
904
905 let held = observations.lock().expect("observations lock");
906 let pump = std::thread::spawn({
907 let pending = Arc::clone(&pending);
908 let observations = Arc::clone(&observations);
909 move || {
910 tokio::runtime::Builder::new_current_thread()
911 .enable_all()
912 .build()
913 .expect("a runtime")
914 .block_on(async {
915 let socket = UdpSocket::bind("127.0.0.1:0").await.expect("bind");
916 dispatch(ack, &pending, &observations, &tx, &socket).await
917 })
918 }
919 });
920 std::thread::sleep(Duration::from_millis(100));
921 assert!(
922 matches!(answer.try_recv(), Err(oneshot::error::TryRecvError::Empty)),
923 "the request woke before the state it registered was queued"
924 );
925 drop(held);
926
927 assert!(pump.join().expect("the pump"), "the queue is still open");
928 assert!(matches!(answer.try_recv(), Ok(Answer::Acknowledged(_))));
929 let state = queue.try_recv().expect("the current state");
930 assert_eq!(state.topic, "commands/valve");
931 assert_eq!(state.payload, b"closed");
932 }
933
934 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
935 async fn confirmable_send_without_a_server_times_out() {
936 let config = CoapConfig::new("127.0.0.1", 1)
937 .ack_timeout(Duration::from_millis(20))
938 .max_retransmits(1);
939 let mut transport = CoapTransport::new(config);
940 transport.connect().await.expect("bind socket");
941 assert!(transport.send("sensors/1", b"x").await.is_err());
942 }
943}