simpdiscoverylib/
lib.rs

1#![deny(missing_docs)]
2#![warn(clippy::unwrap_used)]
3
4//! `simpdiscovery` library crate for simple UDP datagram-based discovery of services on a LAN
5//!
6//! # Example combining a BeaconSender and a BeaconListener
7//! ```
8//! use simpdiscoverylib::{BeaconSender, BeaconListener};
9//! use std::time::Duration;
10//! use portpicker::pick_unused_port;
11//!
12//! let service_port = pick_unused_port().expect("Could not get a free port");
13//! let broadcast_port = pick_unused_port().expect("Could not get a free port");
14//! let my_service_name = "_my_service._tcp.local".as_bytes();
15//! let beacon = BeaconSender::new(service_port, my_service_name, broadcast_port)
16//!     .expect("Could not create sender");
17//! std::thread::spawn(move || {
18//!     beacon.send_loop(Duration::from_secs(1)).expect("Could not run send_loop")
19//! });
20//!
21//! let listener = BeaconListener::new(my_service_name, broadcast_port)
22//!     .expect("Could not create listener");
23//! let beacon = listener.wait(None).expect("Failed to receive beacon");
24//! assert_eq!(beacon.service_name, my_service_name, "Received service name doesn't match");
25//! assert_eq!(beacon.service_port, service_port, "Received service port doesn't match");
26//! ```
27
28use std::net::UdpSocket;
29use std::time::Duration;
30use log::{info, trace};
31use std::fmt::Formatter;
32use std::io;
33
34/// A broadcast address is always relative to a given network. When you have a network, you can
35/// compute its broadcast address by replacing all the host bits with 1s; simply put, the broadcast
36/// address is the highest numbered address you can have on the network, while the network address
37/// is the lowest one (with all host bits set to 0s); this is why you can't use either of them
38/// as actual host addresses: they are reserved for this use.
39///
40/// If your network is `192.168.1.0/24`, then your network address will be `192.168.1.0`
41/// and your broadcast address will be `192.168.1.255`
42///
43/// If your network is `192.168.0.0/16`, then your network address will be `192.168.0.0`
44/// and your broadcast address will be `192.168.255.255`
45///
46/// `255.255.255.255` is a special broadcast address, which means "this network".
47/// It lets you send a broadcast packet to the network you're connected to, without actually
48/// caring about its address.
49///
50/// See [wikipedia article](https://en.wikipedia.org/wiki/Broadcast_address) for more info
51const BROADCAST_ADDRESS : &str = "255.255.255.255";
52
53/// The address `0.0.0.0` is known as the "zero network", which in Internet Protocol standards
54/// stands for this network, i.e. the local network.
55const LISTENING_ADDRESS : &str = "0.0.0.0";
56
57const MAX_INCOMING_BEACON_SIZE : usize = 1024;
58const MAGIC_NUMBER: u16 = 0xbeef;
59
60/// `BeaconSender` is used to send UDP Datagram beacons to the Broadcast IP address on the LAN
61///
62/// # Example of using `BeaconSender`
63/// This example will just exit at the end and the thread above will die along with the process.
64///
65/// In your own code, either:
66///   * don't start a background thread and just loop forever sending beacons in main thread, or
67///   * have some other way to keep the process (and hence the sending thread) alive so
68///     beacons are actually sent before process ends
69///
70/// ```
71/// use simpdiscoverylib::{BeaconSender, BeaconListener};
72/// use std::time::Duration;
73/// use portpicker::pick_unused_port;
74///
75/// let service_port = pick_unused_port().expect("Could not get a free port");
76/// let broadcast_port = pick_unused_port().expect("Could not get a free port for broadcast");
77/// let my_service_name = "_my_service._tcp.local".as_bytes();
78/// let beacon = BeaconSender::new(service_port, my_service_name, broadcast_port)
79///     .expect("Could not create sender");
80/// std::thread::spawn(move || {
81///     beacon.send_loop(Duration::from_secs(1)).expect("Could not enter send_loop");
82///  });
83pub struct BeaconSender {
84    socket: UdpSocket,
85    beacon_payload: Vec<u8>,
86    broadcast_address: String,
87}
88
89fn u16_to_array_of_u8(x:u16) -> [u8;2] {
90    let b1 : u8 = ((x >> 8) & 0xff) as u8;
91    let b2 : u8 = (x & 0xff) as u8;
92    [b1, b2]
93}
94
95fn array_of_u8_to_u16(array: &[u8]) -> u16 {
96    let upper : u16 = (array[0] as u16) << 8;
97    let lower : u16 = array[1] as u16;
98    upper + lower
99}
100
101impl BeaconSender {
102    /// Create a new `BeaconSender` to send `Beacon`s for a service with name `service_name` that
103    /// should be contacted on the port `service_port`
104    pub fn new(service_port: u16, service_name: &[u8], broadcast_port: u16) -> io::Result<Self> {
105        // Setting the port to non-zero (or at least the same port used in listener) causes
106        // this to fail. I am not sure of the correct value to use. Docs on UDP says '0' is
107        // permitted, if you do not expect a response from the UDP Datagram sent.
108        let bind_address = format!("{LISTENING_ADDRESS}:0");
109        let socket:UdpSocket = UdpSocket::bind(&bind_address)
110            .map_err(|e|
111                         io::Error::new(io::ErrorKind::AddrInUse,
112                                        format!("SimpDiscover::BeaconSender could not bind to UdpSocket {bind_address} ({e})")))?;
113        info!("Socket bound to: {}", bind_address);
114
115        socket.set_broadcast(true)?;
116        info!("Broadcast mode set to ON");
117
118        // Create payload with magic number, service_port number and service_name
119        let mut beacon_payload: Vec<u8> = u16_to_array_of_u8(MAGIC_NUMBER).to_vec();
120        beacon_payload.append(&mut u16_to_array_of_u8(service_port).to_vec());
121        beacon_payload.append(&mut service_name.to_vec());
122
123        let broadcast_address = format!("{BROADCAST_ADDRESS}:{broadcast_port}");
124
125        Ok(Self {
126            socket,
127            beacon_payload,
128            broadcast_address,
129        })
130    }
131
132    /// Enter an infinite loop sending `Beacon`s periodically
133    pub fn send_loop(&self, period: Duration) -> io::Result<()> {
134        loop {
135            self.send_one_beacon()?;
136            std::thread::sleep(period);
137        }
138    }
139
140    /// Send a single `Beacon` out
141    pub fn send_one_beacon(&self) -> io::Result<usize> {
142        trace!("Sending Beacon '{}' to: '{}'", String::from_utf8_lossy(&self.beacon_payload[4..]),
143            self.broadcast_address);
144        self.socket.send_to(&self.beacon_payload, &self.broadcast_address)
145    }
146}
147
148/// `Beacon` contains information about the beacon that was received by a `BeaconListener`
149pub struct Beacon {
150    /// The IP address and port the beacon was sent from
151    pub service_ip: String,
152    /// The port the service is running on
153    pub service_port: u16,
154    /// The name of the service sending the beacon
155    pub service_name: Vec<u8>
156}
157
158impl std::fmt::Display for Beacon {
159    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
160        let service_name = String::from_utf8(self.service_name.clone()).unwrap_or_else(|_| "Invalid UTF-8 String".into());
161        write!(f, "ServiceName: '{}', Service IP: {}, Service Port: {}", service_name, self.service_ip, self.service_port)
162    }
163}
164
165/// `BeaconListener` listens for new `Beacons` on the specified port
166///
167/// # Example of using `BeaconListener` with timeout
168/// ```
169/// use simpdiscoverylib::BeaconListener;
170/// use std::time::Duration;
171/// use portpicker::pick_unused_port;
172///
173/// let listening_port = pick_unused_port().expect("Could not get a free port to listen on");
174/// let listener = BeaconListener::new("_my_service._tcp.local".as_bytes(), listening_port)
175///     .expect("Could not create listener");
176///
177/// // Avoid blocking tests by setting a short timeout, expect an error, as there is no sender setup
178/// assert!(listener.wait(Some(Duration::from_millis(1))).is_err());
179/// ```
180pub struct BeaconListener {
181    socket: UdpSocket,
182    service_name: Vec<u8>,
183}
184
185impl BeaconListener {
186    /// Create a new `BeaconListener` on `port` with an option `filter` to be applied to incoming
187    /// beacons. This binds to address "0.0.0.0:listening_port"
188    pub fn new(service_name: &[u8], listening_port: u16) -> io::Result<Self> {
189        let listening_address = format!("{}:{}", LISTENING_ADDRESS, listening_port);
190        let socket = UdpSocket::bind(&listening_address)
191            .map_err(|e|
192                io::Error::new(io::ErrorKind::AddrInUse,
193                               format!("SimpDiscover::BeaconListener could not bind to UdpSocket at {listening_address} ({e})")))?;
194        trace!("Socket bound to: {}", listening_address);
195        socket.set_broadcast(true)?;
196
197        Ok(Self {
198            socket,
199            service_name: service_name.to_vec(),
200        })
201    }
202
203    /// Wait for a `Beacon` on the port specified in `BeaconListener::new()`
204    /// If `timeout` is None, then it will block forever waiting for a beacon matching the optional
205    /// filter (if supplied) in `BeaconListener::new()`. If no `filter` was supplied it will block
206    /// waiting for any beacon to be received.
207    ///
208    /// If `timeout` is `Some(Duration)` then it will block for that duration on the reception of
209    /// each beacon. If the beacon does not match a supplied `filter` then it will loop (blocking
210    /// for `duration` each time until a matching beacon is found.
211    pub fn wait(&self, timeout: Option<Duration>) -> io::Result<Beacon> {
212        self.socket.set_read_timeout(timeout)?;
213        info!("Read timeout set to: {:?}", timeout);
214
215        info!("Waiting for beacon matching '{}'", String::from_utf8_lossy(&self.service_name));
216        loop {
217            let beacon = self.receive_one_beacon()?;
218
219            if beacon.service_name == self.service_name {
220                trace!("Beacon '{}' matches filter '{}': returning beacon",
221                    String::from_utf8_lossy(&beacon.service_name), String::from_utf8_lossy(&self.service_name));
222                return Ok(beacon);
223            } else {
224                trace!("Beacon '{}' does not match filter '{}': ignoring",
225                    String::from_utf8_lossy(&beacon.service_name), String::from_utf8_lossy(&self.service_name));
226            }
227        }
228    }
229
230    /*
231        Receive one beacon
232     */
233    fn receive_one_beacon(&self) -> io::Result<Beacon> {
234        let mut buffer = [0; MAX_INCOMING_BEACON_SIZE];
235
236        loop {
237            let (number_of_bytes, source_address) = self.socket.recv_from(&mut buffer)?;
238            let magic_number = array_of_u8_to_u16(&buffer[0..2]);
239            if magic_number == MAGIC_NUMBER {
240                let service_port = array_of_u8_to_u16(&buffer[2..4]);
241                let service_name = buffer[4..number_of_bytes].to_vec();
242
243                return Ok(Beacon {
244                    service_ip: source_address.ip().to_string(),
245                    service_port,
246                    service_name
247                });
248            }
249        }
250    }
251}