implémentation concrète de DeviceDescriptionProvider

This commit is contained in:
2025-11-29 18:56:11 +01:00
parent 6cbf4791cd
commit 3ec692ea6f
14 changed files with 830 additions and 71 deletions

View File

@@ -24,7 +24,7 @@ axum = "0.8.4"
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
quick-xml = { version = "0.37.0", features = ["serialize"] }
quick-xml = { version = "0.37.5", features = ["serialize"] }
chrono = { version = "0.4.42", features = ["serde"] }
once_cell = "1.20"
parking_lot = "0.12"
@@ -35,6 +35,7 @@ bevy_reflect_derive = "0.17.1"
reqwest = "0.12.23"
utoipa = { version = "5.3", features = ["axum_extras"] }
socket2 = "0.5"
get_if_addrs = "0.5"
[features]
default = ["server"]

View File

@@ -2,6 +2,34 @@
use xmltree::{Element, XMLNode};
fn build_soap_envelope_with_body(body_child: Element) -> Result<String, xmltree::Error> {
// Body
let mut body = Element::new("s:Body");
body.children.push(XMLNode::Element(body_child));
// Envelope
let mut envelope = Element::new("s:Envelope");
envelope.attributes.insert(
"xmlns:s".to_string(),
"http://schemas.xmlsoap.org/soap/envelope/".to_string(),
);
envelope.attributes.insert(
"s:encodingStyle".to_string(),
"http://schemas.xmlsoap.org/soap/encoding/".to_string(),
);
envelope.children.push(XMLNode::Element(body));
let mut buf = Vec::new();
let config = xmltree::EmitterConfig::new()
.write_document_declaration(true)
.perform_indent(true)
.indent_string(" ");
envelope.write_with_config(&mut buf, config)?;
Ok(String::from_utf8(buf).unwrap())
}
/// Construit une réponse SOAP UPnP
///
/// # Arguments
@@ -18,46 +46,39 @@ pub fn build_soap_response(
action: &str,
values: Vec<(String, String)>,
) -> Result<String, xmltree::Error> {
// Construire l'élément de réponse
// Format: <u:ActionResponse xmlns:u="service-urn">
let response_name = format!("u:{}Response", action);
let mut response_elem = Element::new(&response_name);
response_elem
.attributes
.insert("xmlns:u".to_string(), service_urn.to_string());
// Ajouter les valeurs de retour
for (key, value) in values {
let mut child = Element::new(&key);
child.children.push(XMLNode::Text(value));
response_elem.children.push(XMLNode::Element(child));
}
// Construire le Body
let mut body = Element::new("s:Body");
body.children.push(XMLNode::Element(response_elem));
build_soap_envelope_with_body(response_elem)
}
// Construire l'Envelope
let mut envelope = Element::new("s:Envelope");
envelope.attributes.insert(
"xmlns:s".to_string(),
"http://schemas.xmlsoap.org/soap/envelope/".to_string(),
);
envelope.attributes.insert(
"s:encodingStyle".to_string(),
"http://schemas.xmlsoap.org/soap/encoding/".to_string(),
);
envelope.children.push(XMLNode::Element(body));
pub fn build_soap_request(
service_urn: &str,
action: &str,
args: &[(&str, &str)],
) -> Result<String, xmltree::Error> {
let request_name = format!("u:{}", action);
let mut request_elem = Element::new(&request_name);
request_elem
.attributes
.insert("xmlns:u".to_string(), service_urn.to_string());
// Sérialiser en XML
let mut buf = Vec::new();
let config = xmltree::EmitterConfig::new()
.write_document_declaration(true)
.perform_indent(true)
.indent_string(" ");
envelope.write_with_config(&mut buf, config)?;
for (name, value) in args {
let mut child = Element::new(*name);
child.children.push(XMLNode::Text((*value).to_string()));
request_elem.children.push(XMLNode::Element(child));
}
Ok(String::from_utf8(buf).unwrap())
build_soap_envelope_with_body(request_elem)
}
#[cfg(test)]

View File

