Add multiple features
This commit is contained in:
10
crates/wtfnet-discover/Cargo.toml
Normal file
10
crates/wtfnet-discover/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "wtfnet-discover"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
mdns-sd = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
209
crates/wtfnet-discover/src/lib.rs
Normal file
209
crates/wtfnet-discover/src/lib.rs
Normal file
@@ -0,0 +1,209 @@
|
||||
use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::time::{Duration, Instant};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DiscoverError {
|
||||
#[error("mdns error: {0}")]
|
||||
Mdns(String),
|
||||
#[error("io error: {0}")]
|
||||
Io(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MdnsOptions {
|
||||
pub duration_ms: u64,
|
||||
pub service_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SsdpOptions {
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MdnsService {
|
||||
pub service_type: String,
|
||||
pub fullname: String,
|
||||
pub hostname: Option<String>,
|
||||
pub addresses: Vec<String>,
|
||||
pub port: Option<u16>,
|
||||
pub properties: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MdnsReport {
|
||||
pub duration_ms: u64,
|
||||
pub service_type: Option<String>,
|
||||
pub services: Vec<MdnsService>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SsdpService {
|
||||
pub from: String,
|
||||
pub st: Option<String>,
|
||||
pub usn: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub server: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SsdpReport {
|
||||
pub duration_ms: u64,
|
||||
pub services: Vec<SsdpService>,
|
||||
}
|
||||
|
||||
pub async fn mdns_discover(options: MdnsOptions) -> Result<MdnsReport, DiscoverError> {
|
||||
tokio::task::spawn_blocking(move || mdns_discover_blocking(options))
|
||||
.await
|
||||
.map_err(|err| DiscoverError::Mdns(err.to_string()))?
|
||||
}
|
||||
|
||||
pub async fn ssdp_discover(options: SsdpOptions) -> Result<SsdpReport, DiscoverError> {
|
||||
tokio::task::spawn_blocking(move || ssdp_discover_blocking(options))
|
||||
.await
|
||||
.map_err(|err| DiscoverError::Io(err.to_string()))?
|
||||
}
|
||||
|
||||
fn mdns_discover_blocking(options: MdnsOptions) -> Result<MdnsReport, DiscoverError> {
|
||||
let daemon = ServiceDaemon::new().map_err(|err| DiscoverError::Mdns(err.to_string()))?;
|
||||
let mut service_types = BTreeSet::new();
|
||||
if let Some(service_type) = options.service_type.as_ref() {
|
||||
service_types.insert(service_type.clone());
|
||||
} else {
|
||||
let receiver = daemon
|
||||
.browse("_services._dns-sd._udp.local.")
|
||||
.map_err(|err| DiscoverError::Mdns(err.to_string()))?;
|
||||
let deadline = Instant::now() + Duration::from_millis(options.duration_ms / 2);
|
||||
while Instant::now() < deadline {
|
||||
match receiver.recv_timeout(Duration::from_millis(200)) {
|
||||
Ok(ServiceEvent::ServiceFound(service_type, _)) => {
|
||||
service_types.insert(service_type);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut services = Vec::new();
|
||||
let deadline = Instant::now() + Duration::from_millis(options.duration_ms);
|
||||
for service_type in service_types.iter() {
|
||||
let receiver = daemon
|
||||
.browse(service_type)
|
||||
.map_err(|err| DiscoverError::Mdns(err.to_string()))?;
|
||||
while Instant::now() < deadline {
|
||||
match receiver.recv_timeout(Duration::from_millis(200)) {
|
||||
Ok(ServiceEvent::ServiceResolved(info)) => {
|
||||
services.push(format_service_info(service_type, &info));
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MdnsReport {
|
||||
duration_ms: options.duration_ms,
|
||||
service_type: options.service_type,
|
||||
services,
|
||||
})
|
||||
}
|
||||
|
||||
fn format_service_info(service_type: &str, info: &ServiceInfo) -> MdnsService {
|
||||
let mut addresses = Vec::new();
|
||||
for addr in info.get_addresses().iter() {
|
||||
addresses.push(addr.to_string());
|
||||
}
|
||||
let mut properties = BTreeMap::new();
|
||||
for prop in info.get_properties().iter() {
|
||||
properties.insert(prop.key().to_string(), prop.val_str().to_string());
|
||||
}
|
||||
MdnsService {
|
||||
service_type: service_type.to_string(),
|
||||
fullname: info.get_fullname().to_string(),
|
||||
hostname: Some(info.get_hostname().to_string()),
|
||||
addresses,
|
||||
port: Some(info.get_port()),
|
||||
properties,
|
||||
}
|
||||
}
|
||||
|
||||
fn ssdp_discover_blocking(options: SsdpOptions) -> Result<SsdpReport, DiscoverError> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").map_err(|err| DiscoverError::Io(err.to_string()))?;
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_millis(200)))
|
||||
.map_err(|err| DiscoverError::Io(err.to_string()))?;
|
||||
|
||||
let request = [
|
||||
"M-SEARCH * HTTP/1.1",
|
||||
"HOST: 239.255.255.250:1900",
|
||||
"MAN: \"ssdp:discover\"",
|
||||
"MX: 1",
|
||||
"ST: ssdp:all",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
.join("\r\n");
|
||||
let target = "239.255.255.250:1900";
|
||||
let _ = socket.send_to(request.as_bytes(), target);
|
||||
|
||||
let mut services = Vec::new();
|
||||
let deadline = Instant::now() + Duration::from_millis(options.duration_ms);
|
||||
let mut buf = [0u8; 2048];
|
||||
|
||||
while Instant::now() < deadline {
|
||||
match socket.recv_from(&mut buf) {
|
||||
Ok((len, from)) => {
|
||||
if let Ok(payload) = std::str::from_utf8(&buf[..len]) {
|
||||
if let Some(entry) = parse_ssdp_response(payload, from) {
|
||||
services.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SsdpReport {
|
||||
duration_ms: options.duration_ms,
|
||||
services,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_ssdp_response(payload: &str, from: SocketAddr) -> Option<SsdpService> {
|
||||
let mut st = None;
|
||||
let mut usn = None;
|
||||
let mut location = None;
|
||||
let mut server = None;
|
||||
|
||||
for line in payload.lines() {
|
||||
let line = line.trim();
|
||||
if let Some((key, value)) = line.split_once(':') {
|
||||
let key = key.trim().to_ascii_lowercase();
|
||||
let value = value.trim().to_string();
|
||||
match key.as_str() {
|
||||
"st" => st = Some(value),
|
||||
"usn" => usn = Some(value),
|
||||
"location" => location = Some(value),
|
||||
"server" => server = Some(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if st.is_none() && usn.is_none() && location.is_none() && server.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(SsdpService {
|
||||
from: from.to_string(),
|
||||
st,
|
||||
usn,
|
||||
location,
|
||||
server,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user