upnp_rs/discovery/
search.rs

1/*!
2This module provides three functions that provide 1) multicast search, 2) unicast search, and 3)
3multicast search with caching. The caching version of search will merge the set of new responses
4with any (non-expired) previously cached responses.
5
6# Specification
7
8TBD
9
10*/
11use crate::common::headers;
12use crate::common::httpu::{
13    multicast, Options as MulticastOptions, RequestBuilder, Response as MulticastResponse,
14};
15use crate::common::interface::IP;
16use crate::common::uri::{URI, URL};
17use crate::common::user_agent::user_agent_string;
18use crate::discovery::{ControlPoint, ProductVersion, ProductVersions};
19use crate::error::{
20    invalid_field_value, invalid_header_value, invalid_value_for_type, missing_required_field,
21    unsupported_operation, unsupported_version, Error, MessageFormatError,
22};
23use crate::syntax::{
24    HTTP_EXTENSION, HTTP_HEADER_BOOTID, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONFIGID,
25    HTTP_HEADER_CP_FN, HTTP_HEADER_CP_UUID, HTTP_HEADER_DATE, HTTP_HEADER_EXT, HTTP_HEADER_HOST,
26    HTTP_HEADER_LOCATION, HTTP_HEADER_MAN, HTTP_HEADER_MX, HTTP_HEADER_SEARCH_PORT,
27    HTTP_HEADER_SERVER, HTTP_HEADER_ST, HTTP_HEADER_TCP_PORT, HTTP_HEADER_USER_AGENT,
28    HTTP_HEADER_USN, HTTP_METHOD_SEARCH, MULTICAST_ADDRESS,
29};
30use crate::SpecVersion;
31use regex::Regex;
32use std::borrow::Borrow;
33use std::collections::HashMap;
34use std::convert::{TryFrom, TryInto};
35use std::fmt::{Display, Error as FmtError, Formatter};
36use std::net::SocketAddr;
37use std::str::FromStr;
38use std::time::{Duration, SystemTime};
39use tracing::{error, info, trace};
40
41// ------------------------------------------------------------------------------------------------
42// Public Types
43// ------------------------------------------------------------------------------------------------
44
45///
46/// `SearchTarget` corresponds to the set of values defined by the UDA `ST` header.
47///
48/// This type does not separate out the version of a device or service type, it does ensure
49/// that the ':' separator character is present in the combined value.
50///
51#[derive(Clone, Debug)]
52pub enum SearchTarget {
53    /// Corresponds to the value `ssdp:all`
54    All,
55    /// Corresponds to the value `upnp:rootdevice`
56    RootDevice,
57    /// Corresponds to the value `uuid:{device-UUID}`
58    Device(String),
59    /// Corresponds to the value `urn:schemas-upnp-org:device:{deviceType:ver}`
60    DeviceType(String),
61    /// Corresponds to the value `urn:schemas-upnp-org:service:{serviceType:ver}`
62    ServiceType(String),
63    /// Corresponds to the value `urn:{domain-name}:device:{deviceType:ver}`
64    DomainDeviceType(String, String),
65    /// Corresponds to the value `urn:{domain-name}:service:{serviceType:ver}`
66    DomainServiceType(String, String),
67}
68
69///
70/// If present on a search function it will be called once for each received response
71/// in addition to those responses being returned from the function.
72///
73/// The result is a boolean, if true the function will continue to process results,
74/// if false no further responses are processed and the search will only return results
75/// until this last one.
76///
77#[allow(dead_code)]
78type CallbackFn = fn(&Response) -> bool;
79
80///
81/// This type encapsulates a set of mostly optional values to be used to construct messages to
82/// send.
83///
84/// Defaults should be constructed with `Options::default_for`. Currently the only time a value
85/// is required is when the version is set to 2.0, a value **is** required for the control point.
86/// The `Options::for_control_point` will set the control point as well as the version number.
87///
88#[derive(Clone, Debug)]
89pub struct Options {
90    /// The specification that will be used to construct sent messages and to verify responses.
91    /// Default: `SpecVersion:V10`.
92    pub spec_version: SpecVersion,
93    /// The scope of the search to perform. Default: `SearchTarget::RootDevices`.
94    pub search_target: SearchTarget,
95    /// A specific network interface to bind to; if specified the default address for the interface
96    /// will be used, else the address `0.0.0.0:0` will be used. Default: `None`.
97    pub network_interface: Option<String>,
98    /// Denotes whether the implementation wants to only use IPv4, IPv6, or doesn't care.
99    pub network_version: Option<IP>,
100    /// The IP packet TTL value.
101    pub packet_ttl: u32,
102    /// The maximum wait time for devices to use in responding. This will also be used as the read
103    /// timeout on the underlying socket. This value **must** be between `0` and `120`;
104    /// default: `2`.
105    pub max_wait_time: u8,
106    /// If specified this is to be the `ProduceName/Version` component of the user agent string
107    /// the client will generate as part of sent messages. If not specified a default value based
108    /// on the name and version of this crate will be used. Default: `None`.
109    pub product_and_version: Option<ProductVersion>,
110    /// If specified this will be used to add certain control point values in the sent messages.
111    /// This value is **only** used by the 2.0 specification where it is required, otherwise it
112    /// will be ignores. Default: `None`.
113    pub control_point: Option<ControlPoint>,
114}
115
116#[derive(Clone, Debug)]
117struct CachedResponse {
118    response: Response,
119    #[allow(dead_code)]
120    expiration: SystemTime,
121}
122
123///
124/// A cache wrapping a set of responses.
125///
126#[derive(Clone, Debug)]
127pub struct ResponseCache {
128    #[allow(dead_code)]
129    options: Options,
130    #[allow(dead_code)]
131    minimum_refresh: Duration,
132    last_updated: SystemTime,
133    responses: Vec<CachedResponse>,
134}
135
136///
137/// A Single device response.
138///
139#[derive(Clone, Debug)]
140pub struct Response {
141    pub max_age: Duration,
142    pub date: String,
143    pub versions: ProductVersions,
144    pub search_target: SearchTarget,
145    pub service_name: URI,
146    pub location: URL,
147    pub boot_id: u64,
148    pub config_id: Option<u64>,
149    pub search_port: Option<u16>,
150    pub other_headers: HashMap<String, String>,
151}
152
153// ------------------------------------------------------------------------------------------------
154// Public Functions
155// ------------------------------------------------------------------------------------------------
156
157///
158/// Perform a multicast search but store the results in a cache that allows a client to keep
159/// the results around and use the `update` method to refresh the cache from the network.
160///
161/// The search function can be configured using the [`Options`](struct.Options.html) struct,
162/// although the defaults are reasonable for most clients.
163///
164/// # Specification
165///
166/// TBD
167///
168/// # Parameters
169///
170/// * `options` - protocol options such as the specification version to use and any network
171/// configuration values.
172///
173pub fn search(options: Options) -> Result<ResponseCache, Error> {
174    info!("search - options: {:?}", options);
175    options.validate()?;
176    unsupported_operation("search").into()
177}
178
179///
180/// Perform a multicast search but return the results immediately as a vector, not wrapped
181/// in a cache.
182///
183/// The search function can be configured using the [`Options`](struct.Options.html) struct,
184/// although the defaults are reasonable for most clients.
185///
186/// # Specification
187///
188/// TBD
189///
190/// # Parameters
191///
192/// * `options` - protocol options such as the specification version to use and any network
193/// configuration values.
194///
195///
196pub fn search_once(options: Options) -> Result<Vec<Response>, Error> {
197    info!("search_once - options: {:?}", options);
198    options.validate()?;
199    let mut message_builder = RequestBuilder::new(HTTP_METHOD_SEARCH);
200    // All headers from the original 1.0 specification.
201    message_builder
202        .add_header(HTTP_HEADER_HOST, MULTICAST_ADDRESS)
203        .add_header(HTTP_HEADER_MAN, HTTP_EXTENSION)
204        .add_header(HTTP_HEADER_MX, &format!("{}", options.max_wait_time))
205        .add_header(HTTP_HEADER_ST, &options.search_target.to_string());
206    // Headers added by 1.1 specification
207    if options.spec_version >= SpecVersion::V11 {
208        message_builder.add_header(
209            HTTP_HEADER_USER_AGENT,
210            &user_agent_string(options.spec_version, options.product_and_version.clone()),
211        );
212    }
213    // Headers added by 2.0 specification
214    if options.spec_version >= SpecVersion::V20 {
215        match &options.control_point {
216            Some(cp) => {
217                message_builder.add_header(HTTP_HEADER_CP_FN, &cp.friendly_name);
218                if let Some(uuid) = &cp.uuid {
219                    message_builder.add_header(HTTP_HEADER_CP_UUID, uuid);
220                }
221                if let Some(port) = cp.port {
222                    message_builder.add_header(HTTP_HEADER_TCP_PORT, &port.to_string());
223                }
224            }
225            None => {
226                error!("search_once - missing control point, required for UPnP/2.0");
227                return missing_required_field("control_point").into();
228            }
229        }
230    }
231    trace!("search_once - {:?}", &message_builder);
232    let raw_responses = multicast(
233        &message_builder.into(),
234        &MULTICAST_ADDRESS.parse().unwrap(),
235        &options.into(),
236    )?;
237
238    let mut responses: Vec<Response> = Vec::new();
239    for raw_response in raw_responses {
240        responses.push(raw_response.try_into()?);
241    }
242    Ok(responses)
243}
244
245///
246/// Perform a unicast search and return the results immediately as a vector, not wrapped
247/// in a cache.
248///
249/// The search function can be configured using the [`Options`](struct.Options.html) struct,
250/// although the defaults are reasonable for most clients.
251///
252/// # Specification
253///
254/// TBD
255///
256/// # Parameters
257///
258/// * `options` - protocol options such as the specification version to use and any network
259/// configuration values.
260/// * `device_address` - the address of the device to query.
261///
262///
263pub fn search_once_to_device(
264    options: Options,
265    device_address: SocketAddr,
266) -> Result<Vec<Response>, Error> {
267    info!(
268        "search_once_to_device - options: {:?}, device_address: {:?}",
269        options, device_address
270    );
271    options.validate()?;
272    if options.spec_version >= SpecVersion::V11 {
273        let mut message_builder = RequestBuilder::new(HTTP_METHOD_SEARCH);
274        message_builder
275            .add_header(HTTP_HEADER_HOST, MULTICAST_ADDRESS)
276            .add_header(HTTP_HEADER_MAN, HTTP_EXTENSION)
277            .add_header(HTTP_HEADER_ST, &options.search_target.to_string())
278            .add_header(
279                HTTP_HEADER_USER_AGENT,
280                &user_agent_string(options.spec_version, options.product_and_version.clone()),
281            );
282
283        let raw_responses = multicast(&message_builder.into(), &device_address, &options.into())?;
284
285        let mut responses: Vec<Response> = Vec::new();
286        for raw_response in raw_responses {
287            responses.push(raw_response.try_into()?);
288        }
289        Ok(responses)
290    } else {
291        unsupported_version(options.spec_version).into()
292    }
293}
294
295// ------------------------------------------------------------------------------------------------
296// Implementations
297// ------------------------------------------------------------------------------------------------
298
299impl Display for SearchTarget {
300    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
301        write!(
302            f,
303            "{}",
304            match self {
305                SearchTarget::All => "ssdp::all".to_string(),
306                SearchTarget::RootDevice => "upnp:rootdevice".to_string(),
307                SearchTarget::Device(device) => format!("uuid:{}", device),
308                SearchTarget::DeviceType(device) =>
309                    format!("urn:schemas-upnp-org:device:{}", device),
310                SearchTarget::ServiceType(service) =>
311                    format!("urn:schemas-upnp-org:service:{}", service),
312                SearchTarget::DomainDeviceType(domain, device) =>
313                    format!("urn:{}:device:{}", domain, device),
314                SearchTarget::DomainServiceType(domain, service) =>
315                    format!("urn:{}:service:{}", domain, service),
316            }
317        )
318    }
319}
320
321impl FromStr for SearchTarget {
322    type Err = MessageFormatError;
323
324    fn from_str(s: &str) -> Result<Self, Self::Err> {
325        lazy_static! {
326            static ref DOMAIN_URN: Regex =
327                Regex::new(r"^urn:([^:]+):(device|service):(.+)$").unwrap();
328        }
329        if s == "ssdp::all" {
330            Ok(SearchTarget::All)
331        } else if s == "upnp:rootdevice" {
332            Ok(SearchTarget::RootDevice)
333        } else if let Some(device) = s.strip_prefix("uuid:") {
334            Ok(SearchTarget::Device(device.to_string()))
335        } else if let Some(device_type) = s.strip_prefix("urn:schemas-upnp-org:device:") {
336            Ok(SearchTarget::DeviceType(device_type.to_string()))
337        } else if let Some(service_type) = s.strip_prefix("urn:schemas-upnp-org:service:") {
338            Ok(SearchTarget::ServiceType(service_type.to_string()))
339        } else if let Some(domain) = s.strip_prefix("urn:") {
340            match DOMAIN_URN.captures(domain) {
341                Some(captures) => {
342                    if captures.get(2).unwrap().as_str() == "device" {
343                        Ok(SearchTarget::DomainDeviceType(
344                            captures.get(1).unwrap().as_str().to_string(),
345                            captures.get(3).unwrap().as_str().to_string(),
346                        ))
347                    } else {
348                        Ok(SearchTarget::DomainServiceType(
349                            captures.get(1).unwrap().as_str().to_string(),
350                            captures.get(3).unwrap().as_str().to_string(),
351                        ))
352                    }
353                }
354                None => {
355                    error!("Could not parse URN '{}'", s);
356                    invalid_value_for_type("URN", s).into()
357                }
358            }
359        } else {
360            error!("Could not parse '{}' as a search target", s);
361            invalid_value_for_type("SearchTarget", s).into()
362        }
363    }
364}
365
366// ------------------------------------------------------------------------------------------------
367
368impl Options {
369    ///
370    /// Construct an options object for the given specification version.
371    ///
372    pub fn default_for(spec_version: SpecVersion) -> Self {
373        Options {
374            spec_version,
375            network_interface: None,
376            network_version: None,
377            search_target: SearchTarget::RootDevice,
378            packet_ttl: if spec_version == SpecVersion::V10 {
379                4
380            } else {
381                2
382            },
383            max_wait_time: 2,
384            product_and_version: None,
385            control_point: None,
386        }
387    }
388
389    ///
390    /// Construct an options object for the given control point.
391    ///
392    pub fn for_control_point(control_point: ControlPoint) -> Self {
393        let mut new = Self::default_for(SpecVersion::V20);
394        new.control_point = Some(control_point);
395        new
396    }
397
398    ///
399    /// Validate all options, ensuring values as well as version-specific rules.
400    ///
401    pub fn validate(&self) -> Result<(), Error> {
402        lazy_static! {
403            static ref UA_VERSION: Regex = Regex::new(r"^[\d\.]+$").unwrap();
404        }
405        if self.max_wait_time < 1 || self.max_wait_time > 120 {
406            error!(
407                "validate - max_wait_time must be between 1..120 ({})",
408                self.max_wait_time
409            );
410            return invalid_field_value("max_wait_time", &self.max_wait_time.to_string()).into();
411        }
412        if self.spec_version >= SpecVersion::V11 {
413            if let Some(user_agent) = &self.product_and_version {
414                if user_agent.name.contains('/') || !UA_VERSION.is_match(&user_agent.version) {
415                    error!(
416                        "validate - user_agent needs to match 'ProductName/Version' ({:?})",
417                        user_agent
418                    );
419                    return invalid_field_value("UserAgent", &user_agent.to_string()).into();
420                }
421            }
422        }
423        if self.spec_version >= SpecVersion::V20 {
424            if self.control_point.is_none() {
425                error!("validate - control_point required");
426                return missing_required_field("ControlPoint").into();
427            } else if let Some(control_point) = &self.control_point {
428                if control_point.friendly_name.is_empty() {
429                    error!("validate - control_point.friendly_name required");
430                    return invalid_field_value("ControlPoint", &control_point.friendly_name)
431                        .into();
432                }
433            }
434        }
435        Ok(())
436    }
437}
438
439impl From<Options> for MulticastOptions {
440    fn from(options: Options) -> Self {
441        MulticastOptions {
442            network_interface: options.network_interface,
443            network_version: options.network_version,
444            packet_ttl: options.packet_ttl,
445            recv_timeout: options.max_wait_time as u64,
446            ..Default::default()
447        }
448    }
449}
450// ------------------------------------------------------------------------------------------------
451
452const REQUIRED_HEADERS_V10: [&str; 7] = [
453    HTTP_HEADER_CACHE_CONTROL,
454    HTTP_HEADER_DATE,
455    HTTP_HEADER_EXT,
456    HTTP_HEADER_LOCATION,
457    HTTP_HEADER_SERVER,
458    HTTP_HEADER_ST,
459    HTTP_HEADER_USN,
460];
461
462impl TryFrom<MulticastResponse> for Response {
463    type Error = Error;
464
465    fn try_from(response: MulticastResponse) -> Result<Self, Self::Error> {
466        lazy_static! {
467            static ref UA_ALL: Regex =
468                Regex::new(r"^([^/]+)/([\d\.]+),?[ ]+([^/]+)/([\d\.]+),?[ ]+([^/]+)/([\d\.]+)$")
469                    .unwrap();
470        }
471        headers::check_required(&response.headers, &REQUIRED_HEADERS_V10)?;
472        headers::check_empty(
473            response.headers.get(HTTP_HEADER_EXT).unwrap(),
474            HTTP_HEADER_EXT,
475        )?;
476
477        let server = response.headers.get(HTTP_HEADER_SERVER).unwrap();
478        let versions = match UA_ALL.captures(server) {
479            Some(captures) => ProductVersions {
480                product: ProductVersion {
481                    name: captures.get(5).unwrap().as_str().to_string(),
482                    version: captures.get(6).unwrap().as_str().to_string(),
483                },
484                upnp: ProductVersion {
485                    name: captures.get(3).unwrap().as_str().to_string(),
486                    version: captures.get(4).unwrap().as_str().to_string(),
487                },
488                platform: ProductVersion {
489                    name: captures.get(1).unwrap().as_str().to_string(),
490                    version: captures.get(2).unwrap().as_str().to_string(),
491                },
492            },
493            None => {
494                error!("invalid value for server header '{}", server);
495                return invalid_field_value(HTTP_HEADER_SERVER, server).into();
496            }
497        };
498
499        let max_age = headers::check_parsed_value::<u64>(
500            &headers::check_regex(
501                response.headers.get(HTTP_HEADER_CACHE_CONTROL).unwrap(),
502                HTTP_HEADER_CACHE_CONTROL,
503                &Regex::new(r"max-age[ ]*=[ ]*(\d+)").unwrap(),
504            )?,
505            HTTP_HEADER_CACHE_CONTROL,
506        )?;
507
508        let date = headers::check_not_empty(
509            response.headers.get(HTTP_HEADER_DATE),
510            "Thu, 01 Jan 1970 00:00:00 GMT",
511        );
512
513        let location = headers::check_not_empty(
514            response.headers.get(HTTP_HEADER_LOCATION),
515            "http://www.example.org",
516        );
517
518        let service_name =
519            headers::check_not_empty(response.headers.get(HTTP_HEADER_USN), "undefined");
520
521        let search_target =
522            headers::check_not_empty(response.headers.get(HTTP_HEADER_ST), "undefined");
523
524        let mut boot_id = 0u64;
525        let mut config_id: Option<u64> = None;
526        let mut search_port: Option<u16> = None;
527        if versions.upnp.version == SpecVersion::V20.to_string() {
528            boot_id = headers::check_parsed_value::<u64>(
529                response
530                    .headers
531                    .get(HTTP_HEADER_BOOTID)
532                    .unwrap_or(&"0".to_string()),
533                HTTP_HEADER_BOOTID,
534            )?;
535            if let Some(s) = response.headers.get(HTTP_HEADER_CONFIGID) {
536                config_id = s.parse::<u64>().ok();
537            }
538            if let Some(s) = response.headers.get(HTTP_HEADER_SEARCH_PORT) {
539                search_port = s.parse::<u16>().ok();
540            }
541        }
542
543        let remaining_headers: HashMap<String, String> = response
544            .headers
545            .clone()
546            .iter()
547            .filter(|(k, _)| !REQUIRED_HEADERS_V10.contains(&k.as_str()))
548            .map(|(k, v)| (k.clone(), v.clone()))
549            .collect();
550
551        Ok(Response {
552            max_age: Duration::from_secs(max_age),
553            date,
554            versions,
555            location: URI::from_str(&location)
556                .map_err(|_| invalid_header_value(HTTP_HEADER_LOCATION, &location))?,
557            search_target: SearchTarget::from_str(&search_target)
558                .map_err(|_| invalid_field_value("SearchTarget", search_target))?,
559            service_name: URI::from_str(&service_name)
560                .map_err(|_| invalid_field_value("URI", service_name))?,
561            boot_id,
562            config_id,
563            search_port,
564            other_headers: remaining_headers,
565        })
566    }
567}
568
569// ------------------------------------------------------------------------------------------------
570
571impl ResponseCache {
572    pub fn refresh(&mut self) -> Self {
573        self.to_owned()
574    }
575
576    pub fn last_updated(self) -> SystemTime {
577        self.last_updated
578    }
579
580    pub fn responses(&self) -> Vec<&Response> {
581        self.responses.iter().map(|r| r.response.borrow()).collect()
582    }
583}
584
585// ------------------------------------------------------------------------------------------------
586// Private Functions
587// ------------------------------------------------------------------------------------------------
588
589//fn callback_wrapper(inner: &CallbackFn) -> bool {
590//    false
591//}