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}