fix: harden websocket and soap adapters
This commit is contained in:
@@ -1,24 +1,45 @@
|
||||
use crank_core::SoapBindingStyle;
|
||||
use roxmltree::{Document, Node};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::SoapAdapterError;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SoapEnvelopeHeader {
|
||||
pub name: String,
|
||||
pub namespace_uri: Option<String>,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
pub fn build_envelope(
|
||||
operation_name: &str,
|
||||
namespace: Option<&str>,
|
||||
body: &Value,
|
||||
envelope_namespace: &str,
|
||||
binding_style: SoapBindingStyle,
|
||||
headers: &[SoapEnvelopeHeader],
|
||||
) -> String {
|
||||
let mut xml = String::new();
|
||||
xml.push_str(&format!(
|
||||
r#"<soap:Envelope xmlns:soap="{envelope_namespace}">"#
|
||||
));
|
||||
if !headers.is_empty() {
|
||||
xml.push_str("<soap:Header>");
|
||||
for header in headers {
|
||||
append_header(&mut xml, header);
|
||||
}
|
||||
xml.push_str("</soap:Header>");
|
||||
}
|
||||
xml.push_str("<soap:Body>");
|
||||
match namespace {
|
||||
Some(namespace) => xml.push_str(&format!(r#"<m:{operation_name}" xmlns:m="{namespace}">"#)),
|
||||
None => xml.push_str(&format!("<{operation_name}>")),
|
||||
}
|
||||
append_value_children(&mut xml, body);
|
||||
let child_prefix = match binding_style {
|
||||
SoapBindingStyle::DocumentLiteral => None,
|
||||
SoapBindingStyle::RpcLiteral => namespace.map(|_| "m"),
|
||||
};
|
||||
append_value_children(&mut xml, body, child_prefix);
|
||||
match namespace {
|
||||
Some(_) => xml.push_str(&format!("</m:{operation_name}>")),
|
||||
None => xml.push_str(&format!("</{operation_name}>")),
|
||||
@@ -64,6 +85,38 @@ fn parse_fault(fault: Node<'_, '_>) -> SoapAdapterError {
|
||||
}
|
||||
}
|
||||
|
||||
fn append_header(xml: &mut String, header: &SoapEnvelopeHeader) {
|
||||
match header.namespace_uri.as_deref() {
|
||||
Some(namespace_uri) => {
|
||||
xml.push_str(&format!(
|
||||
r#"<h:{} xmlns:h="{}">"#,
|
||||
header.name, namespace_uri
|
||||
));
|
||||
append_header_value(xml, &header.value);
|
||||
xml.push_str(&format!("</h:{}>", header.name));
|
||||
}
|
||||
None => {
|
||||
xml.push('<');
|
||||
xml.push_str(&header.name);
|
||||
xml.push('>');
|
||||
append_header_value(xml, &header.value);
|
||||
xml.push_str("</");
|
||||
xml.push_str(&header.name);
|
||||
xml.push('>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn append_header_value(xml: &mut String, value: &Value) {
|
||||
match value {
|
||||
Value::Null => {}
|
||||
Value::Bool(value) => xml.push_str(if *value { "true" } else { "false" }),
|
||||
Value::Number(value) => xml.push_str(&value.to_string()),
|
||||
Value::String(value) => xml.push_str(&escape_xml(value)),
|
||||
Value::Array(_) | Value::Object(_) => append_value_children(xml, value, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_nested_text(node: Node<'_, '_>, path: &[&str]) -> Option<String> {
|
||||
let mut current = node;
|
||||
for segment in path {
|
||||
@@ -79,70 +132,82 @@ fn find_nested_text(node: Node<'_, '_>, path: &[&str]) -> Option<String> {
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn append_value_children(xml: &mut String, value: &Value) {
|
||||
fn append_value_children(xml: &mut String, value: &Value, namespace_prefix: Option<&str>) {
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
for (key, value) in object {
|
||||
append_named_value(xml, key, value);
|
||||
append_named_value(xml, key, value, namespace_prefix);
|
||||
}
|
||||
}
|
||||
other => append_named_value(xml, "value", other),
|
||||
other => append_named_value(xml, "value", other, namespace_prefix),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_named_value(xml: &mut String, name: &str, value: &Value) {
|
||||
fn append_named_value(xml: &mut String, name: &str, value: &Value, namespace_prefix: Option<&str>) {
|
||||
match value {
|
||||
Value::Array(items) => {
|
||||
for item in items {
|
||||
append_named_value(xml, name, item);
|
||||
append_named_value(xml, name, item, namespace_prefix);
|
||||
}
|
||||
}
|
||||
Value::Object(object) => {
|
||||
xml.push('<');
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
push_start_tag(xml, name, namespace_prefix);
|
||||
for (child_name, child_value) in object {
|
||||
append_named_value(xml, child_name, child_value);
|
||||
append_named_value(xml, child_name, child_value, namespace_prefix);
|
||||
}
|
||||
xml.push_str("</");
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
push_end_tag(xml, name, namespace_prefix);
|
||||
}
|
||||
Value::Null => {
|
||||
xml.push('<');
|
||||
xml.push_str(name);
|
||||
xml.push_str("/>");
|
||||
push_empty_tag(xml, name, namespace_prefix);
|
||||
}
|
||||
Value::Bool(value) => {
|
||||
xml.push('<');
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
push_start_tag(xml, name, namespace_prefix);
|
||||
xml.push_str(if *value { "true" } else { "false" });
|
||||
xml.push_str("</");
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
push_end_tag(xml, name, namespace_prefix);
|
||||
}
|
||||
Value::Number(value) => {
|
||||
xml.push('<');
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
push_start_tag(xml, name, namespace_prefix);
|
||||
xml.push_str(&value.to_string());
|
||||
xml.push_str("</");
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
push_end_tag(xml, name, namespace_prefix);
|
||||
}
|
||||
Value::String(value) => {
|
||||
xml.push('<');
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
push_start_tag(xml, name, namespace_prefix);
|
||||
xml.push_str(&escape_xml(value));
|
||||
xml.push_str("</");
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
push_end_tag(xml, name, namespace_prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_start_tag(xml: &mut String, name: &str, namespace_prefix: Option<&str>) {
|
||||
xml.push('<');
|
||||
if let Some(prefix) = namespace_prefix {
|
||||
xml.push_str(prefix);
|
||||
xml.push(':');
|
||||
}
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
}
|
||||
|
||||
fn push_end_tag(xml: &mut String, name: &str, namespace_prefix: Option<&str>) {
|
||||
xml.push_str("</");
|
||||
if let Some(prefix) = namespace_prefix {
|
||||
xml.push_str(prefix);
|
||||
xml.push(':');
|
||||
}
|
||||
xml.push_str(name);
|
||||
xml.push('>');
|
||||
}
|
||||
|
||||
fn push_empty_tag(xml: &mut String, name: &str, namespace_prefix: Option<&str>) {
|
||||
xml.push('<');
|
||||
if let Some(prefix) = namespace_prefix {
|
||||
xml.push_str(prefix);
|
||||
xml.push(':');
|
||||
}
|
||||
xml.push_str(name);
|
||||
xml.push_str("/>");
|
||||
}
|
||||
|
||||
fn escape_xml(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
@@ -190,9 +255,10 @@ fn node_to_json(node: Node<'_, '_>, _unwrap_root: bool) -> Value {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{build_envelope, decode_envelope};
|
||||
use super::{SoapEnvelopeHeader, build_envelope, decode_envelope};
|
||||
use crank_core::SoapBindingStyle;
|
||||
|
||||
#[test]
|
||||
fn builds_document_literal_envelope() {
|
||||
@@ -201,6 +267,8 @@ mod tests {
|
||||
Some("urn:crm"),
|
||||
&json!({"email":"user@example.com","source":"mcp"}),
|
||||
"http://schemas.xmlsoap.org/soap/envelope/",
|
||||
SoapBindingStyle::DocumentLiteral,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert!(xml.contains("<soap:Envelope"));
|
||||
@@ -226,4 +294,26 @@ mod tests {
|
||||
assert_eq!(value["id"], "lead_123");
|
||||
assert_eq!(value["status"], "created");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_rpc_literal_envelope_with_headers() {
|
||||
let xml = build_envelope(
|
||||
"CreateLead",
|
||||
Some("urn:crm"),
|
||||
&json!({"email":"user@example.com"}),
|
||||
"http://schemas.xmlsoap.org/soap/envelope/",
|
||||
SoapBindingStyle::RpcLiteral,
|
||||
&[SoapEnvelopeHeader {
|
||||
name: "CorrelationId".to_owned(),
|
||||
namespace_uri: Some("urn:headers".to_owned()),
|
||||
value: Value::String("corr-123".to_owned()),
|
||||
}],
|
||||
);
|
||||
|
||||
assert!(xml.contains("<soap:Header>"));
|
||||
assert!(
|
||||
xml.contains(r#"<h:CorrelationId xmlns:h="urn:headers">corr-123</h:CorrelationId>"#)
|
||||
);
|
||||
assert!(xml.contains("<m:email>user@example.com</m:email>"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user