wot_discovery/
lib.rs

1//! Web of Things Discovery
2//!
3//! Discover [Web Of Things](https://www.w3.org/WoT/) that advertise themselves in the network.
4//!
5//! ## Supported Introduction Mechanisms
6//!
7//! - [x] [mDNS-SD (HTTP)](https://www.w3.org/TR/wot-discovery/#introduction-dns-sd-sec)
8
9use std::{marker::PhantomData, net::IpAddr};
10
11use futures_core::Stream;
12use futures_util::StreamExt;
13use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo};
14use tracing::debug;
15
16use wot_td::{
17    extend::{Extend, ExtendablePiece, ExtendableThing},
18    hlist::Nil,
19    thing::Thing,
20};
21
22/// The error type for Discovery operation
23#[derive(thiserror::Error, Debug)]
24#[non_exhaustive]
25pub enum Error {
26    #[error("mdns cannot be accessed {0}")]
27    Mdns(#[from] mdns_sd::Error),
28    #[error("reqwest error {0}")]
29    Reqwest(#[from] reqwest::Error),
30    #[error("Missing address")]
31    NoAddress,
32}
33
34/// A specialized [`Result`] type
35pub type Result<T> = std::result::Result<T, Error>;
36
37const WELL_KNOWN: &str = "/.well-known/wot";
38
39/// Discover [Web Of Things](https://www.w3.org/WoT/) via a supported Introduction Mechanism.
40pub struct Discoverer<Other: ExtendableThing + ExtendablePiece = Nil> {
41    mdns: ServiceDaemon,
42    service_type: String,
43    _other: PhantomData<Other>,
44}
45
46/// Discovered Thing and its mDNS information
47pub struct Discovered<Other: ExtendableThing + ExtendablePiece> {
48    /// Discovered Thing
49    ///
50    /// It is provided as presented by the discovered Servient.
51    pub thing: Thing<Other>,
52    info: ServiceInfo,
53    scheme: String,
54}
55
56impl<Other: ExtendableThing + ExtendablePiece> Discovered<Other> {
57    /// Discovered Servient listening addresses
58    pub fn get_addresses(&self) -> Vec<IpAddr> {
59        self.info
60            .get_addresses()
61            .iter()
62            .map(|ip| ip.to_owned().into())
63            .collect()
64    }
65    /// Discovered Servient listening port
66    pub fn get_port(&self) -> u16 {
67        self.info.get_port()
68    }
69
70    /// Discovered Servient hostname
71    ///
72    /// To be used to make tls requests
73    pub fn get_hostname(&self) -> &str {
74        self.info.get_hostname()
75    }
76
77    /// Discovered Servient scheme
78    pub fn get_scheme(&self) -> &str {
79        &self.scheme
80    }
81}
82
83async fn get_thing<Other: ExtendableThing + ExtendablePiece>(
84    info: ServiceInfo,
85) -> Result<Discovered<Other>> {
86    let host = info.get_addresses().iter().next().ok_or(Error::NoAddress)?;
87    let port = info.get_port();
88    let props = info.get_properties();
89    let path = props.get_property_val_str("td").unwrap_or(WELL_KNOWN);
90    let proto = props
91        .get_property_val_str("scheme")
92        .or_else(|| {
93            // compatibility with
94            props
95                .get_property_val_str("tls")
96                .map(|tls| if tls == "1" { "https" } else { "http" })
97        })
98        .unwrap_or("http");
99
100    debug!("Got {proto} {host} {port} {path}");
101
102    let r = reqwest::get(format!("{proto}://{host}:{port}{path}")).await?;
103
104    let thing = r.json().await?;
105    let scheme = proto.to_owned();
106    let d = Discovered {
107        thing,
108        info,
109        scheme,
110    };
111
112    Ok(d)
113}
114
115impl Discoverer {
116    /// Creates a new Discoverer
117    pub fn new() -> Result<Self> {
118        let mdns = ServiceDaemon::new()?;
119        let service_type = "_wot._tcp.local.".to_owned();
120        Ok(Self {
121            mdns,
122            service_type,
123            _other: PhantomData,
124        })
125    }
126}
127
128impl<Other: ExtendableThing + ExtendablePiece> Discoverer<Other> {
129    /// Extend the [Discoverer] with a [ExtendableThing]
130    pub fn ext<T>(self) -> Discoverer<Other::Target>
131    where
132        Other: Extend<T>,
133        Other::Target: ExtendableThing + ExtendablePiece,
134    {
135        let Discoverer {
136            mdns,
137            service_type,
138            _other,
139        } = self;
140
141        Discoverer {
142            mdns,
143            service_type,
144            _other: PhantomData,
145        }
146    }
147
148    /// Returns an Stream of discovered things
149    pub fn stream(&self) -> Result<impl Stream<Item = Result<Discovered<Other>>>> {
150        let receiver = self.mdns.browse(&self.service_type)?;
151
152        let s = receiver.into_stream().filter_map(|v| async move {
153            tracing::info!("{:?}", v);
154            if let ServiceEvent::ServiceResolved(info) = v {
155                let t = get_thing(info).await;
156                Some(t)
157            } else {
158                None
159            }
160        });
161
162        Ok(s)
163    }
164}