implémentation concrète de DeviceDescriptionProvider
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user