upnp_rs/discovery/notify.rs
1/*!
2This module provides three functions that provide 1) device available, 2) device updated, and
33) device leaving notifications over multicast UDP.
4*/
5use crate::common::httpu::{multicast_once, Options as MulticastOptions, RequestBuilder};
6use crate::common::interface::IP;
7use crate::common::uri::{URI, URL};
8use crate::common::user_agent::user_agent_string;
9use crate::discovery::search::SearchTarget;
10use crate::discovery::ProductVersion;
11use crate::error::{unsupported_version, Error};
12use crate::syntax::{
13 HTTP_HEADER_BOOTID, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONFIGID, HTTP_HEADER_HOST,
14 HTTP_HEADER_LOCATION, HTTP_HEADER_NEXT_BOOTID, HTTP_HEADER_NT, HTTP_HEADER_NTS,
15 HTTP_HEADER_SEARCH_PORT, HTTP_HEADER_SERVER, HTTP_HEADER_USN, HTTP_METHOD_NOTIFY,
16 MULTICAST_ADDRESS, NTS_ALIVE, NTS_BYE, NTS_UPDATE,
17};
18use crate::SpecVersion;
19
20// ------------------------------------------------------------------------------------------------
21// Public Types
22// ------------------------------------------------------------------------------------------------
23
24///
25/// Description of a device sent in _alive_ and _update_ messages.
26///
27#[derive(Clone, Debug)]
28pub struct Device {
29 pub notification_type: SearchTarget,
30 pub service_name: URI,
31 pub location: URL,
32 pub boot_id: u32,
33 pub config_id: u64,
34 pub search_port: Option<u16>,
35 pub secure_location: Option<String>,
36}
37
38///
39/// This type encapsulates a set of mostly optional values to be used to construct messages to
40/// send.
41///
42#[derive(Clone, Debug)]
43pub struct Options {
44 /// The specification that will be used to construct sent messages and to verify responses.
45 /// Default: `SpecVersion:V10`.
46 pub spec_version: SpecVersion,
47 /// A specific network interface to bind to; if specified the default address for the interface
48 /// will be used, else the address `0.0.0.0:0` will be used. Default: `None`.
49 pub network_interface: Option<String>,
50 /// Denotes whether the implementation wants to only use IPv4, IPv6, or doesn't care.
51 pub network_version: Option<IP>,
52 /// The IP packet TTL value.
53 pub packet_ttl: u32,
54 /// The value used to control caching of these notifications by control points.
55 pub max_age: u16,
56 /// If specified this is to be the `ProduceName/Version` component of the user agent string
57 /// the client will generate as part of sent messages. If not specified a default value based
58 /// on the name and version of this crate will be used. Default: `None`.
59 pub product_and_version: Option<ProductVersion>,
60}
61
62// ------------------------------------------------------------------------------------------------
63// Public Functions
64// ------------------------------------------------------------------------------------------------
65
66/**
67Provides an implementation of the `ssdp:alive` notification.
68
69# Specification
70
71When a device is added to the network, it multicasts discovery messages to advertise its root
72device, any embedded devices, and any services. Each discovery message contains four major
73components:
74
751. a potential search target (e.g., device type), sent in an `NT` (Notification Type) header,
762. a composite identifier for the advertisement, sent in a `USN` (Unique Service Name) header,
773. a URL for more information about the device (or enclosing device in the case of a service),
78 sent in a `LOCATION` header, and
794. a duration for which the advertisement is valid, sent in a `CACHE-CONTROL` header.
80
81# Parameters
82
83* `device` - details of the device to publish as a part of the notification message. Not all device
84 fields may be used in all notifications.
85* `options` - protocol options such as the specification version to use and any network
86 configuration values.
87
88*/
89pub fn device_available(device: &mut Device, options: Options) -> Result<(), Error> {
90 let next_boot_id = device.boot_id + 1;
91 let mut message_builder = RequestBuilder::new(HTTP_METHOD_NOTIFY);
92 message_builder
93 .add_header(HTTP_HEADER_HOST, MULTICAST_ADDRESS)
94 .add_header(
95 HTTP_HEADER_CACHE_CONTROL,
96 &format!("max-age={}", options.max_age),
97 )
98 .add_header(HTTP_HEADER_LOCATION, &device.location.to_string())
99 .add_header(HTTP_HEADER_NT, &device.notification_type.to_string())
100 .add_header(HTTP_HEADER_NTS, NTS_ALIVE)
101 .add_header(
102 HTTP_HEADER_SERVER,
103 &user_agent_string(options.spec_version, options.product_and_version.clone()),
104 )
105 .add_header(HTTP_HEADER_USN, &device.service_name.to_string());
106
107 if options.spec_version >= SpecVersion::V11 {
108 message_builder
109 .add_header(HTTP_HEADER_BOOTID, &device.boot_id.to_string())
110 .add_header(HTTP_HEADER_CONFIGID, &device.config_id.to_string());
111 if let Some(search_port) = &device.search_port {
112 message_builder.add_header(HTTP_HEADER_SEARCH_PORT, &search_port.to_string());
113 }
114 }
115
116 if options.spec_version >= SpecVersion::V20 {
117 if let Some(secure_location) = &device.secure_location {
118 message_builder.add_header(HTTP_HEADER_USN, secure_location);
119 }
120 }
121
122 multicast_once(
123 &message_builder.into(),
124 &MULTICAST_ADDRESS.parse().unwrap(),
125 &options.into(),
126 )?;
127
128 device.boot_id = next_boot_id;
129 Ok(())
130}
131
132/**
133Provides an implementation of the `ssdp:upate` notification.
134
135# Specification
136
137When a new UPnP-enabled interface is added to a multi-homed device, the device MUST increase its
138`BOOTID.UPNP.ORG` field value, multicast an `ssdp:update` message for each of the root devices,
139embedded devices and embedded services to all of the existing UPnP-enabled interfaces to announce
140a change in the `BOOTID.UPNP.ORG` field value, and re-advertise itself on all (existing and new)
141UPnP-enabled interfaces with the new `BOOTID.UPNP.ORG` field value. Similarly, if a multi-homed
142device loses connectivity on a UPnP-enabled interface and regains connectivity, or if the IP
143address on one of the UPnP-enabled interfaces changes, the device MUST increase the
144`BOOTID.UPNP.ORG` field value, multicast an `ssdp:update` message for each of the root devices,
145embedded devices and embedded services to all the unaffected UPnP-enabled interfaces to announce a
146change in the `BOOTID.UPNP.ORG` field value, and re-advertise itself on all (affected and
147unaffected) UPnP-enabled interfaces with the new `BOOTID.UPNP.ORG` field value. In all cases, the
148`ssdp:update` message for the root devices MUST be sent as soon as possible. Other `ssdp:update`
149messages SHOULD be spread over time. However, all ssdp:update messages MUST be sent before any
150announcement messages with the new `BOOTID.UPNP.ORG` field value can be sent.
151
152
153When `ssdp:update` messages are sent on multiple UPnP-enabled interfaces, the messages MUST contain
154identical field values except for the `HOST` and `LOCATION` field values. The `HOST` field value
155of an advertisement MUST be the standard multicast address specified for the protocol (IPv4 or IPv6)
156used on the interface. The URL specified in the `LOCATION` field value MUST be reachable on the
157interface on which the advertisement is sent.
158
159# Parameters
160
161* `device` - details of the device to publish as a part of the notification message. Not all device
162 fields may be used in all notifications.
163* `options` - protocol options such as the specification version to use and any network
164 configuration values.
165
166*/
167pub fn device_update(device: &mut Device, options: Options) -> Result<(), Error> {
168 if options.spec_version == SpecVersion::V10 {
169 unsupported_version(options.spec_version).into()
170 } else {
171 let next_boot_id = device.boot_id + 1;
172 let mut message_builder = RequestBuilder::new(HTTP_METHOD_NOTIFY);
173 message_builder
174 .add_header(HTTP_HEADER_HOST, MULTICAST_ADDRESS)
175 .add_header(HTTP_HEADER_LOCATION, &device.location.to_string())
176 .add_header(HTTP_HEADER_NT, &device.notification_type.to_string())
177 .add_header(HTTP_HEADER_NTS, NTS_UPDATE)
178 .add_header(HTTP_HEADER_USN, &device.service_name.to_string())
179 .add_header(HTTP_HEADER_BOOTID, &device.boot_id.to_string())
180 .add_header(HTTP_HEADER_NEXT_BOOTID, &next_boot_id.to_string())
181 .add_header(HTTP_HEADER_CONFIGID, &device.config_id.to_string());
182
183 if let Some(search_port) = &device.search_port {
184 message_builder.add_header(HTTP_HEADER_SEARCH_PORT, &search_port.to_string());
185 }
186
187 if options.spec_version >= SpecVersion::V20 {
188 if let Some(secure_location) = &device.secure_location {
189 message_builder.add_header(HTTP_HEADER_USN, secure_location);
190 }
191 }
192
193 multicast_once(
194 &message_builder.into(),
195 &MULTICAST_ADDRESS.parse().unwrap(),
196 &options.into(),
197 )?;
198 device.boot_id = next_boot_id;
199 Ok(())
200 }
201}
202
203/**
204Provides an implementation of the `ssdp:byebye` notification.
205
206# Specification
207
208When a device and its services are going to be removed from the network, the device SHOULD
209multicast an `ssdp:byebye` message corresponding to each of the `ssdp:alive` messages it multicasted
210that have not already expired. If the device is removed abruptly from the network, it might not be
211possible to multicast a message. As a fallback, discovery messages MUST include an expiration
212value in a `CACHE-CONTROL` field value (as explained above); if not re-advertised, the discovery
213message eventually expires on its own.
214
215When a device is about to be removed from the network, it should explicitly revoke its discovery
216messages by sending one multicast request for each `ssdp:alive message` it sent. Each multicast
217request must have method `NOTIFY` and `ssdp:byeby`e in the `NTS` header in the following format.
218
219# Parameters
220
221* `device` - details of the device to publish as a part of the notification message. Not all device
222 fields may be used in all notifications.
223* `options` - protocol options such as the specification version to use and any network
224 configuration values.
225
226*/
227pub fn device_unavailable(device: &mut Device, options: Options) -> Result<(), Error> {
228 let next_boot_id = device.boot_id + 1;
229 let mut message_builder = RequestBuilder::new(HTTP_METHOD_NOTIFY);
230 message_builder
231 .add_header(HTTP_HEADER_HOST, MULTICAST_ADDRESS)
232 .add_header(HTTP_HEADER_NT, &device.notification_type.to_string())
233 .add_header(HTTP_HEADER_NTS, NTS_BYE)
234 .add_header(HTTP_HEADER_USN, &device.service_name.to_string());
235
236 if options.spec_version >= SpecVersion::V11 {
237 message_builder
238 .add_header(HTTP_HEADER_BOOTID, &device.boot_id.to_string())
239 .add_header(HTTP_HEADER_CONFIGID, &device.config_id.to_string());
240 }
241
242 multicast_once(
243 &message_builder.into(),
244 &MULTICAST_ADDRESS.parse().unwrap(),
245 &options.into(),
246 )?;
247 device.boot_id = next_boot_id;
248 Ok(())
249}
250
251// ------------------------------------------------------------------------------------------------
252// Implementations
253// ------------------------------------------------------------------------------------------------
254
255const CACHE_CONTROL_MAX_AGE: u16 = 1800;
256
257impl Options {
258 pub fn default_for(spec_version: SpecVersion) -> Self {
259 Options {
260 spec_version,
261 network_interface: None,
262 network_version: None,
263 max_age: CACHE_CONTROL_MAX_AGE,
264 packet_ttl: if spec_version == SpecVersion::V10 {
265 4
266 } else {
267 2
268 },
269 product_and_version: None,
270 }
271 }
272}
273
274impl From<Options> for MulticastOptions {
275 fn from(options: Options) -> Self {
276 MulticastOptions {
277 network_interface: options.network_interface,
278 network_version: options.network_version,
279 packet_ttl: options.packet_ttl,
280 ..Default::default()
281 }
282 }
283}