1use anyhow::{bail, ensure, Context, Result};
10use quick_xml::events::Event;
11use quick_xml::Reader;
12use std::collections::HashMap;
13use std::net::{Ipv4Addr, SocketAddr};
14
15pub const PORT: u16 = 3702;
16pub const GROUP_V4: Ipv4Addr = Ipv4Addr::new(239, 255, 255, 250);
17pub const MAX_DATAGRAM: usize = 65_507;
19pub const MAX_DEPTH: usize = 32;
21pub const MAX_ITEMS: usize = 64;
23
24pub fn group() -> SocketAddr {
25 SocketAddr::from((GROUP_V4, PORT))
26}
27
28const NS_SOAP: &str = "http://www.w3.org/2003/05/soap-envelope";
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Version {
32 V2005,
34 V2009,
36}
37
38impl Version {
39 pub fn discovery_ns(self) -> &'static str {
40 match self {
41 Version::V2005 => "http://schemas.xmlsoap.org/ws/2005/04/discovery",
42 Version::V2009 => "http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01",
43 }
44 }
45 pub fn addressing_ns(self) -> &'static str {
46 match self {
47 Version::V2005 => "http://schemas.xmlsoap.org/ws/2004/08/addressing",
48 Version::V2009 => "http://www.w3.org/2005/08/addressing",
49 }
50 }
51 pub fn to_all(self) -> &'static str {
53 match self {
54 Version::V2005 => "urn:schemas-xmlsoap-org:ws:2005:04:discovery",
55 Version::V2009 => "urn:docs-oasis-open-org:ws-dd:ns:discovery:2009:01",
56 }
57 }
58 pub fn anonymous(self) -> &'static str {
60 match self {
61 Version::V2005 => "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous",
62 Version::V2009 => "http://www.w3.org/2005/08/addressing/anonymous",
63 }
64 }
65 pub fn name(self) -> &'static str {
66 match self {
67 Version::V2005 => "2005/04",
68 Version::V2009 => "2009/01",
69 }
70 }
71 pub fn parse(s: &str) -> Result<Self> {
72 match s {
73 "2005/04" | "2005" => Ok(Version::V2005),
74 "2009/01" | "2009" | "1.1" => Ok(Version::V2009),
75 other => bail!("WS-Discovery version {other:?} is not 2005/04 or 2009/01"),
76 }
77 }
78 fn of(ns: &str) -> Option<Self> {
79 [Version::V2005, Version::V2009]
80 .into_iter()
81 .find(|v| v.discovery_ns() == ns)
82 }
83}
84
85pub const WELL_KNOWN: &[(&str, &str)] = &[
89 ("wsdp", "http://schemas.xmlsoap.org/ws/2006/02/devprof"),
90 ("pub", "http://schemas.microsoft.com/windows/pub/2005/07"),
91 ("dn", "http://www.onvif.org/ver10/network/wsdl"),
92 ("tds", "http://www.onvif.org/ver10/device/wsdl"),
93];
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96pub struct QName {
97 pub ns: String,
98 pub local: String,
99}
100
101impl QName {
102 pub fn parse(s: &str) -> Result<Self> {
104 if let Some(rest) = s.strip_prefix('{') {
105 let (ns, local) = rest
106 .split_once('}')
107 .context("a type in Clark notation is {namespace}LocalName")?;
108 ensure!(
109 !ns.is_empty() && valid_ncname(local),
110 "{s:?} is not {{namespace}}LocalName"
111 );
112 return Ok(QName {
113 ns: ns.into(),
114 local: local.into(),
115 });
116 }
117 let (prefix, local) = s
118 .split_once(':')
119 .with_context(|| format!("{s:?} is not a type; write {{namespace}}LocalName"))?;
120 let ns = match WELL_KNOWN.iter().find(|(p, _)| *p == prefix) {
121 Some((_, ns)) => *ns,
122 None => bail!("prefix {prefix:?} is not one NetGet knows (wsdp, pub, dn, tds); write {{namespace}}{local}"),
123 };
124 ensure!(valid_ncname(local), "{local:?} is not an XML local name");
125 Ok(QName {
126 ns: ns.into(),
127 local: local.into(),
128 })
129 }
130 pub fn clark(&self) -> String {
131 format!("{{{}}}{}", self.ns, self.local)
132 }
133}
134
135fn valid_ncname(s: &str) -> bool {
136 let mut chars = s.chars();
137 matches!(chars.next(), Some(c) if c.is_alphabetic() || c == '_')
138 && chars.all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.'))
139}
140
141#[derive(Debug, Clone, Default, PartialEq)]
143pub struct Target {
144 pub endpoint: String,
145 pub types: Vec<QName>,
146 pub scopes: Vec<String>,
147 pub xaddrs: Vec<String>,
148 pub metadata_version: Option<u64>,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum Kind {
153 Probe,
154 ProbeMatches,
155 Resolve,
156 ResolveMatches,
157 Hello,
158 Bye,
159}
160
161impl Kind {
162 pub fn name(self) -> &'static str {
163 match self {
164 Kind::Probe => "Probe",
165 Kind::ProbeMatches => "ProbeMatches",
166 Kind::Resolve => "Resolve",
167 Kind::ResolveMatches => "ResolveMatches",
168 Kind::Hello => "Hello",
169 Kind::Bye => "Bye",
170 }
171 }
172 fn of(local: &str) -> Option<Self> {
173 [
174 Kind::Probe,
175 Kind::ProbeMatches,
176 Kind::Resolve,
177 Kind::ResolveMatches,
178 Kind::Hello,
179 Kind::Bye,
180 ]
181 .into_iter()
182 .find(|k| k.name() == local)
183 }
184}
185
186#[derive(Debug, Clone, PartialEq)]
187pub struct Message {
188 pub version: Version,
189 pub kind: Kind,
190 pub message_id: String,
191 pub relates_to: Option<String>,
192 pub types: Vec<QName>,
194 pub scopes: Vec<String>,
195 pub match_by: Option<String>,
196 pub endpoint: Option<String>,
198 pub targets: Vec<Target>,
200}
201
202fn local_and_prefix(name: &[u8]) -> (String, Option<String>) {
203 let s = String::from_utf8_lossy(name);
204 match s.split_once(':') {
205 Some((p, l)) => (l.to_string(), Some(p.to_string())),
206 None => (s.to_string(), None),
207 }
208}
209
210pub fn parse(datagram: &[u8]) -> Result<Message> {
213 ensure!(
214 datagram.len() <= MAX_DATAGRAM,
215 "a {}-byte datagram exceeds {MAX_DATAGRAM}",
216 datagram.len()
217 );
218 let text = std::str::from_utf8(datagram).context("the message is not UTF-8")?;
219 let mut reader = Reader::from_str(text);
220 reader.config_mut().trim_text(true);
221 let mut frames: Vec<HashMap<String, String>> = vec![HashMap::new()];
223 let mut path: Vec<(String, String)> = Vec::new();
224 let mut message_id = None;
225 let mut relates_to = None;
226 let mut action_body: Option<(Version, Kind)> = None;
227 let mut types = Vec::new();
228 let mut scopes = Vec::new();
229 let mut match_by = None;
230 let mut endpoint = None;
231 let mut targets: Vec<Target> = Vec::new();
232 let mut current: Option<Target> = None;
233 let resolve = |frames: &[HashMap<String, String>], prefix: Option<&str>| -> Option<String> {
234 let key = prefix.unwrap_or("");
235 frames.iter().rev().find_map(|f| f.get(key).cloned())
236 };
237 loop {
238 let event = reader
239 .read_event()
240 .context("the message is not well-formed XML")?;
241 match event {
242 Event::Start(ref e) | Event::Empty(ref e) => {
243 ensure!(
244 path.len() < MAX_DEPTH,
245 "elements nest more than {MAX_DEPTH} deep"
246 );
247 let mut frame = HashMap::new();
248 let mut attrs = HashMap::new();
249 for a in e.attributes() {
250 let a = a.context("a malformed attribute")?;
251 let key = String::from_utf8_lossy(a.key.as_ref()).to_string();
252 let value = a
253 .decode_and_unescape_value(reader.decoder())
254 .context("a malformed attribute value")?
255 .to_string();
256 if key == "xmlns" {
257 frame.insert(String::new(), value);
258 } else if let Some(p) = key.strip_prefix("xmlns:") {
259 frame.insert(p.to_string(), value);
260 } else {
261 attrs.insert(key, value);
262 }
263 }
264 frames.push(frame);
265 let (local, prefix) = local_and_prefix(e.name().as_ref());
266 let ns = resolve(&frames, prefix.as_deref()).unwrap_or_default();
267 let empty = matches!(event, Event::Empty(_));
268 let in_body =
269 path.len() == 2 && path[1] == (NS_SOAP.to_string(), "Body".to_string());
270 if in_body && action_body.is_none() {
271 if let (Some(v), Some(k)) = (Version::of(&ns), Kind::of(&local)) {
272 action_body = Some((v, k));
273 }
274 }
275 if let Some((v, k)) = action_body {
276 if ns == v.discovery_ns() {
277 let opens_target = matches!(
278 (k, local.as_str()),
279 (Kind::ProbeMatches, "ProbeMatch")
280 | (Kind::ResolveMatches, "ResolveMatch")
281 ) || (path.len() == 2
282 && matches!(k, Kind::Hello | Kind::Bye));
283 if opens_target {
284 ensure!(targets.len() < MAX_ITEMS, "more than {MAX_ITEMS} matches");
285 current = Some(Target::default());
286 }
287 if local == "Scopes" {
288 match_by = attrs.get("MatchBy").cloned();
289 }
290 }
291 }
292 path.push((ns, local));
293 if empty {
294 close(
295 &mut path,
296 &mut frames,
297 action_body,
298 &mut current,
299 &mut targets,
300 );
301 }
302 }
303 Event::Text(t) => {
304 let value = t.unescape().context("malformed text")?.trim().to_string();
305 let Some((ns, local)) = path.last().cloned() else {
306 continue;
307 };
308 let parent = path
309 .len()
310 .checked_sub(2)
311 .map(|i| path[i].1.clone())
312 .unwrap_or_default();
313 let items = || -> Result<Vec<String>> {
314 let v: Vec<String> = value.split_whitespace().map(str::to_string).collect();
315 ensure!(
316 v.len() <= MAX_ITEMS,
317 "a list of more than {MAX_ITEMS} items"
318 );
319 Ok(v)
320 };
321 let Some((version, kind)) = action_body else {
324 let addressing = ns == Version::V2005.addressing_ns()
325 || ns == Version::V2009.addressing_ns();
326 if addressing && local == "MessageID" {
327 message_id = Some(value);
328 } else if addressing && local == "RelatesTo" {
329 relates_to = Some(value);
330 }
331 continue;
332 };
333 if ns == version.discovery_ns() {
334 let slot = current.as_mut();
335 match (local.as_str(), slot) {
336 ("Types", Some(t)) => t.types = qnames(&items()?, &frames)?,
337 ("Types", None) => types = qnames(&items()?, &frames)?,
338 ("Scopes", Some(t)) => t.scopes = items()?,
339 ("Scopes", None) => scopes = items()?,
340 ("XAddrs", Some(t)) => t.xaddrs = items()?,
341 ("MetadataVersion", Some(t)) => t.metadata_version = value.parse().ok(),
342 _ => {}
343 }
344 } else if local == "Address"
345 && ns == version.addressing_ns()
346 && parent == "EndpointReference"
347 {
348 match current.as_mut() {
349 Some(t) => t.endpoint = value,
350 None if kind == Kind::Resolve => endpoint = Some(value),
351 None => {}
352 }
353 }
354 }
355 Event::End(_) => close(
356 &mut path,
357 &mut frames,
358 action_body,
359 &mut current,
360 &mut targets,
361 ),
362 Event::Eof => break,
363 _ => {}
364 }
365 }
366 let (version, kind) = action_body.context(
367 "not a WS-Discovery message (no Probe, Resolve, Hello, Bye or matches in the body)",
368 )?;
369 if kind == Kind::Bye {
370 endpoint = targets.first().map(|t| t.endpoint.clone());
371 }
372 Ok(Message {
373 version,
374 kind,
375 message_id: message_id.context("the message has no MessageID")?,
376 relates_to,
377 types,
378 scopes,
379 match_by,
380 endpoint,
381 targets,
382 })
383}
384
385fn close(
386 path: &mut Vec<(String, String)>,
387 frames: &mut Vec<HashMap<String, String>>,
388 action_body: Option<(Version, Kind)>,
389 current: &mut Option<Target>,
390 targets: &mut Vec<Target>,
391) {
392 let closed = path.pop();
393 frames.pop();
394 if let (Some((ns, local)), Some((v, k))) = (closed, action_body) {
395 let closes_target = ns == v.discovery_ns()
396 && (matches!(
397 (k, local.as_str()),
398 (Kind::ProbeMatches, "ProbeMatch") | (Kind::ResolveMatches, "ResolveMatch")
399 ) || (path.len() == 2
400 && matches!(k, Kind::Hello | Kind::Bye)
401 && local == k.name()));
402 if closes_target {
403 if let Some(t) = current.take() {
404 targets.push(t);
405 }
406 }
407 }
408}
409
410fn qnames(items: &[String], frames: &[HashMap<String, String>]) -> Result<Vec<QName>> {
411 items
412 .iter()
413 .map(|item| {
414 let (prefix, local) = item.split_once(':').unwrap_or(("", item.as_str()));
415 let ns = frames
416 .iter()
417 .rev()
418 .find_map(|f| f.get(prefix).cloned())
419 .with_context(|| format!("type {item:?} uses an undeclared prefix"))?;
420 Ok(QName {
421 ns,
422 local: local.to_string(),
423 })
424 })
425 .collect()
426}
427
428fn esc(s: &str) -> String {
429 quick_xml::escape::escape(s).into_owned()
430}
431
432fn envelope(
434 v: Version,
435 action: &str,
436 message_id: &str,
437 to: &str,
438 extra_header: &str,
439 body: &str,
440) -> String {
441 format!(
442 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\
443<s:Envelope xmlns:s=\"{NS_SOAP}\" xmlns:a=\"{a}\" xmlns:d=\"{d}\">\
444<s:Header><a:Action>{d}/{action}</a:Action><a:MessageID>{id}</a:MessageID><a:To>{to}</a:To>{extra_header}</s:Header>\
445<s:Body>{body}</s:Body></s:Envelope>",
446 a = v.addressing_ns(),
447 d = v.discovery_ns(),
448 id = esc(message_id),
449 to = esc(to),
450 )
451}
452
453fn types_xml(types: &[QName]) -> String {
454 if types.is_empty() {
455 return String::new();
456 }
457 let mut declared: Vec<(String, &str)> = Vec::new();
458 let mut values = Vec::new();
459 for t in types {
460 let prefix = match declared.iter().find(|(_, ns)| *ns == t.ns) {
461 Some((p, _)) => p.clone(),
462 None => {
463 let p = WELL_KNOWN
464 .iter()
465 .find(|(_, ns)| *ns == t.ns)
466 .map(|(p, _)| p.to_string())
467 .unwrap_or_else(|| format!("n{}", declared.len()));
468 declared.push((p.clone(), &t.ns));
469 p
470 }
471 };
472 values.push(format!("{prefix}:{}", t.local));
473 }
474 let decls: String = declared
475 .iter()
476 .map(|(p, ns)| format!(" xmlns:{p}=\"{}\"", esc(ns)))
477 .collect();
478 format!("<d:Types{decls}>{}</d:Types>", values.join(" "))
479}
480
481fn list_xml(element: &str, items: &[String], attrs: &str) -> String {
482 if items.is_empty() {
483 return String::new();
484 }
485 format!(
486 "<d:{element}{attrs}>{}</d:{element}>",
487 esc(&items.join(" "))
488 )
489}
490
491fn target_xml(t: &Target) -> String {
492 format!(
493 "<a:EndpointReference><a:Address>{}</a:Address></a:EndpointReference>{}{}{}<d:MetadataVersion>{}</d:MetadataVersion>",
494 esc(&t.endpoint),
495 types_xml(&t.types),
496 list_xml("Scopes", &t.scopes, ""),
497 list_xml("XAddrs", &t.xaddrs, ""),
498 t.metadata_version.unwrap_or(1)
499 )
500}
501
502fn app_sequence(instance_id: u64, number: u64) -> String {
503 format!("<d:AppSequence InstanceId=\"{instance_id}\" MessageNumber=\"{number}\"/>")
504}
505
506pub fn new_message_id() -> String {
507 format!("urn:uuid:{}", uuid::Uuid::new_v4())
508}
509
510pub fn matches(
512 v: Version,
513 kind: Kind,
514 relates_to: &str,
515 targets: &[Target],
516 instance_id: u64,
517 number: u64,
518) -> Result<String> {
519 let (wrapper, each) = match kind {
520 Kind::ProbeMatches => ("ProbeMatches", "ProbeMatch"),
521 Kind::ResolveMatches => ("ResolveMatches", "ResolveMatch"),
522 other => bail!("{} is not a matches message", other.name()),
523 };
524 ensure!(targets.len() <= MAX_ITEMS, "at most {MAX_ITEMS} matches");
525 let body: String = targets
526 .iter()
527 .map(|t| format!("<d:{each}>{}</d:{each}>", target_xml(t)))
528 .collect();
529 let header = format!(
530 "<a:RelatesTo>{}</a:RelatesTo>{}",
531 esc(relates_to),
532 app_sequence(instance_id, number)
533 );
534 Ok(envelope(
535 v,
536 wrapper,
537 &new_message_id(),
538 v.anonymous(),
539 &header,
540 &format!("<d:{wrapper}>{body}</d:{wrapper}>"),
541 ))
542}
543
544pub fn probe(
545 v: Version,
546 message_id: &str,
547 types: &[QName],
548 scopes: &[String],
549 match_by: Option<&str>,
550) -> String {
551 let attrs = match_by
552 .map(|m| format!(" MatchBy=\"{}\"", esc(m)))
553 .unwrap_or_default();
554 envelope(
555 v,
556 "Probe",
557 message_id,
558 v.to_all(),
559 "",
560 &format!(
561 "<d:Probe>{}{}</d:Probe>",
562 types_xml(types),
563 list_xml("Scopes", scopes, &attrs)
564 ),
565 )
566}
567
568pub fn resolve(v: Version, message_id: &str, endpoint: &str) -> String {
569 envelope(
570 v,
571 "Resolve",
572 message_id,
573 v.to_all(),
574 "",
575 &format!("<d:Resolve><a:EndpointReference><a:Address>{}</a:Address></a:EndpointReference></d:Resolve>", esc(endpoint)),
576 )
577}
578
579pub fn hello(v: Version, t: &Target, instance_id: u64, number: u64) -> String {
580 envelope(
581 v,
582 "Hello",
583 &new_message_id(),
584 v.to_all(),
585 &app_sequence(instance_id, number),
586 &format!("<d:Hello>{}</d:Hello>", target_xml(t)),
587 )
588}
589
590pub fn bye(v: Version, endpoint: &str, instance_id: u64, number: u64) -> String {
591 envelope(
592 v,
593 "Bye",
594 &new_message_id(),
595 v.to_all(),
596 &app_sequence(instance_id, number),
597 &format!(
598 "<d:Bye><a:EndpointReference><a:Address>{}</a:Address></a:EndpointReference></d:Bye>",
599 esc(endpoint)
600 ),
601 )
602}
603
604pub fn target_from_json(v: &serde_json::Value) -> Result<Target> {
606 let strings = |key: &str| -> Result<Vec<String>> {
607 match v.get(key) {
608 None | Some(serde_json::Value::Null) => Ok(vec![]),
609 Some(serde_json::Value::Array(a)) => {
610 ensure!(
611 a.len() <= MAX_ITEMS,
612 "{key} has more than {MAX_ITEMS} entries"
613 );
614 a.iter()
615 .map(|x| {
616 let s = x.as_str().with_context(|| format!("{key} holds strings"))?;
617 ensure!(
618 !s.is_empty() && !s.chars().any(char::is_whitespace),
619 "{key} entries are single URIs without spaces: {s:?}"
620 );
621 Ok(s.to_string())
622 })
623 .collect()
624 }
625 Some(_) => bail!("{key} must be an array of strings"),
626 }
627 };
628 let endpoint = v["endpoint_reference"]
629 .as_str()
630 .context("endpoint_reference is required, e.g. urn:uuid:...")?;
631 ensure!(
632 !endpoint.is_empty() && !endpoint.chars().any(char::is_whitespace),
633 "endpoint_reference is one URI"
634 );
635 Ok(Target {
636 endpoint: endpoint.into(),
637 types: strings("types")?
638 .iter()
639 .map(|s| QName::parse(s))
640 .collect::<Result<_>>()?,
641 scopes: strings("scopes")?,
642 xaddrs: strings("xaddrs")?,
643 metadata_version: match v.get("metadata_version") {
644 None | Some(serde_json::Value::Null) => None,
645 Some(x) => Some(x.as_u64().context("metadata_version is a whole number")?),
646 },
647 })
648}
649
650pub fn target_json(t: &Target) -> serde_json::Value {
651 serde_json::json!({
652 "endpoint_reference": crate::utils::sanitize::line_field(&t.endpoint),
653 "types": t.types.iter().map(|q| crate::utils::sanitize::line_field(&q.clark())).collect::<Vec<_>>(),
654 "scopes": t.scopes.iter().map(|s| crate::utils::sanitize::line_field(s)).collect::<Vec<_>>(),
655 "xaddrs": t.xaddrs.iter().map(|s| crate::utils::sanitize::line_field(s)).collect::<Vec<_>>(),
656 "metadata_version": t.metadata_version,
657 })
658}