1use 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#[derive(Clone, Debug)]
52pub enum SearchTarget {
53 All,
55 RootDevice,
57 Device(String),
59 DeviceType(String),
61 ServiceType(String),
63 DomainDeviceType(String, String),
65 DomainServiceType(String, String),
67}
68
69#[allow(dead_code)]
78type CallbackFn = fn(&Response) -> bool;
79
80#[derive(Clone, Debug)]
89pub struct Options {
90 pub spec_version: SpecVersion,
93 pub search_target: SearchTarget,
95 pub network_interface: Option<String>,
98 pub network_version: Option<IP>,
100 pub packet_ttl: u32,
102 pub max_wait_time: u8,
106 pub product_and_version: Option<ProductVersion>,
110 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#[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#[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
153pub fn search(options: Options) -> Result<ResponseCache, Error> {
174 info!("search - options: {:?}", options);
175 options.validate()?;
176 unsupported_operation("search").into()
177}
178
179pub 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 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 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 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
245pub 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
295impl 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
366impl Options {
369 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 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 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}
450const 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
569impl 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