fix: harden websocket and soap adapters
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user