Add multiple features

This commit is contained in:
DaZuo0122
2026-01-16 23:16:58 +08:00
parent c367ca29e4
commit cb022127c0
18 changed files with 1883 additions and 4 deletions

View 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"] }

View 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,
})
}