@@ -53,10 +53,10 @@ mod envelope;
mod fault;
mod parser;
pub use builder::build_soap_response;
pub use builder::{build_soap_response,build_soap_request};
pub use envelope::{SoapBody, SoapEnvelope, SoapHeader};
pub use fault::{SoapFault, build_soap_fault};
pub use parser::{SoapAction, parse_soap_action};
pub use parser::{SoapAction, parse_soap_action, parse_soap_envelope};
/// Codes d'erreur SOAP UPnP standards
pub mod error_codes {

View File

@@ -1,3 +1,22 @@
/*!
The PMOMusic SSDP client is a *control point*.
It must **not** bind to UDP port 1900.
Reason:
* The SSDP *server* (UPnP device mode) must listen on 0.0.0.0:1900 for M-SEARCH discovery.
* The SSDP *client* only needs to send M-SEARCH and receive unicast HTTP/200 replies.
* If both client and server bind on 1900 (even with SO_REUSEPORT) the kernel load-balances
incoming datagrams between sockets. As a result, NOTIFY and HTTP/200 messages are lost
randomly by the client.
Therefore:
* SSDP server → bind(0.0.0.0:1900), join multicast, answer M-SEARCH.
* SSDP client → bind(0.0.0.0:0), use an ephemeral port, send M-SEARCH, receive replies.
The client may still join the multicast group for debugging, but NOTIFY reception is optional.
*/
//! Client SSDP pour la découverte des devices UPnP
use super::{MAX_AGE, SSDP_MULTICAST_ADDR, SSDP_PORT};
@@ -47,41 +66,35 @@ impl SsdpClient {
let socket2 = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
socket2.set_reuse_address(true)?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = socket2.as_raw_fd();
let optval: libc::c_int = 1;
unsafe {
let result = libc::setsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_REUSEPORT,
&optval as *const _ as *const libc::c_void,
std::mem::size_of_val(&optval) as libc::socklen_t,
);
if result != 0 {
return Err(std::io::Error::last_os_error());
}
}
debug!("✅ SsdpClient SO_REUSEPORT enabled (Unix)");
}
#[cfg(windows)]
{
debug!("✅ SsdpClient SO_REUSEADDR enabled (Windows - SO_REUSEPORT not needed)");
}
let bind_addr: SocketAddr = format!("0.0.0.0:{}", SSDP_PORT).parse().unwrap();
let bind_addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
socket2.bind(&bind_addr.into())?;
let socket: UdpSocket = socket2.into();
socket.join_multicast_v4(
&SSDP_MULTICAST_ADDR.parse().unwrap(),
&"0.0.0.0".parse().unwrap(),
)?;
socket.set_read_timeout(Some(Duration::from_secs(1)))?;
socket.set_multicast_loop_v4(false)?;
socket.set_multicast_loop_v4(true)?; // utile en dev local
for iface in get_if_addrs::get_if_addrs()? {
if let std::net::IpAddr::V4(ipv4) = iface.ip() {
if !ipv4.is_loopback() {
match socket.join_multicast_v4(&SSDP_MULTICAST_ADDR.parse().unwrap(), &ipv4) {
Ok(()) => {
debug!("SSDP: joined {} on {}", SSDP_MULTICAST_ADDR, ipv4);
}
Err(e) => {
warn!(
"SSDP: failed to join {} on {}: {}",
SSDP_MULTICAST_ADDR, ipv4, e
);
}
}
}
}
}
info!("✅ SSDP client ready on {}", addr);
@@ -158,12 +171,16 @@ impl SsdpClient {
fn parse_message(data: &str, from: SocketAddr) -> Option<SsdpEvent> {
let mut lines = data.lines();
let first_line = lines.next()?.trim();
let upper = first_line.to_ascii_uppercase();
let headers = parse_headers(lines);
if first_line.to_ascii_uppercase().starts_with("NOTIFY") {
if upper.starts_with("NOTIFY ") {
handle_notify(&headers, from)
} else if first_line.to_ascii_uppercase().starts_with("HTTP/1.1 200") {
} else if upper.starts_with("HTTP/") && upper.contains(" 200 ") {
handle_search_response(&headers, from)
} else if upper.starts_with("M-SEARCH ") {
// Another control point querying us; we are not a device, so we ignore.
None
} else {
None
}