use crate::soap::security::WsSecurityToken;
use crate::types::xml_escape;
use std::fmt::Write;
const NAMESPACES: &[(&str, &str)] = &[
("s", "http://www.w3.org/2003/05/soap-envelope"),
("enc", "http://www.w3.org/2003/05/soap-encoding"),
("xsi", "http://www.w3.org/2001/XMLSchema-instance"),
("xsd", "http://www.w3.org/2001/XMLSchema"),
("wsa", "http://www.w3.org/2005/08/addressing"),
(
"wsse",
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
),
(
"wsu",
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
),
("wsnt", "http://docs.oasis-open.org/wsn/b-2"),
("tt", "http://www.onvif.org/ver10/schema"),
("tds", "http://www.onvif.org/ver10/device/wsdl"),
("trt", "http://www.onvif.org/ver10/media/wsdl"),
("tr2", "http://www.onvif.org/ver20/media/wsdl"),
("tev", "http://www.onvif.org/ver10/events/wsdl"),
("tptz", "http://www.onvif.org/ver20/ptz/wsdl"),
("timg", "http://www.onvif.org/ver20/imaging/wsdl"),
("tan", "http://www.onvif.org/ver20/analytics/wsdl"),
("ter", "http://www.onvif.org/ver10/error"),
("trc", "http://www.onvif.org/ver10/recording/wsdl"),
("tse", "http://www.onvif.org/ver10/search/wsdl"),
("trp", "http://www.onvif.org/ver10/replay/wsdl"),
];
pub struct SoapEnvelope {
security: Option<WsSecurityToken>,
body_content: String,
wsa_to: Option<String>,
}
impl SoapEnvelope {
pub fn new(body_content: String) -> Self {
Self {
security: None,
body_content,
wsa_to: None,
}
}
pub fn with_security(mut self, token: WsSecurityToken) -> Self {
self.security = Some(token);
self
}
pub fn with_wsa_to(mut self, to: impl Into<String>) -> Self {
self.wsa_to = Some(to.into());
self
}
pub fn build(self) -> String {
let mut out = String::with_capacity(2048);
out.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
out.push_str("<s:Envelope");
for (prefix, uri) in NAMESPACES {
write!(out, r#" xmlns:{prefix}="{uri}""#).unwrap();
}
out.push('>');
let has_header = self.security.is_some() || self.wsa_to.is_some();
if has_header {
out.push_str("<s:Header>");
if let Some(to) = &self.wsa_to {
write!(out, "<wsa:To>{}</wsa:To>", xml_escape(to)).unwrap();
}
if let Some(sec) = &self.security {
sec.write_xml(&mut out);
}
out.push_str("</s:Header>");
}
out.push_str("<s:Body>");
out.push_str(&self.body_content);
out.push_str("</s:Body>");
out.push_str("</s:Envelope>");
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::soap::security::WsSecurityToken;
fn parse_ok(xml: &str) -> bool {
crate::soap::xml::XmlNode::parse(xml).is_ok()
}
#[test]
fn test_build_produces_valid_xml() {
let xml = SoapEnvelope::new("<tds:GetCapabilities/>".to_string()).build();
assert!(parse_ok(&xml), "produced XML should be well-formed");
}
#[test]
fn test_envelope_starts_with_xml_declaration() {
let xml = SoapEnvelope::new(String::new()).build();
assert!(xml.starts_with(r#"<?xml version="1.0" encoding="UTF-8"?>"#));
}
#[test]
fn test_envelope_root_element() {
let xml = SoapEnvelope::new(String::new()).build();
assert!(xml.contains("<s:Envelope"));
assert!(xml.ends_with("</s:Envelope>"));
}
#[test]
fn test_body_content_preserved() {
let body = r#"<tds:GetCapabilities><tds:Category>All</tds:Category></tds:GetCapabilities>"#;
let xml = SoapEnvelope::new(body.to_string()).build();
assert!(xml.contains(body));
assert!(xml.contains("<s:Body>"));
assert!(xml.contains("</s:Body>"));
}
#[test]
fn test_required_namespaces_present() {
let xml = SoapEnvelope::new(String::new()).build();
assert!(xml.contains(r#"xmlns:s="http://www.w3.org/2003/05/soap-envelope""#));
assert!(xml.contains(r#"xmlns:tt="http://www.onvif.org/ver10/schema""#));
assert!(xml.contains(r#"xmlns:tds="http://www.onvif.org/ver10/device/wsdl""#));
assert!(xml.contains(r#"xmlns:wsse=""#));
assert!(xml.contains(r#"xmlns:wsu=""#));
}
#[test]
fn test_no_header_when_no_security_no_wsa() {
let xml = SoapEnvelope::new(String::new()).build();
assert!(!xml.contains("<s:Header>"));
}
#[test]
fn test_header_present_with_security() {
let token = WsSecurityToken::from_parts("admin", "digest", "nonce", "2024-01-01T00:00:00Z");
let xml = SoapEnvelope::new(String::new())
.with_security(token)
.build();
assert!(xml.contains("<s:Header>"));
assert!(xml.contains("</s:Header>"));
assert!(xml.contains("<wsse:Security>"));
}
#[test]
fn test_header_contains_wsa_to() {
let xml = SoapEnvelope::new(String::new())
.with_wsa_to("http://192.168.1.100/onvif/device_service")
.build();
assert!(xml.contains("<s:Header>"));
assert!(xml.contains("<wsa:To>http://192.168.1.100/onvif/device_service</wsa:To>"));
}
#[test]
fn test_security_fields_in_xml() {
let token = WsSecurityToken::from_parts(
"operator",
"Zm9vYmFy", "bm9uY2U=", "2024-06-15T08:00:00Z",
);
let xml = SoapEnvelope::new(String::new())
.with_security(token)
.build();
assert!(xml.contains("<wsse:Username>operator</wsse:Username>"));
assert!(xml.contains(">Zm9vYmFy</wsse:Password>"));
assert!(xml.contains(">bm9uY2U=</wsse:Nonce>"));
assert!(xml.contains(">2024-06-15T08:00:00Z</wsu:Created>"));
}
#[test]
fn test_full_envelope_is_parseable_and_navigable() {
use crate::soap::xml::{find_response, parse_soap_body};
let token = WsSecurityToken::from_parts("admin", "d", "n", "2024-01-01T00:00:00Z");
let envelope = SoapEnvelope::new(
"<tds:GetCapabilities><tds:Category>All</tds:Category></tds:GetCapabilities>"
.to_string(),
)
.with_security(token)
.build();
assert!(parse_ok(&envelope));
let body = parse_soap_body(&envelope).unwrap();
assert_eq!(body.local_name, "Body");
let req = find_response(&body, "GetCapabilities").unwrap();
assert_eq!(req.child("Category").unwrap().text(), "All");
}
}