#![cfg(any(feature = "artifact-binding", feature = "slo", feature = "ecp"))]
use crate::error::Error;
use crate::xml::emit::{emit_document, emit_element};
use crate::xml::parse::{Document, Element, Node, QName};
pub const SOAP_NS: &str = "http://schemas.xmlsoap.org/soap/envelope/";
pub const CONTENT_TYPE: &str = "text/xml; charset=utf-8";
pub const SOAP_ACTION: &str = "\"\"";
#[must_use]
pub fn request_headers() -> Vec<(String, String)> {
vec![
("Content-Type".to_owned(), CONTENT_TYPE.to_owned()),
("SOAPAction".to_owned(), SOAP_ACTION.to_owned()),
]
}
pub fn wrap(payload_xml: &str) -> Result<String, Error> {
let payload_doc = Document::parse(payload_xml.as_bytes())?;
let payload_elem = payload_doc.root().clone();
wrap_element(payload_elem)
}
pub(crate) fn wrap_element(payload: Element) -> Result<String, Error> {
let body = Element::build(QName::new(Some(SOAP_NS.to_owned()), "Body"))
.with_child(Node::Element(payload))
.finish();
let envelope = Element::build(QName::new(Some(SOAP_NS.to_owned()), "Envelope"))
.with_namespace(Some("soap".to_owned()), SOAP_NS)
.with_child(Node::Element(body))
.finish();
let doc = Document::new(envelope)?;
emit_document(&doc)
}
#[cfg(feature = "ecp")]
pub(crate) fn wrap_with_header(
header_blocks: Vec<Element>,
payload: Element,
) -> Result<String, Error> {
let mut header = Element::build(QName::new(Some(SOAP_NS.to_owned()), "Header"));
for block in header_blocks {
header = header.with_child(Node::Element(block));
}
let header = header.finish();
let body = Element::build(QName::new(Some(SOAP_NS.to_owned()), "Body"))
.with_child(Node::Element(payload))
.finish();
let envelope = Element::build(QName::new(Some(SOAP_NS.to_owned()), "Envelope"))
.with_namespace(Some("soap".to_owned()), SOAP_NS)
.with_child(Node::Element(header))
.with_child(Node::Element(body))
.finish();
let doc = Document::new(envelope)?;
emit_document(&doc)
}
#[derive(Debug)]
pub struct UnwrappedBody {
document: Document,
}
impl UnwrappedBody {
#[cfg(all(feature = "artifact-binding", feature = "weak-algos"))]
pub(crate) fn payload(&self) -> &Element {
self.document.root()
}
pub fn payload_xml(&self) -> Result<Vec<u8>, Error> {
Ok(emit_element(self.document.root())?.into_bytes())
}
#[cfg(all(feature = "artifact-binding", feature = "weak-algos"))]
pub(crate) fn document_ref(&self) -> &Document {
&self.document
}
}
pub fn unwrap(envelope_bytes: &[u8]) -> Result<UnwrappedBody, Error> {
unwrap_inner(envelope_bytes)
}
fn single_body_payload(body: &Element) -> Result<&Element, Error> {
let mut children = body.child_elements();
let payload = children.next().ok_or_else(|| {
Error::XmlParse("SOAP: soap:Body contains no payload element".to_string())
})?;
if children.next().is_some() {
return Err(Error::XmlParse(
"SOAP: soap:Body must contain exactly one payload element".to_string(),
));
}
Ok(payload)
}
fn unwrap_inner(envelope_bytes: &[u8]) -> Result<UnwrappedBody, Error> {
let doc = Document::parse(envelope_bytes)?;
let envelope = doc.root();
if envelope.qname().namespace() != Some(SOAP_NS) || envelope.qname().local() != "Envelope" {
return Err(Error::XmlParse(
"SOAP: envelope root is not soap:Envelope".to_string(),
));
}
let body = envelope
.child_element(Some(SOAP_NS), "Body")
.ok_or_else(|| Error::XmlParse("SOAP: missing soap:Body".to_string()))?;
if let Some(fault) = body.child_element(Some(SOAP_NS), "Fault") {
let faultcode = fault
.child_element(None, "faultcode")
.map(Element::text_content)
.unwrap_or_default();
let faultstring = fault.child_element(None, "faultstring").map(|e| {
e.text_content()
});
return Err(Error::SoapFault {
faultcode,
faultstring: faultstring.filter(|s| !s.is_empty()),
});
}
let payload = single_body_payload(body)?;
let payload_xml = emit_element(payload)?;
let document = Document::parse(payload_xml.as_bytes())?;
Ok(UnwrappedBody { document })
}
#[cfg(feature = "ecp")]
#[derive(Debug)]
pub(crate) struct UnwrappedEnvelope {
document: Document,
}
#[cfg(feature = "ecp")]
impl UnwrappedEnvelope {
pub(crate) fn header_block(&self, namespace: &str, local: &str) -> Option<&Element> {
self.document
.root()
.child_element(Some(SOAP_NS), "Header")
.and_then(|h| h.child_element(Some(namespace), local))
}
pub(crate) fn header_block_count(&self, namespace: &str, local: &str) -> usize {
self.document
.root()
.child_element(Some(SOAP_NS), "Header")
.map_or(0, |h| h.all_child_elements(Some(namespace), local).count())
}
pub(crate) fn body_payload(&self) -> Option<&Element> {
self.document
.root()
.child_element(Some(SOAP_NS), "Body")
.and_then(|b| b.child_elements().next())
}
pub(crate) fn body_payload_xml(&self) -> Result<Vec<u8>, Error> {
let payload = self.body_payload().ok_or_else(|| {
Error::XmlParse("SOAP: soap:Body contains no payload element".to_string())
})?;
Ok(emit_element(payload)?.into_bytes())
}
}
#[cfg(feature = "ecp")]
pub(crate) fn unwrap_envelope(envelope_bytes: &[u8]) -> Result<UnwrappedEnvelope, Error> {
let doc = Document::parse(envelope_bytes)?;
let envelope = doc.root();
if envelope.qname().namespace() != Some(SOAP_NS) || envelope.qname().local() != "Envelope" {
return Err(Error::XmlParse(
"SOAP: envelope root is not soap:Envelope".to_string(),
));
}
let body = envelope
.child_element(Some(SOAP_NS), "Body")
.ok_or_else(|| Error::XmlParse("SOAP: missing soap:Body".to_string()))?;
if let Some(fault) = body.child_element(Some(SOAP_NS), "Fault") {
let faultcode = fault
.child_element(None, "faultcode")
.map(Element::text_content)
.unwrap_or_default();
let faultstring = fault
.child_element(None, "faultstring")
.map(Element::text_content);
return Err(Error::SoapFault {
faultcode,
faultstring: faultstring.filter(|s| !s.is_empty()),
});
}
single_body_payload(body)?;
Ok(UnwrappedEnvelope { document: doc })
}
#[cfg(test)]
mod tests {
use super::*;
const SAMLP_NS: &str = "urn:oasis:names:tc:SAML:2.0:protocol";
#[test]
fn wrap_then_unwrap_round_trips_payload() {
let payload = r#"<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_lr1"/>"#;
let envelope = wrap(payload).expect("wrap");
let doc = Document::parse(envelope.as_bytes()).expect("re-parse");
assert_eq!(doc.root().qname().namespace(), Some(SOAP_NS));
assert_eq!(doc.root().qname().local(), "Envelope");
let body = doc.root().child_element(Some(SOAP_NS), "Body").unwrap();
assert!(
body.child_element(Some(SAMLP_NS), "LogoutRequest")
.is_some()
);
let unwrapped = unwrap(envelope.as_bytes()).expect("unwrap");
let bytes = unwrapped.payload_xml().expect("payload_xml");
let reparsed = Document::parse(&bytes).expect("payload reparse");
assert_eq!(reparsed.root().qname().local(), "LogoutRequest");
assert_eq!(reparsed.root().attribute(None, "ID"), Some("_lr1"));
}
#[test]
fn unwrap_rejects_non_envelope_root() {
let err = unwrap(b"<not-soap/>").unwrap_err();
assert!(matches!(err, Error::XmlParse(_)));
}
#[test]
fn unwrap_rejects_multiple_body_payloads() {
let xml = format!(
r#"<soap:Envelope xmlns:soap="{SOAP_NS}"><soap:Body><samlp:LogoutResponse xmlns:samlp="{SAMLP_NS}" ID="_a"/><decoy/></soap:Body></soap:Envelope>"#
);
let err = unwrap(xml.as_bytes()).unwrap_err();
match err {
Error::XmlParse(m) => assert!(m.contains("exactly one payload"), "got: {m}"),
other => panic!("expected XmlParse, got {other:?}"),
}
}
#[test]
fn unwrap_rejects_missing_body() {
let xml = format!(r#"<soap:Envelope xmlns:soap="{SOAP_NS}"/>"#);
let err = unwrap(xml.as_bytes()).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("soap:Body"), "got: {msg}"),
other => panic!("expected XmlParse, got {other:?}"),
}
}
#[test]
fn unwrap_rejects_empty_body() {
let xml = format!(r#"<soap:Envelope xmlns:soap="{SOAP_NS}"><soap:Body/></soap:Envelope>"#);
let err = unwrap(xml.as_bytes()).unwrap_err();
match err {
Error::XmlParse(msg) => assert!(msg.contains("payload"), "got: {msg}"),
other => panic!("expected XmlParse, got {other:?}"),
}
}
#[test]
fn unwrap_surfaces_soap_fault_with_code_and_string() {
let xml = format!(
r#"<soap:Envelope xmlns:soap="{SOAP_NS}">
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>artifact resolution failed</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>"#
);
let err = unwrap(xml.as_bytes()).unwrap_err();
match err {
Error::SoapFault {
faultcode,
faultstring,
} => {
assert_eq!(faultcode, "soap:Server");
assert_eq!(faultstring.as_deref(), Some("artifact resolution failed"));
}
other => panic!("expected SoapFault, got {other:?}"),
}
}
#[test]
fn unwrap_fault_without_faultstring_yields_none() {
let xml = format!(
r#"<soap:Envelope xmlns:soap="{SOAP_NS}">
<soap:Body><soap:Fault><faultcode>soap:Client</faultcode></soap:Fault></soap:Body>
</soap:Envelope>"#
);
let err = unwrap(xml.as_bytes()).unwrap_err();
match err {
Error::SoapFault {
faultcode,
faultstring,
} => {
assert_eq!(faultcode, "soap:Client");
assert!(faultstring.is_none());
}
other => panic!("expected SoapFault, got {other:?}"),
}
}
#[test]
fn request_headers_use_soap_conventions() {
let headers = request_headers();
assert!(
headers
.iter()
.any(|(k, v)| k == "Content-Type" && v == "text/xml; charset=utf-8")
);
assert!(
headers
.iter()
.any(|(k, v)| k == "SOAPAction" && v == "\"\"")
);
}
}