slimproto/
discovery.rs

1/// This module provides the `discover` function which "pings" for a server
2/// on the network returning its address if it exists.
3
4use crate::{proto::{Server, ServerTlv, ServerTlvMap, SLIM_PORT}, Capabilities};
5
6use std::{
7    collections::HashMap,
8    io,
9    net::{Ipv4Addr, SocketAddr, UdpSocket, SocketAddrV4},
10    sync::{
11        atomic::{AtomicBool, Ordering},
12        Arc,
13    },
14    thread::{sleep, spawn},
15    time::Duration,
16};
17
18/// Repeatedly send discover "pings" to the server with an optional timeout.
19///
20/// Returns:
21/// - `Ok(None)` on timeout
22/// - `Ok(Some(Server))` on server response.
23/// - `io::Error` if an error occurs
24///
25/// Note that the Slim Protocol is IPv4 only.
26/// This function will try forever if no timeout is passed in which case `Ok(None)` can never
27/// be returned.
28pub fn discover(timeout: Option<Duration>) -> io::Result<Option<Server>> {
29    const UDPMAXSIZE: usize = 1450; // as defined in LMS code
30
31    let cx = UdpSocket::bind((Ipv4Addr::new(0, 0, 0, 0), 0))?;
32    cx.set_broadcast(true)?;
33    cx.set_read_timeout(timeout)?;
34
35    let cx_send = cx.try_clone()?;
36    let running = Arc::new(AtomicBool::new(true));
37    let is_running = running.clone();
38    spawn(move || {
39        let buf = b"eNAME\0IPAD\0JSON\0VERS"; // Also \0UUID\0JVID
40        while is_running.load(Ordering::Relaxed) {
41            cx_send
42                .send_to(buf, (Ipv4Addr::new(255, 255, 255, 255), SLIM_PORT))
43                .ok();
44            sleep(Duration::from_secs(5));
45        }
46    });
47
48    let mut buf = [0u8; UDPMAXSIZE];
49    let response = cx.recv_from(&mut buf);
50    running.store(false, Ordering::Relaxed);
51
52    response.map_or_else(
53        |e| match e.kind() {
54            io::ErrorKind::WouldBlock => Ok(None),
55            _ => Err(e),
56        },
57        |(len, sock_addr)| match sock_addr {
58            SocketAddr::V4(addr) => Ok(Some(Server {
59                socket: SocketAddrV4::new(*addr.ip(), SLIM_PORT),
60                // ip_address: *addr.ip(),
61                // port: SLIM_PORT,
62                tlv_map: {
63                    if len > 0 && buf[0] == b'E' {
64                        Some(decode_tlv(&buf[1..]))
65                    } else {
66                        None
67                    }
68                },
69                sync_group_id: None,
70                caps: Capabilities(Vec::new()), // No capabilities in discovery
71            })),
72            _ => Ok(None),
73        },
74    )
75}
76
77fn decode_tlv(buf: &[u8]) -> ServerTlvMap {
78    let mut ret = HashMap::new();
79    let mut view = &buf[..];
80
81    while view.len() > 4 && view[0].is_ascii() {
82        let token = String::from_utf8(view[..4].to_vec()).unwrap_or_default();
83        let valen = view[4] as usize;
84        view = &view[5..];
85
86        if view.len() < valen {
87            break;
88        }
89
90        let value = String::from_utf8(view[..valen].to_vec()).unwrap_or_default();
91
92        let value = match token.as_str() {
93            "NAME" => ServerTlv::Name(value),
94            "VERS" => ServerTlv::Version(value),
95            "IPAD" => {
96                if let Ok(addr) = value.parse::<Ipv4Addr>() {
97                    ServerTlv::Address(addr)
98                } else {
99                    break;
100                }
101            }
102            "JSON" => {
103                if let Ok(port) = value.parse::<u16>() {
104                    ServerTlv::Port(port)
105                } else {
106                    break;
107                }
108            }
109            _ => {
110                break;
111            }
112        };
113
114        ret.insert(token, value);
115        view = &view[valen..];
116    }
117
118    ret
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn server_discover() {
127        let res = discover(Some(Duration::from_secs(1)));
128        assert!(res.is_ok());
129
130        if let Ok(Some(server)) = res {
131            assert!(!server.socket.ip().is_unspecified());
132            assert!(server.tlv_map.is_some());
133        }
134    }
135}