simple_ssdp/
service.rs

1use std::net::Ipv4Addr;
2use std::net::SocketAddr;
3use std::sync::Arc;
4
5use log::debug;
6use log::trace;
7use tokio::net::UdpSocket;
8
9use crate::http_helper::generate_ssdp_discover_answer;
10use crate::socket_helper::join_socket;
11use crate::MulticastAddr;
12use crate::SSDP_PORT;
13
14#[derive(Clone, PartialEq, Eq, Debug)]
15/// This Describes the basic data exchanged between [Service] and [crate::client::Client]
16///
17/// Usages:
18/// - It's used to create a new [Service]
19/// - [crate::client::Client] holds a list of all it could find within the network
20pub struct ServiceDescription {
21    /// Unique Identifier, often a UUID
22    ///     
23    /// ```text
24    /// upnp:uuid:83760048-2d32-4e48-854f-f63a8fa9fd09
25    /// uuid:83760048-2d32-4e48-854f-f63a8fa9fd09
26    /// ```
27    pub usn_uri: String,
28
29    /// Descriptive name, usually a [crate::client::Client] searches for this term
30    ///
31    /// ```text
32    /// upnp:clockradio
33    /// ms:wince
34    /// my:app
35    /// ```
36    pub service_type_uri: String,
37
38    /// Cache-Control max age
39    ///
40    /// The time this service usually expires and the [crate::client::Client] should search for again.
41    ///
42    /// According to [RFC 2616](https://www.ietf.org/rfc/rfc2616.txt) the maximum value is: 31536000
43    pub expiration: u32,
44
45    /// Location of the [Service], should be a valid URL
46    ///
47    /// ```text
48    /// http://foo.com/bar
49    /// https://myapp/service
50    /// ```
51    pub location: String,
52}
53
54/// The SSDP Service
55///
56/// Call [Service::new] with [ServiceDescription] to create a new [Service]
57pub struct Service {
58    service_description: ServiceDescription,
59    // TODO we might want to hold a list of all Clients aswell
60}
61
62// TODO when starting Service send NOTIFY ssdp:alive to Multicast
63// TODO when stopping Service send NOTIFY ssdp::byebye to Multicast
64// TODO make this permanently running and accept Signals signaling to stop etc.
65
66impl Service {
67    /// Creates a new [Service]
68    ///
69    /// Requires a [ServiceDescription] to describe this Service
70    pub fn new(service_description: ServiceDescription) -> Self {
71        Service {
72            service_description,
73        }
74    }
75
76    /// Opens the listener
77    ///
78    /// This process is blocking so best to start it in its own thread
79    pub async fn listen(&self, address: MulticastAddr) -> Result<(), Box<dyn std::error::Error>> {
80        let local_addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, SSDP_PORT));
81        let socket = Arc::new(UdpSocket::bind(local_addr).await?);
82
83        join_socket(&address, socket.clone())?;
84
85        // Create a buffer to store the received data
86        let mut buf = vec![0; 1024];
87
88        debug!("Start listening for SSDP discovery messages...");
89
90        // Listen for discovery requests and respond
91        loop {
92            let (len, addr) = socket.recv_from(&mut buf).await?;
93            let copy = buf.clone();
94            trace!(
95                "Received {} bytes from {}: {:#?}",
96                len,
97                addr,
98                String::from_utf8(copy).unwrap().replace('\0', "")
99            );
100
101            let mut headers = [httparse::EMPTY_HEADER; 64];
102            let mut req = httparse::Request::new(&mut headers);
103
104            if req.parse(&buf[..len]).is_err() {
105                trace!("Could not parse request to HTTP");
106                continue;
107            }
108
109            if req.method.is_none() || req.method.is_some_and(|method| method != "M-SEARCH") {
110                trace!("Request is not M-SEARCH");
111                continue;
112            }
113
114            // TODO right now this depends on the order of the header - as I only plan using client and service I don't really care
115            let man = req.headers.get(2);
116            if man.is_none()
117                || man.is_some_and(|man| String::from_utf8_lossy(man.value) != "\"ssdp:discover\"")
118            {
119                trace!("Request uses wrong MAN header");
120                continue;
121            }
122
123            let st = req.headers.get(3);
124            if st.is_none()
125                || st.is_some_and(|st| -> bool {
126                    let st = String::from_utf8_lossy(st.value);
127
128                    st != "ssdp:all" && st != self.service_description.service_type_uri
129                })
130            {
131                trace!("ST header that's not interesting for us submitted");
132                continue;
133            }
134
135            let s = req.headers.get(0);
136            match s {
137                None => {
138                    trace!("S was not submitted");
139                    continue;
140                }
141                Some(s) => {
142                    let s = String::from_utf8_lossy(s.value);
143                    let resp_msg =
144                        generate_ssdp_discover_answer(&self.service_description, s.to_string());
145
146                    socket.send_to(resp_msg.as_bytes(), &addr).await?;
147                    trace!("Send SSDP response {:#?} to {}", resp_msg, addr);
148                }
149            }
150            if s.is_none() {
151                trace!("S was not submitted");
152                continue;
153            }
154        }
155    }
156}