1use crate::{mdns::{self, DnsMessage}, mdns2};
7use anyhow::{Context, Result};
8use byteorder::ReadBytesExt;
9use std::{
10 collections::{BTreeMap, HashMap},
11 io::{Cursor, Read},
12 net::{IpAddr, Ipv4Addr, Ipv6Addr},
13 time::Duration,
14};
15use tokio_util::bytes::Buf;
16
17#[derive(Debug, Clone)]
18pub enum CommissioningMode {
19 No,
20 Yes,
21 WithPasscode,
22}
23
24#[derive(Debug, Clone)]
25pub struct MatterDeviceInfo {
26 pub instance: String,
27 pub device: String,
28 pub ips: Vec<IpAddr>,
29 pub name: Option<String>,
30 pub vendor_id: Option<String>,
31 pub product_id: Option<String>,
32 pub discriminator: Option<String>,
33 pub commissioning_mode: Option<CommissioningMode>,
34 pub pairing_hint: Option<String>,
35 pub source_ip: String,
36 pub port: Option<u16>,
37}
38
39impl MatterDeviceInfo {
40 pub fn print_compact(&self) {
41 let mut info = format!("{} ({})", self.instance, self.device);
42 if let Some(name) = &self.name {
43 info += &format!(", name: {}", name);
44 }
45 if let Some(vendor_id) = &self.vendor_id {
46 info += &format!(", vendor_id: {}", vendor_id);
47 }
48 if let Some(product_id) = &self.product_id {
49 info += &format!(", product_id: {}", product_id);
50 }
51 if let Some(discriminator) = &self.discriminator {
52 info += &format!(", discriminator: {}", discriminator);
53 }
54 if let Some(cm) = &self.commissioning_mode {
55 info += &format!(", commissioning_mode: {:?}", cm);
56 }
57 if let Some(pairing_hint) = &self.pairing_hint {
58 info += &format!(", pairing_hint: {}", pairing_hint);
59 }
60 if let Some(port) = &self.port {
61 info += &format!(", port: {}", port);
62 }
63 println!("{}", info);
64 if !self.ips.is_empty() {
65 println!(" ips:");
66 for ip in &self.ips {
67 println!(" {}", ip);
68 }
69 }
70
71 }
72}
73
74
75pub fn parse_txt_records(data: &[u8]) -> Result<HashMap<String, String>> {
76 let mut cursor = Cursor::new(data);
77 let mut out = HashMap::new();
78 while cursor.remaining() > 0 {
79 let len = cursor.read_u8()?;
80 let mut buf = vec![0; len as usize];
81 cursor.read_exact(buf.as_mut_slice())?;
82 let splitstr = std::str::from_utf8(&buf)?.splitn(2, "=");
83 let x: Vec<&str> = splitstr.collect();
84 if x.len() == 2 {
85 out.insert(x[0].to_owned(), x[1].to_owned());
86 }
87 }
88 Ok(out)
89}
90
91fn remove_string_suffix(string: &str, suffix: &str) -> String {
92 if let Some(s) = string.strip_suffix(suffix) {
93 s.to_owned()
94 } else {
95 string.to_owned()
96 }
97}
98
99pub fn to_matter_info2(msg: &DnsMessage, svc: &str) -> Result<Vec<MatterDeviceInfo>> {
100 let mut out = Vec::new();
101 let mut matter_service = false;
102 let svcname = ".".to_owned() + svc + ".";
103 for answer in &msg.answers {
104 if answer.name == svcname[1..] {
105 matter_service = true
106 }
107 }
108 if !matter_service {
109 return Err(anyhow::anyhow!("not matter service"));
110 }
111 let mut services = HashMap::new();
112 let mut targets = HashMap::new();
113 for additional in &msg.additional {
114 if additional.typ == mdns::TYPE_A {
115 let arr: [u8; 4] = match additional.rdata.clone().try_into() {
116 Ok(v) => v,
117 Err(_e) => return Err(anyhow::anyhow!("A record is not correct")),
118 };
119 let val = IpAddr::V4(Ipv4Addr::from_bits(u32::from_be_bytes(arr)));
120 if !targets.contains_key(&additional.name) {
121 targets.insert(additional.name.clone(), Vec::new());
122 }
123 targets.get_mut(&additional.name).unwrap().push(val);
124 }
125 if additional.typ == mdns::TYPE_AAAA {
126 let arr: [u8; 16] = match additional.rdata.clone().try_into() {
127 Ok(v) => v,
128 Err(_e) => return Err(anyhow::anyhow!("AAAA record is not correct")),
129 };
130 let val = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes(arr)));
131 if !targets.contains_key(&additional.name) {
132 targets.insert(additional.name.clone(), Vec::new());
133 }
134 targets.get_mut(&additional.name).unwrap().push(val);
135 }
136 }
137 let mut all = msg.additional.to_vec();
138 all.append(&mut msg.answers.to_vec());
139 for additional in &all {
140 if additional.typ == mdns::TYPE_SRV {
141 let service_name = remove_string_suffix(&additional.name, &svcname);
142 if additional.rdata.len() < 6 {
143 continue;
144 }
145 let port = ((additional.rdata[4] as u16) << 8) | (additional.rdata[5] as u16);
146 let target_name = {
147 if let Some(at) = additional.target.as_ref() {
148 at
149 } else {
150 continue;
151 }
152 };
153 let target_ip = targets.get(target_name).cloned().unwrap_or_default();
154 let mi = MatterDeviceInfo {
155 instance: service_name.clone(),
156 device: remove_string_suffix(target_name, ".local.").to_owned(),
157 ips: target_ip,
158 name: None,
159 discriminator: None,
160 commissioning_mode: None,
161 pairing_hint: None,
162 source_ip: msg.source.to_string(),
163 vendor_id: None,
164 product_id: None,
165 port: Some(port),
166 };
167 services.insert(service_name, mi);
168 }
169 }
170 for s in services.values() {
171 out.push(s.clone());
172 }
173
174 Ok(out)
175}
176
177pub fn to_matter_info(msg: &DnsMessage, svc: &str) -> Result<MatterDeviceInfo> {
178 let mut device = None;
179 let mut service = None;
180 let mut ips = BTreeMap::new();
181 let mut name = None;
182 let mut discriminator = None;
183 let mut cm = None;
184 let mut pairing_hint = None;
185 let mut vendor_id = None;
186 let mut product_id = None;
187 let mut port: Option<u16> = None;
188
189 let mut matter_service = false;
190 let svcname = ".".to_owned() + svc + ".";
191 for answer in &msg.answers {
192 if answer.name == svcname[1..] {
193 matter_service = true
194 }
195 }
196 for additional in &msg.additional {
197 if additional.typ == mdns::TYPE_A {
198 let arr: [u8; 4] = match additional.rdata.clone().try_into() {
199 Ok(v) => v,
200 Err(_e) => return Err(anyhow::anyhow!("A record is not correct")),
201 };
202 let val = IpAddr::V4(Ipv4Addr::from_bits(u32::from_be_bytes(arr)));
203 ips.insert(val, true);
204 device = Some(remove_string_suffix(&additional.name, ".local."));
205 }
206 if additional.typ == mdns::TYPE_AAAA {
207 let arr: [u8; 16] = match additional.rdata.clone().try_into() {
208 Ok(v) => v,
209 Err(_e) => return Err(anyhow::anyhow!("AAAA record is not correct")),
210 };
211 let val = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes(arr)));
212 ips.insert(val, true);
213 device = Some(remove_string_suffix(&additional.name, ".local."));
214 }
215 if additional.typ == mdns::TYPE_SRV {
216 service = Some(remove_string_suffix(&additional.name, &svcname));
217 if additional.rdata.len() >= 6 {
218 port = Some(((additional.rdata[4] as u16) << 8) | (additional.rdata[5] as u16))
219 }
220 }
221 if additional.typ == mdns::TYPE_TXT {
222 let rec = parse_txt_records(&additional.rdata)?;
223 name = rec.get("DN").cloned();
224 discriminator = rec.get("D").cloned();
225 pairing_hint = rec.get("PH").cloned();
226 if let Some(vp) = rec.get("VP") {
227 let mut split = vp.split("+");
228 vendor_id = split.next().map(str::to_owned);
229 product_id = split.next().map(str::to_owned);
230 }
231 cm = match rec.get("CM") {
232 Some(v) => match v.as_str() {
233 "0" => Some(CommissioningMode::No),
234 "1" => Some(CommissioningMode::Yes),
235 "2" => Some(CommissioningMode::WithPasscode),
236 _ => None,
237 },
238 None => None,
239 };
240 }
241 }
242
243 if !matter_service {
244 return Err(anyhow::anyhow!("not matter service"));
245 }
246
247 Ok(MatterDeviceInfo {
248 instance: service.context("service name not detected")?,
249 device: device.context("device name not detected")?,
250 ips: ips.into_keys().collect(),
251 name,
252 discriminator,
253 commissioning_mode: cm,
254 pairing_hint,
255 source_ip: msg.source.to_string(),
256 vendor_id,
257 product_id,
258 port,
259 })
260}
261
262async fn discover_common(timeout: Duration, svc_type: &str) -> Result<Vec<MatterDeviceInfo>> {
263 let stop = tokio_util::sync::CancellationToken::new();
264 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<DnsMessage>();
265
266 mdns::discover(svc_type, mdns::QTYPE_ANY, sender, stop.child_token()).await?;
267
268 tokio::spawn(async move {
269 tokio::time::sleep(timeout).await;
270 stop.cancel();
271 });
272 let mut cache = HashMap::new();
273 let mut out = Vec::new();
274 while let Some(dns) = receiver.recv().await {
275 if cache.contains_key(&dns) {
276 continue;
277 }
278 let info = match to_matter_info(&dns, svc_type) {
279 Ok(info) => info,
280 Err(_) => continue,
281 };
282 out.push(info);
283 cache.insert(dns, true);
284 }
285 Ok(out)
286}
287
288pub async fn discover_commissionable(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
290 discover_common(timeout, "_matterc._udp.local").await
291}
292
293pub async fn discover_commissioned(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
295 discover_common(timeout, "_matter._tcp.local").await
296}
297
298
299async fn discover_common2(timeout: Duration, svc_type: &str) -> Result<Vec<MatterDeviceInfo>> {
300 let stop = tokio_util::sync::CancellationToken::new();
301 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<DnsMessage>();
302
303 mdns::discover(svc_type, mdns::QTYPE_ANY, sender, stop.child_token()).await?;
304
305 tokio::spawn(async move {
306 tokio::time::sleep(timeout).await;
307 stop.cancel();
308 });
309 let mut cache = HashMap::new();
310 let mut out: Vec<MatterDeviceInfo> = Vec::new();
311 while let Some(dns) = receiver.recv().await {
312 if cache.contains_key(&dns) {
313 continue;
314 }
315 let info = match to_matter_info2(&dns, svc_type) {
316 Ok(info) => info,
317 Err(e) => {
318 log::trace!("failed to parse mdns message from {}: {:?}", dns.source, e);
319 continue;
320 },
321 };
322 for i in &info {
323 out.push(i.clone());
324 }
325 cache.insert(dns, true);
326 }
327 Ok(out)
328}
329
330pub async fn discover_commissionable2(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
332 discover_common2(timeout, "_matterc._udp.local").await
333}
334
335pub async fn discover_commissioned2(timeout: Duration, device: &Option<String>) -> Result<Vec<MatterDeviceInfo>> {
337 let query = {
338 match device {
339 None => "_matter._tcp.local".to_owned(),
340 Some(d) => format!("{}._matter._tcp.local", d),
341 }
342 };
343 discover_common2(timeout, &query).await
344}
345
346
347
348pub async fn extract_matter_info(target: &str, mdns: &mdns2::MdnsService) -> Result<MatterDeviceInfo> {
349 let txt_records = mdns.lookup(target, mdns::TYPE_TXT).await;
350 let mut txt_info = HashMap::new();
351 for txt_rr in txt_records {
352 txt_info.extend(parse_txt_records(&txt_rr.rdata)?);
353 }
354 let srv_records = mdns.lookup(target, mdns::TYPE_SRV).await;
355 let srv_rr = srv_records.first().ok_or_else(|| anyhow::anyhow!("No SRV record found for {}", target))?;
356 let (srv_target, port) = match srv_rr.data {
357 mdns::RRData::SRV { ref target, port, .. } => (target.clone(), port),
358 _ => return Err(anyhow::anyhow!("Invalid SRV record for {}", target)),
359 };
360 let mut ips = Vec::new();
361 let a_records = mdns.lookup(&srv_target, mdns::TYPE_A).await;
362 for a_rr in a_records {
363 if let mdns::RRData::A(ip) = a_rr.data {
364 ips.push(ip.into());
365 }
366 }
367 let aaaa_records = mdns.lookup(&srv_target, mdns::TYPE_AAAA).await;
368 for aaaa_rr in aaaa_records {
369 if let mdns::RRData::AAAA(ip) = aaaa_rr.data {
370 ips.push(ip.into());
371 }
372 }
373 let (vendor_id, product_id) = {
374 let vp = txt_info.get("VP");
375 if let Some(vp) = vp {
376 let mut parts = vp.split('+');
377 let vendor_id = parts.next();
378 let product_id = parts.next();
379 (vendor_id.map(|v| v.to_owned()), product_id.map(|p| p.to_owned()))
380 } else {
381 (None, None)
382 }
383 };
384 let discriminator = txt_info.get("D").cloned();
385 let name = txt_info.get("DN").cloned();
386 let commissioning_mode = match txt_info.get("CM") {
387 Some(v) => match v.as_str() {
388 "0" => Some(CommissioningMode::No),
389 "1" => Some(CommissioningMode::Yes),
390 "2" => Some(CommissioningMode::WithPasscode),
391 _ => None,
392 },
393 None => None,
394 };
395 let pairing_hint = txt_info.get("PH").cloned();
396 Ok(MatterDeviceInfo {
397 name,
398 instance: target.trim_end_matches('.').to_owned(),
399 device: srv_target.trim_end_matches('.').to_owned(),
400 ips,
401 vendor_id,
402 product_id,
403 discriminator,
404 commissioning_mode,
405 pairing_hint,
406 source_ip: "".to_owned(),
407 port: Some(port),
408 })
409}