uwuhi/
service.rs

1//! Service discovery and advertising.
2
3use std::{
4    collections::{btree_map::Entry, BTreeMap},
5    fmt,
6    str::FromStr,
7};
8
9use crate::{
10    name::{DomainName, Label},
11    packet::records::{PTR, SRV, TXT},
12    Error,
13};
14
15pub mod advertising;
16pub mod discovery;
17
18/// Transport protocol used by an advertised service (`_tcp` or `_udp`).
19#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub enum ServiceTransport {
21    /// Service uses TCP.
22    TCP,
23    /// Anything but TCP (UDP, SCTP, etc.).
24    Other,
25}
26
27impl ServiceTransport {
28    fn as_str(&self) -> &str {
29        match self {
30            ServiceTransport::TCP => "_tcp",
31            ServiceTransport::Other => "_udp",
32        }
33    }
34
35    pub fn to_label(&self) -> Label {
36        Label::new(self.as_str())
37    }
38}
39
40impl FromStr for ServiceTransport {
41    type Err = Error;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        match s {
45            "_tcp" => Ok(Self::TCP),
46            "_udp" => Ok(Self::Other),
47            _ => Err(Error::InvalidValue),
48        }
49    }
50}
51
52/// A service type identifier.
53///
54/// A service type is identified by a unique name ([`Label`]), and the [`ServiceTransport`] the
55/// service can be reached with.
56///
57/// *Instances* of a service running on a specific machine are represented by [`ServiceInstance`].
58#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub struct Service {
60    /// The service name, starting with an underscore.
61    name: Label,
62    transport: ServiceTransport,
63}
64
65impl Service {
66    /// Creates a new service.
67    ///
68    /// # Panics
69    ///
70    /// Panics if `name` does not start with an underscore (`_`).
71    pub fn new(name: Label, transport: ServiceTransport) -> Self {
72        assert!(name.as_bytes().starts_with(b"_"));
73        Self { name, transport }
74    }
75
76    pub fn from_ptr(ptr: PTR<'_>) -> Result<Self, Error> {
77        let mut labels = ptr.ptrdname().labels().iter();
78        let service_name = labels.next().ok_or(Error::Eof)?;
79        let transport = labels.next().ok_or(Error::Eof)?;
80        if labels.next().is_none() {
81            // Domain missing, this is probably not a valid service.
82            return Err(Error::Eof);
83        }
84        Ok(Service {
85            name: service_name.clone(),
86            transport: match transport.as_bytes() {
87                b"_tcp" => ServiceTransport::TCP,
88                b"_udp" => ServiceTransport::Other,
89                _ => return Err(Error::InvalidValue),
90            },
91        })
92    }
93
94    #[inline]
95    pub fn name(&self) -> &Label {
96        &self.name
97    }
98
99    #[inline]
100    pub fn transport(&self) -> ServiceTransport {
101        self.transport
102    }
103}
104
105impl fmt::Display for Service {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(f, "{}.{}", self.name, self.transport.as_str())
108    }
109}
110
111impl fmt::Debug for Service {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        fmt::Display::fmt(self, f)
114    }
115}
116
117/// A named instance of a [`Service`].
118#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub struct ServiceInstance {
120    instance_name: Label,
121    service: Service,
122}
123
124impl ServiceInstance {
125    /// Creates a new [`ServiceInstance`] from its components.
126    ///
127    /// `instance_name` can be a free-form string, typically identifying the machine the service is
128    /// running on.
129    ///
130    /// `service_name` must start with an underscore and is an agreed-upon identifier for the
131    /// service being offered.
132    ///
133    /// # Panics
134    ///
135    /// Panics if `service_name` does not start with an underscore (`_`).
136    pub fn new(instance_name: Label, service_name: Label, transport: ServiceTransport) -> Self {
137        Self {
138            instance_name,
139            service: Service::new(service_name, transport),
140        }
141    }
142
143    pub fn from_service(instance_name: Label, service: Service) -> Self {
144        Self {
145            instance_name,
146            service,
147        }
148    }
149
150    pub fn from_ptr(ptr: PTR<'_>) -> Result<Self, Error> {
151        let mut labels = ptr.ptrdname().labels().iter();
152        let instance_name = labels.next().ok_or(Error::Eof)?;
153        let service_name = labels.next().ok_or(Error::Eof)?;
154        let transport = labels.next().ok_or(Error::Eof)?;
155        if labels.next().is_none() {
156            // Domain missing, this is probably not a valid service.
157            return Err(Error::Eof);
158        }
159        Ok(ServiceInstance {
160            instance_name: instance_name.clone(),
161            service: Service {
162                name: service_name.clone(),
163                transport: match transport.as_bytes() {
164                    b"_tcp" => ServiceTransport::TCP,
165                    b"_udp" => ServiceTransport::Other,
166                    _ => return Err(Error::InvalidValue),
167                },
168            },
169        })
170    }
171
172    #[inline]
173    pub fn instance_name(&self) -> &Label {
174        &self.instance_name
175    }
176
177    #[inline]
178    pub fn service(&self) -> &Service {
179        &self.service
180    }
181
182    #[inline]
183    pub fn service_name(&self) -> &Label {
184        self.service.name()
185    }
186
187    #[inline]
188    pub fn service_transport(&self) -> ServiceTransport {
189        self.service.transport
190    }
191}
192
193impl fmt::Display for ServiceInstance {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        write!(f, "{}.{}", self.instance_name, self.service,)
196    }
197}
198
199impl fmt::Debug for ServiceInstance {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        fmt::Display::fmt(self, f)
202    }
203}
204
205/// Describes how a [`ServiceInstance`] can be reached, and supplies service metadata.
206pub struct InstanceDetails {
207    host: DomainName,
208    port: u16,
209    txt: TxtRecords,
210}
211
212impl InstanceDetails {
213    pub fn new(host: DomainName, port: u16) -> Self {
214        Self {
215            host,
216            port,
217            txt: TxtRecords::new(),
218        }
219    }
220
221    /// Parses an [`SRV`] record containing instance details.
222    pub fn from_srv(srv: &SRV<'_>) -> Result<Self, Error> {
223        Ok(Self {
224            host: srv.target().clone(),
225            port: srv.port(),
226            txt: TxtRecords::new(),
227        })
228    }
229
230    /// Returns the [`DomainName`] on which the service can be found.
231    #[inline]
232    pub fn host(&self) -> &DomainName {
233        &self.host
234    }
235
236    /// Returns the port on which the service is listening.
237    #[inline]
238    pub fn port(&self) -> u16 {
239        self.port
240    }
241
242    #[inline]
243    pub fn txt_records(&self) -> &TxtRecords {
244        &self.txt
245    }
246
247    #[inline]
248    pub fn txt_records_mut(&mut self) -> &mut TxtRecords {
249        &mut self.txt
250    }
251}
252
253/// List of `key=value` records stored in a DNS-SD TXT record of a service instance.
254#[derive(Debug)]
255pub struct TxtRecords {
256    // keys are lowercased
257    // FIXME this should keep the original order
258    map: BTreeMap<String, TxtRecord>,
259}
260
261#[derive(Debug)]
262struct TxtRecord {
263    key: String,
264    value: Option<Vec<u8>>,
265}
266
267impl TxtRecords {
268    pub fn new() -> Self {
269        Self {
270            map: BTreeMap::new(),
271        }
272    }
273
274    pub fn from_txt(txt: &TXT<'_>) -> Self {
275        let mut map = BTreeMap::new();
276
277        for entry in txt.entries() {
278            let mut split = entry.splitn(2, |&b| b == b'=');
279            let key = split.next().unwrap();
280            let key = match String::from_utf8(key.to_vec()) {
281                Ok(key) => key,
282                Err(e) => {
283                    log::debug!("non-ASCII TXT key: {}", e);
284                    continue;
285                }
286            };
287            let entry = map.entry(key.to_ascii_lowercase());
288            if let Entry::Occupied(_) = entry {
289                log::debug!("TXT key '{}' already occupied, ignoring", entry.key());
290            }
291
292            match split.next() {
293                Some(value) => {
294                    entry.or_insert(TxtRecord {
295                        key,
296                        value: Some(value.to_vec()),
297                    });
298                }
299                None => {
300                    // boolean flag
301                    entry.or_insert(TxtRecord { key, value: None });
302                }
303            }
304        }
305
306        Self { map }
307    }
308
309    /// Adds a TXT record with no value.
310    pub fn add_flag(&mut self, key: String) {
311        self.map
312            .insert(key.to_ascii_lowercase(), TxtRecord { key, value: None });
313    }
314
315    /// Returns an iterator over all key-value pairs.
316    pub fn iter(&self) -> impl Iterator<Item = (&str, TxtRecordValue<'_>)> {
317        self.map.iter().map(|(_, rec)| match &rec.value {
318            Some(v) => (rec.key.as_str(), TxtRecordValue::Value(&v)),
319            None => (rec.key.as_str(), TxtRecordValue::NoValue),
320        })
321    }
322
323    pub fn get(&self, key: &str) -> Option<TxtRecordValue<'_>> {
324        self.map
325            .get(&key.to_ascii_lowercase())
326            .map(|rec| match &rec.value {
327                Some(v) => TxtRecordValue::Value(v),
328                None => TxtRecordValue::NoValue,
329            })
330    }
331
332    pub fn is_empty(&self) -> bool {
333        self.map.is_empty()
334    }
335}
336
337impl fmt::Display for TxtRecords {
338    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339        for (i, rec) in self.map.values().enumerate() {
340            if i != 0 {
341                f.write_str(" ")?;
342            }
343
344            f.write_str(&rec.key)?;
345            match &rec.value {
346                Some(v) => {
347                    f.write_str("=")?;
348                    v.escape_ascii().fmt(f)?;
349                }
350                None => {}
351            }
352        }
353        Ok(())
354    }
355}
356
357pub enum TxtRecordValue<'a> {
358    NoValue,
359    Value(&'a [u8]),
360}
361
362impl<'a> fmt::Debug for TxtRecordValue<'a> {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        match self {
365            Self::NoValue => f.write_str("-"),
366            Self::Value(v) => match std::str::from_utf8(v) {
367                Ok(s) => s.fmt(f),
368                Err(_) => {
369                    for byte in *v {
370                        byte.escape_ascii().fmt(f)?;
371                    }
372                    Ok(())
373                }
374            },
375        }
376    }
377}