1use anyhow::{bail, ensure, Context, Result};
6use base64::Engine;
7use quick_xml::escape::escape;
8use quick_xml::events::Event as XmlEvent;
9use quick_xml::name::ResolveResult;
10use quick_xml::NsReader;
11use serde_json::{json, Map, Value};
12use sha1::{Digest, Sha1};
13
14pub const MAX_REQUEST: usize = 256 * 1024;
16pub const MAX_RESPONSE: usize = 1024 * 1024;
19pub const MAX_DEPTH: usize = 32;
21pub const MAX_ELEMENTS: usize = 20_000;
23pub const MAX_ITEMS: usize = 64;
25pub const MAX_NONCE: usize = 128;
27
28pub const ENV: &str = "http://www.w3.org/2003/05/soap-envelope";
29pub const ENV11: &str = "http://schemas.xmlsoap.org/soap/envelope/";
30pub const TDS: &str = "http://www.onvif.org/ver10/device/wsdl";
31pub const TRT: &str = "http://www.onvif.org/ver10/media/wsdl";
32pub const TT: &str = "http://www.onvif.org/ver10/schema";
33pub const TER: &str = "http://www.onvif.org/ver10/error";
34pub const WSSE: &str =
35 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
36pub const WSU: &str =
37 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
38pub const PASSWORD_DIGEST: &str =
39 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest";
40pub const BASE64_BINARY: &str =
41 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary";
42
43pub const DEVICE_PATH: &str = "/onvif/device_service";
45pub const MEDIA_PATH: &str = "/onvif/media_service";
46
47#[derive(Debug, Default, Clone)]
48pub struct Element {
49 pub ns: String,
51 pub name: String,
52 pub attrs: Vec<(String, String)>,
54 pub children: Vec<Element>,
55 pub text: String,
56}
57
58impl Element {
59 pub fn child(&self, name: &str) -> Option<&Element> {
60 self.children.iter().find(|c| c.name == name)
61 }
62 pub fn children_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Element> {
63 self.children.iter().filter(move |c| c.name == name)
64 }
65 pub fn text_of(&self, name: &str) -> Option<String> {
66 self.child(name).map(|c| c.text.trim().to_string())
67 }
68 pub fn attr(&self, name: &str) -> Option<&str> {
69 self.attrs
70 .iter()
71 .find(|(k, _)| k == name)
72 .map(|(_, v)| v.as_str())
73 }
74 pub fn path(&self, names: &[&str]) -> Option<String> {
76 let mut e = self;
77 for n in names {
78 e = e.child(n)?;
79 }
80 Some(e.text.trim().to_string())
81 }
82}
83
84fn local(name: &[u8]) -> String {
85 let s = String::from_utf8_lossy(name);
86 s.rsplit(':').next().unwrap_or_default().to_string()
87}
88
89pub fn tree(xml: &[u8], max: usize) -> Result<Element> {
92 ensure!(xml.len() <= max, "document larger than {max} bytes");
93 let mut reader = NsReader::from_reader(xml);
94 reader.config_mut().trim_text(false);
95 let mut stack: Vec<Element> = vec![Element::default()];
96 let mut count = 0usize;
97 let mut buf = Vec::new();
98 loop {
99 let (ns, ev) = reader
100 .read_resolved_event_into(&mut buf)
101 .context("the document is not well-formed XML")?;
102 let ns = match ns {
103 ResolveResult::Bound(n) => String::from_utf8_lossy(n.as_ref()).into_owned(),
104 ResolveResult::Unbound => String::new(),
105 ResolveResult::Unknown(p) => {
106 bail!(
107 "undeclared namespace prefix {}",
108 String::from_utf8_lossy(&p)
109 )
110 }
111 };
112 let empty = matches!(ev, XmlEvent::Empty(_));
113 match ev {
114 XmlEvent::Start(e) | XmlEvent::Empty(e) => {
115 count += 1;
116 ensure!(count <= MAX_ELEMENTS, "more than {MAX_ELEMENTS} elements");
117 ensure!(stack.len() <= MAX_DEPTH, "nested deeper than {MAX_DEPTH}");
118 let mut el = Element {
119 ns,
120 name: local(e.local_name().as_ref()),
121 ..Default::default()
122 };
123 for a in e.attributes() {
124 let a = a.context("a malformed attribute")?;
125 let v = a
126 .decode_and_unescape_value(reader.decoder())
127 .context("a malformed attribute value")?
128 .into_owned();
129 el.attrs.push((local(a.key.local_name().as_ref()), v));
130 }
131 if empty {
132 if let Some(parent) = stack.last_mut() {
133 parent.children.push(el);
134 }
135 } else {
136 stack.push(el);
137 }
138 }
139 XmlEvent::End(_) => {
140 ensure!(stack.len() > 1, "an end tag with nothing open");
141 let el = stack.pop().unwrap_or_default();
142 if let Some(parent) = stack.last_mut() {
143 parent.children.push(el);
144 }
145 }
146 XmlEvent::Text(t) => {
147 if let Some(top) = stack.last_mut() {
148 top.text
149 .push_str(&t.unescape().context("a bad character reference")?);
150 }
151 }
152 XmlEvent::CData(t) => {
153 if let Some(top) = stack.last_mut() {
154 top.text.push_str(&String::from_utf8_lossy(&t));
155 }
156 }
157 XmlEvent::DocType(_) => bail!("a DOCTYPE is not allowed in a SOAP envelope"),
158 XmlEvent::Eof => break,
159 _ => {}
160 }
161 buf.clear();
162 }
163 ensure!(stack.len() == 1, "the document ends inside an element");
164 stack
165 .pop()
166 .unwrap_or_default()
167 .children
168 .into_iter()
169 .next()
170 .context("an empty document")
171}
172
173#[derive(Debug, Default, Clone)]
175pub struct UsernameToken {
176 pub username: String,
177 pub password: String,
178 pub password_type: Option<String>,
179 pub nonce: Option<String>,
180 pub nonce_encoding: Option<String>,
181 pub created: Option<String>,
182}
183
184#[derive(Debug)]
187pub struct Request {
188 pub soap12: bool,
190 pub token: Option<UsernameToken>,
191 pub ns: String,
193 pub operation: String,
194 pub body: Element,
195}
196
197pub fn parse_request(xml: &[u8]) -> Result<Request> {
198 let env = tree(xml, MAX_REQUEST)?;
199 ensure!(
200 env.name == "Envelope" && (env.ns == ENV || env.ns == ENV11),
201 "not a SOAP envelope"
202 );
203 let token = env
204 .child("Header")
205 .and_then(|h| {
206 h.children
207 .iter()
208 .find(|c| c.name == "Security" && c.ns == WSSE)
209 })
210 .and_then(|s| s.child("UsernameToken"))
211 .map(|t| UsernameToken {
212 username: t.text_of("Username").unwrap_or_default(),
213 password: t.text_of("Password").unwrap_or_default(),
214 password_type: t
215 .child("Password")
216 .and_then(|p| p.attr("Type"))
217 .map(str::to_string),
218 nonce: t.text_of("Nonce"),
219 nonce_encoding: t
220 .child("Nonce")
221 .and_then(|n| n.attr("EncodingType"))
222 .map(str::to_string),
223 created: t.text_of("Created"),
224 });
225 let body = env.child("Body").context("the envelope has no Body")?;
226 let op = body.children.first().context("the Body is empty")?.clone();
227 Ok(Request {
228 soap12: env.ns == ENV,
229 token,
230 ns: op.ns.clone(),
231 operation: op.name.clone(),
232 body: op,
233 })
234}
235
236pub fn password_digest(nonce: &[u8], created: &str, password: &str) -> String {
239 let mut h = Sha1::new();
240 h.update(nonce);
241 h.update(created.as_bytes());
242 h.update(password.as_bytes());
243 base64::engine::general_purpose::STANDARD.encode(h.finalize())
244}
245
246fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
247 if a.len() != b.len() {
248 return false;
249 }
250 a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
251}
252
253pub type Refusal = &'static str;
255
256pub fn verify_token(
260 token: Option<&UsernameToken>,
261 username: &str,
262 password: &str,
263 now: chrono::DateTime<chrono::Utc>,
264 max_skew: chrono::Duration,
265) -> std::result::Result<Vec<u8>, Refusal> {
266 let t = token.ok_or("no_username_token")?;
267 match t.password_type.as_deref() {
268 Some(PASSWORD_DIGEST) => {}
269 Some(other) if other.ends_with("#PasswordDigest") => {}
270 _ => return Err("password_not_digest"),
271 }
272 if !t
274 .nonce_encoding
275 .as_deref()
276 .is_none_or(|e| e.ends_with("#Base64Binary"))
277 {
278 return Err("nonce_not_base64");
279 }
280 let nonce = base64::engine::general_purpose::STANDARD
281 .decode(t.nonce.as_deref().ok_or("no_nonce")?.trim())
282 .map_err(|_| "nonce_not_base64")?;
283 if nonce.is_empty() || nonce.len() > MAX_NONCE {
284 return Err("bad_nonce_length");
285 }
286 let created = t.created.as_deref().ok_or("no_created")?;
287 if created.len() > 64 {
288 return Err("bad_created");
289 }
290 let when = chrono::DateTime::parse_from_rfc3339(created)
291 .map_err(|_| "bad_created")?
292 .with_timezone(&chrono::Utc);
293 if (now - when).abs() > max_skew {
294 return Err("stale_created");
295 }
296 let expected = password_digest(&nonce, created, password);
297 let user_ok = constant_time_eq(t.username.as_bytes(), username.as_bytes());
298 let digest_ok = constant_time_eq(t.password.trim().as_bytes(), expected.as_bytes());
299 if !user_ok {
300 return Err("unknown_user");
301 }
302 if !digest_ok {
303 return Err("bad_digest");
304 }
305 Ok(nonce)
306}
307
308pub fn esc(s: &str) -> String {
309 escape(s).into_owned()
310}
311
312pub fn envelope(body: &str) -> String {
314 format!(
315 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<env:Envelope xmlns:env=\"{ENV}\" xmlns:tds=\"{TDS}\" xmlns:trt=\"{TRT}\" xmlns:tt=\"{TT}\" xmlns:ter=\"{TER}\"><env:Body>{body}</env:Body></env:Envelope>"
316 )
317}
318
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum FaultCode {
321 Sender,
322 Receiver,
323 VersionMismatch,
324}
325
326impl FaultCode {
327 pub fn name(self) -> &'static str {
328 match self {
329 FaultCode::Sender => "Sender",
330 FaultCode::Receiver => "Receiver",
331 FaultCode::VersionMismatch => "VersionMismatch",
332 }
333 }
334}
335
336pub fn fault(code: FaultCode, subcodes: &[&str], reason: &str) -> String {
338 let mut sub = String::new();
339 for s in subcodes.iter().rev() {
340 sub = format!(
341 "<env:Subcode><env:Value>ter:{}</env:Value>{sub}</env:Subcode>",
342 esc(s)
343 );
344 }
345 format!(
346 "<env:Fault><env:Code><env:Value>env:{}</env:Value>{sub}</env:Code><env:Reason><env:Text xml:lang=\"en\">{}</env:Text></env:Reason></env:Fault>",
347 code.name(),
348 esc(reason)
349 )
350}
351
352fn s<'a>(v: &'a Value, k: &str) -> &'a str {
353 v[k].as_str().unwrap_or_default()
354}
355
356fn n(v: &Value, k: &str, default: u64) -> u64 {
357 v[k].as_u64().unwrap_or(default)
358}
359
360fn b(v: &Value, k: &str, default: bool) -> bool {
361 v[k].as_bool().unwrap_or(default)
362}
363
364fn items<'a>(v: &'a Value, k: &str) -> impl Iterator<Item = &'a Value> {
365 v[k].as_array().into_iter().flatten().take(MAX_ITEMS)
366}
367
368pub fn device_information(v: &Value) -> String {
369 format!(
370 "<tds:GetDeviceInformationResponse><tds:Manufacturer>{}</tds:Manufacturer><tds:Model>{}</tds:Model><tds:FirmwareVersion>{}</tds:FirmwareVersion><tds:SerialNumber>{}</tds:SerialNumber><tds:HardwareId>{}</tds:HardwareId></tds:GetDeviceInformationResponse>",
371 esc(s(v, "manufacturer")),
372 esc(s(v, "model")),
373 esc(s(v, "firmware_version")),
374 esc(s(v, "serial_number")),
375 esc(s(v, "hardware_id"))
376 )
377}
378
379pub fn scopes(v: &Value) -> String {
380 let mut out = String::from("<tds:GetScopesResponse>");
381 for scope in items(v, "scopes") {
382 out.push_str(&format!(
383 "<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>{}</tt:ScopeItem></tds:Scopes>",
384 esc(scope.as_str().unwrap_or_default())
385 ));
386 }
387 out.push_str("</tds:GetScopesResponse>");
388 out
389}
390
391pub fn network_interfaces(v: &Value) -> String {
392 let mut out = String::from("<tds:GetNetworkInterfacesResponse>");
393 for i in items(v, "interfaces") {
394 let dhcp = b(i, "dhcp", false);
395 let address = format!(
396 "<tt:Address>{}</tt:Address><tt:PrefixLength>{}</tt:PrefixLength>",
397 esc(s(i, "ipv4_address")),
398 n(i, "prefix_length", 24)
399 );
400 out.push_str(&format!(
401 "<tds:NetworkInterfaces token=\"{}\"><tt:Enabled>{}</tt:Enabled><tt:Info><tt:Name>{}</tt:Name><tt:HwAddress>{}</tt:HwAddress><tt:MTU>{}</tt:MTU></tt:Info><tt:IPv4><tt:Enabled>true</tt:Enabled><tt:Config>{}<tt:DHCP>{dhcp}</tt:DHCP></tt:Config></tt:IPv4></tds:NetworkInterfaces>",
402 esc(s(i, "token")),
403 b(i, "enabled", true),
404 esc(s(i, "name")),
405 esc(s(i, "mac")),
406 n(i, "mtu", 1500),
407 if dhcp {
408 format!("<tt:FromDHCP>{address}</tt:FromDHCP>")
409 } else {
410 format!("<tt:Manual>{address}</tt:Manual>")
411 }
412 ));
413 }
414 out.push_str("</tds:GetNetworkInterfacesResponse>");
415 out
416}
417
418fn resolution(w: u64, h: u64) -> String {
419 format!("<tt:Resolution><tt:Width>{w}</tt:Width><tt:Height>{h}</tt:Height></tt:Resolution>")
420}
421
422pub fn profiles(v: &Value) -> String {
423 let mut out = String::from("<trt:GetProfilesResponse>");
424 for p in items(v, "profiles") {
425 let token = esc(s(p, "token"));
426 let source = match s(p, "video_source_token") {
427 "" => "video_source".to_string(),
428 t => esc(t),
429 };
430 let (w, h) = (n(p, "width", 1920), n(p, "height", 1080));
431 let fps = n(p, "framerate", 25);
432 let encoding = match s(p, "encoding") {
433 "" => "H264",
434 e => e,
435 };
436 let h264 = if encoding == "H264" {
437 format!("<tt:H264><tt:GovLength>{fps}</tt:GovLength><tt:H264Profile>Main</tt:H264Profile></tt:H264>")
438 } else {
439 String::new()
440 };
441 out.push_str(&format!(
442 "<trt:Profiles token=\"{token}\" fixed=\"true\"><tt:Name>{}</tt:Name><tt:VideoSourceConfiguration token=\"vsc_{source}\"><tt:Name>{source}</tt:Name><tt:UseCount>1</tt:UseCount><tt:SourceToken>{source}</tt:SourceToken><tt:Bounds x=\"0\" y=\"0\" width=\"{w}\" height=\"{h}\"/></tt:VideoSourceConfiguration><tt:VideoEncoderConfiguration token=\"vec_{token}\"><tt:Name>{token}</tt:Name><tt:UseCount>1</tt:UseCount><tt:Encoding>{}</tt:Encoding>{}<tt:Quality>{}</tt:Quality><tt:RateControl><tt:FrameRateLimit>{fps}</tt:FrameRateLimit><tt:EncodingInterval>1</tt:EncodingInterval><tt:BitrateLimit>{}</tt:BitrateLimit></tt:RateControl>{h264}<tt:Multicast><tt:Address><tt:Type>IPv4</tt:Type><tt:IPv4Address>0.0.0.0</tt:IPv4Address></tt:Address><tt:Port>0</tt:Port><tt:TTL>1</tt:TTL><tt:AutoStart>false</tt:AutoStart></tt:Multicast><tt:SessionTimeout>PT60S</tt:SessionTimeout></tt:VideoEncoderConfiguration></trt:Profiles>",
443 esc(match s(p, "name") { "" => s(p, "token"), name => name }),
444 esc(encoding),
445 resolution(w, h),
446 n(p, "quality", 5),
447 n(p, "bitrate", 4096),
448 ));
449 }
450 out.push_str("</trt:GetProfilesResponse>");
451 out
452}
453
454pub fn video_sources(v: &Value) -> String {
455 let mut out = String::from("<trt:GetVideoSourcesResponse>");
456 for src in items(v, "sources") {
457 out.push_str(&format!(
458 "<trt:VideoSources token=\"{}\"><tt:Framerate>{}</tt:Framerate>{}</trt:VideoSources>",
459 esc(s(src, "token")),
460 n(src, "framerate", 25),
461 resolution(n(src, "width", 1920), n(src, "height", 1080))
462 ));
463 }
464 out.push_str("</trt:GetVideoSourcesResponse>");
465 out
466}
467
468pub fn media_uri(operation: &str, v: &Value) -> String {
470 format!(
471 "<trt:{operation}Response><trt:MediaUri><tt:Uri>{}</tt:Uri><tt:InvalidAfterConnect>{}</tt:InvalidAfterConnect><tt:InvalidAfterReboot>{}</tt:InvalidAfterReboot><tt:Timeout>PT{}S</tt:Timeout></trt:MediaUri></trt:{operation}Response>",
472 esc(s(v, "uri")),
473 b(v, "invalid_after_connect", false),
474 b(v, "invalid_after_reboot", false),
475 n(v, "timeout_secs", 60)
476 )
477}
478
479pub fn capabilities(base: &str, device: bool, media: bool) -> String {
481 let mut out = String::from("<tds:GetCapabilitiesResponse><tds:Capabilities>");
482 if device {
483 out.push_str(&format!(
484 "<tt:Device><tt:XAddr>{}{DEVICE_PATH}</tt:XAddr></tt:Device>",
485 esc(base)
486 ));
487 }
488 if media {
489 out.push_str(&format!(
490 "<tt:Media><tt:XAddr>{}{MEDIA_PATH}</tt:XAddr><tt:StreamingCapabilities><tt:RTPMulticast>false</tt:RTPMulticast><tt:RTP_TCP>true</tt:RTP_TCP><tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP></tt:StreamingCapabilities></tt:Media>",
491 esc(base)
492 ));
493 }
494 out.push_str("</tds:Capabilities></tds:GetCapabilitiesResponse>");
495 out
496}
497
498pub fn services(base: &str) -> String {
499 let mut out = String::from("<tds:GetServicesResponse>");
500 for (ns, path) in [(TDS, DEVICE_PATH), (TRT, MEDIA_PATH)] {
501 out.push_str(&format!(
502 "<tds:Service><tds:Namespace>{ns}</tds:Namespace><tds:XAddr>{}{path}</tds:XAddr><tds:Version><tt:Major>2</tt:Major><tt:Minor>40</tt:Minor></tds:Version></tds:Service>",
503 esc(base)
504 ));
505 }
506 out.push_str("</tds:GetServicesResponse>");
507 out
508}
509
510pub fn system_date_and_time(now: chrono::DateTime<chrono::Utc>) -> String {
511 use chrono::{Datelike, Timelike};
512 let dt = format!(
513 "<tt:Time><tt:Hour>{}</tt:Hour><tt:Minute>{}</tt:Minute><tt:Second>{}</tt:Second></tt:Time><tt:Date><tt:Year>{}</tt:Year><tt:Month>{}</tt:Month><tt:Day>{}</tt:Day></tt:Date>",
514 now.hour(),
515 now.minute(),
516 now.second(),
517 now.year(),
518 now.month(),
519 now.day()
520 );
521 format!(
522 "<tds:GetSystemDateAndTimeResponse><tds:SystemDateAndTime><tt:DateTimeType>Manual</tt:DateTimeType><tt:DaylightSavings>false</tt:DaylightSavings><tt:TimeZone><tt:TZ>UTC0</tt:TZ></tt:TimeZone><tt:UTCDateTime>{dt}</tt:UTCDateTime><tt:LocalDateTime>{dt}</tt:LocalDateTime></tds:SystemDateAndTime></tds:GetSystemDateAndTimeResponse>"
523 )
524}
525
526pub fn security_header(username: &str, password: &str, nonce: &[u8], created: &str) -> String {
530 format!(
531 "<wsse:Security xmlns:wsse=\"{WSSE}\" xmlns:wsu=\"{WSU}\" env:mustUnderstand=\"true\"><wsse:UsernameToken><wsse:Username>{}</wsse:Username><wsse:Password Type=\"{PASSWORD_DIGEST}\">{}</wsse:Password><wsse:Nonce EncodingType=\"{BASE64_BINARY}\">{}</wsse:Nonce><wsu:Created>{}</wsu:Created></wsse:UsernameToken></wsse:Security>",
532 esc(username),
533 password_digest(nonce, created, password),
534 base64::engine::general_purpose::STANDARD.encode(nonce),
535 esc(created)
536 )
537}
538
539pub fn request_envelope(security: Option<&str>, body: &str) -> String {
541 let header = security
542 .map(|h| format!("<env:Header>{h}</env:Header>"))
543 .unwrap_or_default();
544 format!(
545 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<env:Envelope xmlns:env=\"{ENV}\" xmlns:tds=\"{TDS}\" xmlns:trt=\"{TRT}\" xmlns:tt=\"{TT}\">{header}<env:Body>{body}</env:Body></env:Envelope>"
546 )
547}
548
549pub enum Reply {
552 Ok(Element),
553 Fault(Value),
554}
555
556fn qname_local(s: &str) -> String {
557 s.trim().rsplit(':').next().unwrap_or_default().to_string()
558}
559
560pub fn parse_reply(xml: &[u8]) -> Result<Reply> {
561 let env = tree(xml, MAX_RESPONSE)?;
562 ensure!(
563 env.name == "Envelope" && (env.ns == ENV || env.ns == ENV11),
564 "the reply is not a SOAP envelope"
565 );
566 let body = env.child("Body").context("the reply has no Body")?;
567 let el = body.children.first().context("the reply's Body is empty")?;
568 if el.name != "Fault" {
569 return Ok(Reply::Ok(el.clone()));
570 }
571 if let Some(code) = el.child("Code") {
573 let mut subcodes = Vec::new();
574 let mut at = code.child("Subcode");
575 while let Some(sc) = at {
576 if subcodes.len() >= 8 {
577 break;
578 }
579 subcodes.push(qname_local(&sc.text_of("Value").unwrap_or_default()));
580 at = sc.child("Subcode");
581 }
582 let reason = el
583 .child("Reason")
584 .and_then(|r| r.text_of("Text"))
585 .unwrap_or_default();
586 return Ok(Reply::Fault(json!({
587 "code": qname_local(&code.text_of("Value").unwrap_or_default()),
588 "subcodes": subcodes,
589 "reason": reason,
590 })));
591 }
592 Ok(Reply::Fault(json!({
593 "code": qname_local(&el.text_of("faultcode").unwrap_or_default()),
594 "subcodes": [],
595 "reason": el.text_of("faultstring").unwrap_or_default(),
596 })))
597}
598
599pub fn xaddr_path(xaddr: &str) -> Option<String> {
601 let rest = xaddr
602 .trim()
603 .strip_prefix("http://")
604 .or_else(|| xaddr.trim().strip_prefix("https://"))?;
605 Some(match rest.find('/') {
606 Some(i) => rest[i..].to_string(),
607 None => "/".to_string(),
608 })
609}
610
611fn num(e: &Element, path: &[&str]) -> Value {
612 match e.path(path) {
613 Some(t) => t
614 .parse::<u64>()
615 .map(Value::from)
616 .or_else(|_| t.parse::<f64>().map(Value::from))
617 .unwrap_or(Value::Null),
618 None => Value::Null,
619 }
620}
621
622fn opt(e: &Element, path: &[&str]) -> Value {
623 e.path(path).map(Value::from).unwrap_or(Value::Null)
624}
625
626fn boolean(e: &Element, path: &[&str]) -> Value {
627 match e.path(path).as_deref() {
628 Some("true" | "1") => json!(true),
629 Some("false" | "0") => json!(false),
630 _ => Value::Null,
631 }
632}
633
634pub fn result_json(operation: &str, el: &Element) -> Value {
636 match operation {
637 "GetDeviceInformation" => json!({
638 "manufacturer": opt(el, &["Manufacturer"]),
639 "model": opt(el, &["Model"]),
640 "firmware_version": opt(el, &["FirmwareVersion"]),
641 "serial_number": opt(el, &["SerialNumber"]),
642 "hardware_id": opt(el, &["HardwareId"]),
643 }),
644 "GetScopes" => json!({"scopes": el.children_named("Scopes").take(MAX_ITEMS).map(|s| json!({
645 "uri": opt(s, &["ScopeItem"]), "definition": opt(s, &["ScopeDef"])})).collect::<Vec<_>>()}),
646 "GetNetworkInterfaces" => {
647 json!({"interfaces": el.children_named("NetworkInterfaces").take(MAX_ITEMS).map(|i| {
648 let config = i.child("IPv4").and_then(|v| v.child("Config"));
649 let addrs: Vec<Value> = config.map(|c| c.children.iter()
650 .filter(|a| matches!(a.name.as_str(), "Manual" | "FromDHCP" | "LinkLocal"))
651 .map(|a| json!({"source": a.name, "address": opt(a, &["Address"]), "prefix_length": num(a, &["PrefixLength"])}))
652 .collect()).unwrap_or_default();
653 json!({"token": i.attr("token"), "enabled": boolean(i, &["Enabled"]), "name": opt(i, &["Info", "Name"]),
654 "mac": opt(i, &["Info", "HwAddress"]), "mtu": num(i, &["Info", "MTU"]),
655 "dhcp": config.map(|c| boolean(c, &["DHCP"])).unwrap_or(Value::Null), "ipv4": addrs})
656 }).collect::<Vec<_>>()})
657 }
658 "GetCapabilities" => {
659 let mut services = Map::new();
660 if let Some(c) = el.child("Capabilities") {
661 for cap in &c.children {
662 if let Some(x) = cap.text_of("XAddr") {
663 services.insert(cap.name.clone(), json!(x));
664 }
665 }
666 }
667 json!({"services": services})
668 }
669 "GetServices" => {
670 json!({"services": el.children_named("Service").take(MAX_ITEMS).map(|s| json!({
671 "namespace": opt(s, &["Namespace"]), "xaddr": opt(s, &["XAddr"]),
672 "version": format!("{}.{}", s.path(&["Version", "Major"]).unwrap_or_default(), s.path(&["Version", "Minor"]).unwrap_or_default())})).collect::<Vec<_>>()})
673 }
674 "GetSystemDateAndTime" => {
675 let t = el.child("SystemDateAndTime");
676 json!({
677 "date_time_type": t.map(|t| opt(t, &["DateTimeType"])).unwrap_or(Value::Null),
678 "time_zone": t.map(|t| opt(t, &["TimeZone", "TZ"])).unwrap_or(Value::Null),
679 "utc": t.and_then(utc_of).map(|d| json!(d.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))).unwrap_or(Value::Null),
680 })
681 }
682 "GetProfiles" => json!({"profiles": el.children_named("Profiles").take(MAX_ITEMS).map(|p| {
683 let enc = p.child("VideoEncoderConfiguration");
684 json!({"token": p.attr("token"), "name": opt(p, &["Name"]),
685 "video_source_token": opt(p, &["VideoSourceConfiguration", "SourceToken"]),
686 "encoding": enc.map(|e| opt(e, &["Encoding"])).unwrap_or(Value::Null),
687 "width": enc.map(|e| num(e, &["Resolution", "Width"])).unwrap_or(Value::Null),
688 "height": enc.map(|e| num(e, &["Resolution", "Height"])).unwrap_or(Value::Null),
689 "framerate": enc.map(|e| num(e, &["RateControl", "FrameRateLimit"])).unwrap_or(Value::Null),
690 "bitrate": enc.map(|e| num(e, &["RateControl", "BitrateLimit"])).unwrap_or(Value::Null)})
691 }).collect::<Vec<_>>()}),
692 "GetVideoSources" => {
693 json!({"sources": el.children_named("VideoSources").take(MAX_ITEMS).map(|v| json!({
694 "token": v.attr("token"), "framerate": num(v, &["Framerate"]),
695 "width": num(v, &["Resolution", "Width"]), "height": num(v, &["Resolution", "Height"])})).collect::<Vec<_>>()})
696 }
697 "GetStreamUri" | "GetSnapshotUri" => json!({
698 "uri": opt(el, &["MediaUri", "Uri"]),
699 "timeout": opt(el, &["MediaUri", "Timeout"]),
700 "invalid_after_connect": boolean(el, &["MediaUri", "InvalidAfterConnect"]),
701 }),
702 _ => json!({}),
703 }
704}
705
706pub fn utc_of(t: &Element) -> Option<chrono::DateTime<chrono::Utc>> {
708 use chrono::TimeZone;
709 let u = t.child("UTCDateTime")?;
710 let g = |p: &[&str]| u.path(p).and_then(|v| v.parse::<u32>().ok());
711 let date = chrono::NaiveDate::from_ymd_opt(
712 g(&["Date", "Year"])? as i32,
713 g(&["Date", "Month"])?,
714 g(&["Date", "Day"])?,
715 )?;
716 let time = chrono::NaiveTime::from_hms_opt(
717 g(&["Time", "Hour"])?,
718 g(&["Time", "Minute"])?,
719 g(&["Time", "Second"])?,
720 )?;
721 chrono::Utc
722 .from_local_datetime(&date.and_time(time))
723 .single()
724}
725
726pub fn request_body(operation: &str, args: &Value) -> Result<String> {
728 let token = || -> String { esc(s(args, "profile_token")) };
729 Ok(match operation {
730 "GetSystemDateAndTime" | "GetDeviceInformation" | "GetScopes" | "GetNetworkInterfaces" => {
731 format!("<tds:{operation}/>")
732 }
733 "GetCapabilities" => {
734 "<tds:GetCapabilities><tds:Category>All</tds:Category></tds:GetCapabilities>".into()
735 }
736 "GetServices" => {
737 "<tds:GetServices><tds:IncludeCapability>false</tds:IncludeCapability></tds:GetServices>"
738 .into()
739 }
740 "GetProfiles" | "GetVideoSources" => format!("<trt:{operation}/>"),
741 "GetStreamUri" => format!(
742 "<trt:GetStreamUri><trt:StreamSetup><tt:Stream>{}</tt:Stream><tt:Transport><tt:Protocol>{}</tt:Protocol></tt:Transport></trt:StreamSetup><trt:ProfileToken>{}</trt:ProfileToken></trt:GetStreamUri>",
743 esc(match s(args, "stream") { "" => "RTP-Unicast", x => x }),
744 esc(match s(args, "protocol") { "" => "RTSP", x => x }),
745 token()
746 ),
747 "GetSnapshotUri" => format!(
748 "<trt:GetSnapshotUri><trt:ProfileToken>{}</trt:ProfileToken></trt:GetSnapshotUri>",
749 token()
750 ),
751 other => bail!("no request body for {other}"),
752 })
753}