fix: harden websocket and soap adapters

This commit is contained in:
a.tolmachev
2026-04-11 01:37:24 +03:00
parent 2770c5935f
commit e60d848293
6 changed files with 365 additions and 60 deletions
+114
View File
@@ -1,10 +1,12 @@
use std::{collections::BTreeMap, time::Duration};
use crank_core::{SoapTarget, SoapVersion};
use crank_mapping::JsonPath;
use reqwest::{
Client,
header::{HeaderMap, HeaderName, HeaderValue},
};
use serde_json::{Value, json};
use crate::{SoapAdapterError, SoapRequest, SoapResponse, xml};
@@ -36,11 +38,14 @@ impl SoapAdapter {
.as_deref()
.ok_or(SoapAdapterError::MissingEndpoint)?;
let headers = build_headers(target, request)?;
let envelope_headers = resolve_envelope_headers(target, request)?;
let envelope = xml::build_envelope(
&target.operation_name,
target.metadata.namespaces.first().map(String::as_str),
&request.body,
envelope_namespace(target.soap_version),
target.binding_style,
&envelope_headers,
);
let response = self
.client
@@ -70,6 +75,56 @@ impl SoapAdapter {
}
}
fn resolve_envelope_headers(
target: &SoapTarget,
request: &SoapRequest,
) -> Result<Vec<xml::SoapEnvelopeHeader>, SoapAdapterError> {
let context = json!({
"request": {
"body": request.body.clone(),
},
"mcp": request.body.clone(),
});
let mut rendered = Vec::new();
for header in &target.headers {
let Some(value) = resolve_header_value(header, &context, &request.body)? else {
if header.required {
return Err(SoapAdapterError::MissingRequiredHeader {
name: header.name.clone(),
});
}
continue;
};
rendered.push(xml::SoapEnvelopeHeader {
name: header.name.clone(),
namespace_uri: header.namespace_uri.clone(),
value,
});
}
Ok(rendered)
}
fn resolve_header_value(
header: &crank_core::SoapHeaderConfig,
context: &Value,
body: &Value,
) -> Result<Option<Value>, SoapAdapterError> {
if let Some(path) = header.value_path.as_deref() {
let path = JsonPath::parse(path).map_err(|_| SoapAdapterError::InvalidHeaderValuePath {
path: path.to_owned(),
})?;
return Ok(path.read(context).cloned());
}
Ok(match body {
Value::Object(map) => map.get(&header.name).cloned(),
_ => None,
})
}
fn build_headers(
target: &SoapTarget,
request: &SoapRequest,
@@ -209,4 +264,63 @@ mod tests {
assert_eq!(response.status_code, 200);
assert_eq!(response.body["id"], "lead_123");
}
#[tokio::test]
async fn renders_soap_headers_and_rpc_literal_children() {
async fn handler(body: Bytes) -> String {
let text = String::from_utf8_lossy(&body);
assert!(text.contains("<soap:Header>"));
assert!(
text.contains(
r#"<h:CorrelationId xmlns:h="urn:headers">corr-123</h:CorrelationId>"#
)
);
assert!(text.contains("<m:CreateLead"));
assert!(text.contains("<m:email>user@example.com</m:email>"));
r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<CreateLeadResponse>
<id>lead_rpc</id>
</CreateLeadResponse>
</soap:Body>
</soap:Envelope>"#
.to_owned()
}
let app = Router::new().route("/", post(handler));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
let target = SoapTarget {
binding_style: SoapBindingStyle::RpcLiteral,
headers: vec![crank_core::SoapHeaderConfig {
name: "CorrelationId".to_owned(),
namespace_uri: Some("urn:headers".to_owned()),
required: true,
value_path: Some("$.request.body.correlation_id".to_owned()),
}],
..test_target(format!("http://{}", address))
};
let adapter = SoapAdapter::new();
let response = adapter
.execute(
&target,
&SoapRequest {
headers: BTreeMap::new(),
body: json!({
"email": "user@example.com",
"correlation_id": "corr-123"
}),
timeout_ms: 1_000,
},
)
.await
.unwrap();
assert_eq!(response.body["id"], "lead_rpc");
}
}