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}