1use bytes::Bytes;
47use hyper::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
48use hyper::{Method, Request, Response};
49use serde_json::Value;
50
51use super::translation::{
52 ProtocolTranslator, TranslatedRequest, TranslatedResponse, TranslationBody, TranslationError,
53};
54use crate::config::SoapTranslation;
55
56const SOAP_CONTENT_TYPE: &str = "text/xml";
60
61const JSON_CONTENT_TYPE: &str = "application/json";
63
64const SOAP_ENVELOPE_NS: &str = "http://schemas.xmlsoap.org/soap/envelope/";
66
67const PARSE_DEPTH_CAP: usize = 256;
70
71#[derive(Debug, Clone)]
73pub struct SoapTranslator {
74 direction: SoapDirection,
76 operation: String,
78 namespace: String,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum SoapDirection {
85 SoapToRest,
87 RestToSoap,
89}
90
91impl SoapTranslator {
92 pub fn soap_to_rest(cfg: &SoapTranslation) -> Result<Self, TranslationError> {
96 if cfg.operation.trim().is_empty() {
97 return Err(TranslationError::SchemaNotFound(
98 "soap translation requires an operation name".to_string(),
99 ));
100 }
101 Ok(SoapTranslator {
102 direction: SoapDirection::SoapToRest,
103 operation: cfg.operation.clone(),
104 namespace: cfg.namespace.clone(),
105 })
106 }
107
108 pub fn rest_to_soap(cfg: &SoapTranslation) -> Result<Self, TranslationError> {
112 if cfg.operation.trim().is_empty() {
113 return Err(TranslationError::SchemaNotFound(
114 "soap translation requires an operation name".to_string(),
115 ));
116 }
117 if cfg.namespace.trim().is_empty() {
118 return Err(TranslationError::SchemaNotFound(
119 "rest_to_soap translation requires a namespace".to_string(),
120 ));
121 }
122 Ok(SoapTranslator {
123 direction: SoapDirection::RestToSoap,
124 operation: cfg.operation.clone(),
125 namespace: cfg.namespace.clone(),
126 })
127 }
128
129 pub fn direction(&self) -> SoapDirection {
131 self.direction
132 }
133}
134
135impl ProtocolTranslator for SoapTranslator {
136 fn translate_request(
137 &self,
138 req: &Request<TranslationBody>,
139 ) -> Result<TranslatedRequest, TranslationError> {
140 match self.direction {
141 SoapDirection::SoapToRest => {
142 let body = req.body().as_bytes();
143 let envelope = parse_envelope(body)?;
144 let payload = extract_body_payload(&envelope, &self.operation)?;
145 let json = xml_element_to_json(&payload);
146 let body = serde_json::to_vec(&json).map_err(|e| {
147 TranslationError::TranslationFailed(format!(
148 "failed to serialize soap body to json: {e}"
149 ))
150 })?;
151 let mut headers = HeaderMap::new();
152 headers.insert(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE));
153 Ok(TranslatedRequest {
154 method: req.method().clone(),
155 path: req.uri().path().to_string(),
156 headers,
157 body: Bytes::from(body),
158 })
159 }
160 SoapDirection::RestToSoap => {
161 let body = req.body().as_bytes();
162 let json: Value = serde_json::from_slice(body).map_err(|e| {
163 TranslationError::InvalidBody(format!("expected a JSON body: {e}"))
164 })?;
165 let envelope = build_envelope(&self.operation, &self.namespace, &json);
166 let mut headers = HeaderMap::new();
167 headers.insert(CONTENT_TYPE, HeaderValue::from_static(SOAP_CONTENT_TYPE));
168 Ok(TranslatedRequest {
169 method: Method::POST,
170 path: req.uri().path().to_string(),
171 headers,
172 body: Bytes::from(envelope.into_bytes()),
173 })
174 }
175 }
176 }
177
178 fn translate_response(
179 &self,
180 resp: &Response<TranslationBody>,
181 ) -> Result<TranslatedResponse, TranslationError> {
182 match self.direction {
183 SoapDirection::SoapToRest => {
184 let body = resp.body().as_bytes();
187 let json: Value = if body.is_empty() {
188 Value::Null
189 } else {
190 serde_json::from_slice(body).map_err(|e| {
191 TranslationError::InvalidBody(format!(
192 "expected a JSON response from the rest upstream: {e}"
193 ))
194 })?
195 };
196 let envelope = build_envelope(&self.operation, &self.namespace, &json);
197 let mut headers = HeaderMap::new();
198 headers.insert(CONTENT_TYPE, HeaderValue::from_static(SOAP_CONTENT_TYPE));
199 Ok(TranslatedResponse {
200 status: resp.status(),
201 headers,
202 body: Bytes::from(envelope.into_bytes()),
203 })
204 }
205 SoapDirection::RestToSoap => {
206 let body = resp.body().as_bytes();
209 let envelope = parse_envelope(body)?;
210 let payload = extract_body_payload(&envelope, &self.operation)?;
211 let json = xml_element_to_json(&payload);
212 let body = serde_json::to_vec(&json).map_err(|e| {
213 TranslationError::TranslationFailed(format!(
214 "failed to serialize soap body to json: {e}"
215 ))
216 })?;
217 let mut headers = HeaderMap::new();
218 headers.insert(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE));
219 Ok(TranslatedResponse {
220 status: resp.status(),
221 headers,
222 body: Bytes::from(body),
223 })
224 }
225 }
226 }
227
228 fn content_type_in(&self) -> &str {
229 match self.direction {
230 SoapDirection::SoapToRest => SOAP_CONTENT_TYPE,
231 SoapDirection::RestToSoap => JSON_CONTENT_TYPE,
232 }
233 }
234
235 fn content_type_out(&self) -> &str {
236 match self.direction {
237 SoapDirection::SoapToRest => JSON_CONTENT_TYPE,
238 SoapDirection::RestToSoap => SOAP_CONTENT_TYPE,
239 }
240 }
241}
242
243fn build_envelope(operation: &str, namespace: &str, payload: &Value) -> String {
250 let mut out = String::with_capacity(256);
251 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
252 out.push_str("<soap:Envelope xmlns:soap=\"");
253 out.push_str(SOAP_ENVELOPE_NS);
254 out.push_str("\">");
255 out.push_str("<soap:Body>");
256 out.push('<');
258 out.push_str(operation);
259 out.push_str(" xmlns=\"");
260 out.push_str(namespace);
261 out.push_str("\">");
262 json_to_xml_element(payload, operation, &mut out);
263 out.push_str("</");
264 out.push_str(operation);
265 out.push('>');
266 out.push_str("</soap:Body>");
267 out.push_str("</soap:Envelope>");
268 out
269}
270
271fn json_to_xml_element(value: &Value, tag: &str, out: &mut String) {
277 match value {
278 Value::Null => {}
279 Value::Bool(b) => {
280 out.push_str(&escape_xml_text(&b.to_string()));
281 }
282 Value::Number(n) => {
283 out.push_str(&escape_xml_text(&n.to_string()));
284 }
285 Value::String(s) => {
286 out.push_str(&escape_xml_text(s));
287 }
288 Value::Array(arr) => {
289 for item in arr {
290 out.push('<');
291 out.push_str(tag);
292 out.push('>');
293 json_to_xml_element(item, tag, out);
294 out.push_str("</");
295 out.push_str(tag);
296 out.push('>');
297 }
298 }
299 Value::Object(obj) => {
300 for (k, v) in obj {
301 out.push('<');
302 out.push_str(&escape_xml_name(k));
303 out.push('>');
304 json_to_xml_element(v, k, out);
305 out.push_str("</");
306 out.push_str(&escape_xml_name(k));
307 out.push('>');
308 }
309 }
310 }
311}
312
313fn extract_body_payload(
317 envelope: &XmlElement,
318 operation: &str,
319) -> Result<XmlElement, TranslationError> {
320 let body = find_child_by_local_name(envelope, "Body").ok_or_else(|| {
321 TranslationError::InvalidBody("soap envelope has no Body element".to_string())
322 })?;
323 for child in &body.children {
327 if let XmlChild::Element(el) = child {
328 if local_name(&el.name) == operation {
329 return Ok(el.clone());
330 }
331 }
332 }
333 Err(TranslationError::InvalidBody(format!(
334 "soap Body has no child element matching operation '{operation}'"
335 )))
336}
337
338fn find_child_by_local_name<'a>(el: &'a XmlElement, name: &str) -> Option<&'a XmlElement> {
341 for child in &el.children {
342 if let XmlChild::Element(c) = child {
343 if local_name(&c.name) == name {
344 return Some(c);
345 }
346 }
347 }
348 None
349}
350
351fn local_name(qname: &str) -> &str {
354 qname
355 .rsplit_once(':')
356 .map(|(_, local)| local)
357 .unwrap_or(qname)
358}
359
360fn xml_element_to_json(el: &XmlElement) -> Value {
363 let mut child_map: std::collections::BTreeMap<String, Vec<Value>> =
365 std::collections::BTreeMap::new();
366 let mut text = String::new();
367 let mut has_elements = false;
368 for child in &el.children {
369 match child {
370 XmlChild::Element(c) => {
371 has_elements = true;
372 child_map
373 .entry(local_name(&c.name).to_string())
374 .or_default()
375 .push(xml_element_to_json(c));
376 }
377 XmlChild::Text(t) => {
378 text.push_str(t);
379 }
380 }
381 }
382
383 if !has_elements {
384 let trimmed = text.trim();
386 if trimmed.is_empty() {
387 return Value::Null;
388 }
389 return parse_scalar(trimmed);
390 }
391
392 let mut obj = serde_json::Map::new();
394 for (attr, val) in &el.attributes {
395 obj.insert(format!("@{attr}"), parse_scalar(val));
396 }
397 for (tag, values) in child_map {
398 if values.len() == 1 {
399 obj.insert(tag, values.into_iter().next().unwrap());
400 } else {
401 obj.insert(tag, Value::Array(values));
402 }
403 }
404 Value::Object(obj)
405}
406
407fn parse_scalar(s: &str) -> Value {
410 if s.eq_ignore_ascii_case("true") {
411 return Value::Bool(true);
412 }
413 if s.eq_ignore_ascii_case("false") {
414 return Value::Bool(false);
415 }
416 if s.eq_ignore_ascii_case("null") {
417 return Value::Null;
418 }
419 if let Ok(n) = s.parse::<i64>() {
420 return Value::Number(n.into());
421 }
422 if let Ok(n) = s.parse::<u64>() {
423 return Value::Number(n.into());
424 }
425 if let Ok(f) = s.parse::<f64>() {
426 if let Some(n) = serde_json::Number::from_f64(f) {
427 return Value::Number(n);
428 }
429 }
430 Value::String(s.to_string())
431}
432
433fn escape_xml_text(s: &str) -> String {
435 let mut out = String::with_capacity(s.len());
436 for c in s.chars() {
437 match c {
438 '<' => out.push_str("<"),
439 '>' => out.push_str(">"),
440 '&' => out.push_str("&"),
441 c => out.push(c),
442 }
443 }
444 out
445}
446
447fn escape_xml_name(name: &str) -> String {
452 let mut out = String::with_capacity(name.len());
453 for (i, c) in name.chars().enumerate() {
454 let valid = if i == 0 {
455 c.is_ascii_alphabetic() || c == '_'
456 } else {
457 c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'
458 };
459 if valid {
460 out.push(c);
461 } else {
462 out.push('_');
463 }
464 }
465 if out.is_empty() {
466 out.push('_');
467 }
468 out
469}
470
471#[derive(Debug, Clone, PartialEq, Eq)]
478pub struct XmlElement {
479 pub name: String,
480 pub attributes: Vec<(String, String)>,
481 pub children: Vec<XmlChild>,
482}
483
484#[derive(Debug, Clone, PartialEq, Eq)]
486pub enum XmlChild {
487 Element(XmlElement),
488 Text(String),
489}
490
491fn parse_envelope(input: &[u8]) -> Result<XmlElement, TranslationError> {
497 let text = std::str::from_utf8(input)
498 .map_err(|e| TranslationError::InvalidBody(format!("soap body is not valid UTF-8: {e}")))?;
499 let mut parser = XmlParser::new(text);
500 parser.skip_prolog()?;
501 let root = parser.parse_element(0)?;
502 parser.skip_whitespace();
504 if !parser.remaining().is_empty() {
505 return Err(TranslationError::InvalidBody(
506 "trailing content after the root element".to_string(),
507 ));
508 }
509 Ok(root)
510}
511
512struct XmlParser<'a> {
515 input: &'a str,
516 pos: usize,
517}
518
519impl<'a> XmlParser<'a> {
520 fn new(input: &'a str) -> Self {
521 XmlParser { input, pos: 0 }
522 }
523
524 fn remaining(&self) -> &'a str {
525 &self.input[self.pos..]
526 }
527
528 fn peek(&self) -> Option<char> {
529 self.remaining().chars().next()
530 }
531
532 fn advance(&mut self, n: usize) {
533 self.pos += n;
534 }
535
536 fn skip_whitespace(&mut self) {
537 while let Some(c) = self.peek() {
538 if c.is_whitespace() {
539 self.advance(c.len_utf8());
540 } else {
541 break;
542 }
543 }
544 }
545
546 fn skip_prolog(&mut self) -> Result<(), TranslationError> {
549 loop {
550 self.skip_whitespace();
551 if self.remaining().starts_with("<?") {
552 let end = self.remaining().find("?>").ok_or_else(|| {
554 TranslationError::InvalidBody("unterminated processing instruction".to_string())
555 })?;
556 self.advance(end + 2);
557 continue;
558 }
559 if self.remaining().starts_with("<!--") {
560 let end = self.remaining().find("-->").ok_or_else(|| {
562 TranslationError::InvalidBody("unterminated comment".to_string())
563 })?;
564 self.advance(end + 3);
565 continue;
566 }
567 if self.remaining().starts_with("<!") {
568 return Err(TranslationError::InvalidBody(
571 "DTDs and entity declarations are not supported".to_string(),
572 ));
573 }
574 break;
575 }
576 Ok(())
577 }
578
579 fn parse_element(&mut self, depth: usize) -> Result<XmlElement, TranslationError> {
581 if depth > PARSE_DEPTH_CAP {
582 return Err(TranslationError::InvalidBody(format!(
583 "xml element nesting exceeds the parser depth cap ({PARSE_DEPTH_CAP})"
584 )));
585 }
586 self.skip_whitespace();
587 if self.peek() != Some('<') {
588 return Err(TranslationError::InvalidBody(
589 "expected '<' to start an element".to_string(),
590 ));
591 }
592 self.advance(1); let name = self.parse_name()?;
594 let mut attributes = Vec::new();
595 loop {
596 self.skip_whitespace();
597 match self.peek() {
598 Some('/') => {
599 self.advance(1);
601 if self.peek() != Some('>') {
602 return Err(TranslationError::InvalidBody(
603 "expected '>' to close an empty element".to_string(),
604 ));
605 }
606 self.advance(1);
607 return Ok(XmlElement {
608 name,
609 attributes,
610 children: Vec::new(),
611 });
612 }
613 Some('>') => {
614 self.advance(1);
615 break;
616 }
617 Some(_) => {
618 let attr = self.parse_attribute()?;
619 attributes.push(attr);
620 }
621 None => {
622 return Err(TranslationError::InvalidBody(
623 "unterminated element start tag".to_string(),
624 ));
625 }
626 }
627 }
628
629 let mut children = Vec::new();
631 loop {
632 let rest = self.remaining();
633 if rest.starts_with("</") {
634 self.advance(2);
635 let close_name = self.parse_name()?;
636 if local_name(&close_name) != local_name(&name) {
637 return Err(TranslationError::InvalidBody(format!(
638 "mismatched closing tag: expected </{name}>, got </{close_name}>"
639 )));
640 }
641 self.skip_whitespace();
642 if self.peek() != Some('>') {
643 return Err(TranslationError::InvalidBody(
644 "expected '>' to close the end tag".to_string(),
645 ));
646 }
647 self.advance(1);
648 return Ok(XmlElement {
649 name,
650 attributes,
651 children,
652 });
653 }
654 if rest.starts_with("<!--") {
655 let end = rest.find("-->").ok_or_else(|| {
656 TranslationError::InvalidBody("unterminated comment".to_string())
657 })?;
658 self.advance(end + 3);
659 continue;
660 }
661 if rest.starts_with("<![CDATA[") {
662 let end = rest.find("]]>").ok_or_else(|| {
663 TranslationError::InvalidBody("unterminated CDATA section".to_string())
664 })?;
665 let cdata = &rest[9..end];
666 children.push(XmlChild::Text(cdata.to_string()));
667 self.advance(end + 3);
668 continue;
669 }
670 if rest.starts_with('<') {
671 let el = self.parse_element(depth + 1)?;
672 children.push(XmlChild::Element(el));
673 continue;
674 }
675 let end = rest.find('<').unwrap_or(rest.len());
677 let text = &rest[..end];
678 if !text.is_empty() {
679 children.push(XmlChild::Text(decode_entities(text)));
680 self.advance(end);
681 }
682 }
683 }
684
685 fn parse_name(&mut self) -> Result<String, TranslationError> {
687 let start = self.pos;
688 while let Some(c) = self.peek() {
689 if c.is_ascii_alphanumeric() || c == '_' || c == ':' || c == '-' || c == '.' {
690 self.advance(c.len_utf8());
691 } else {
692 break;
693 }
694 }
695 if self.pos == start {
696 return Err(TranslationError::InvalidBody(
697 "expected an XML name".to_string(),
698 ));
699 }
700 Ok(self.input[start..self.pos].to_string())
701 }
702
703 fn parse_attribute(&mut self) -> Result<(String, String), TranslationError> {
705 let name = self.parse_name()?;
706 self.skip_whitespace();
707 if self.peek() != Some('=') {
708 return Err(TranslationError::InvalidBody(
709 "expected '=' after an attribute name".to_string(),
710 ));
711 }
712 self.advance(1);
713 self.skip_whitespace();
714 let quote = self.peek().ok_or_else(|| {
715 TranslationError::InvalidBody("expected a quoted attribute value".to_string())
716 })?;
717 if quote != '"' && quote != '\'' {
718 return Err(TranslationError::InvalidBody(
719 "attribute value must be quoted".to_string(),
720 ));
721 }
722 self.advance(1);
723 let value_start = self.pos;
724 while let Some(c) = self.peek() {
725 if c == quote {
726 let value = decode_entities(&self.input[value_start..self.pos]);
727 self.advance(1);
728 return Ok((name, value));
729 }
730 self.advance(c.len_utf8());
731 }
732 Err(TranslationError::InvalidBody(
733 "unterminated attribute value".to_string(),
734 ))
735 }
736}
737
738fn decode_entities(s: &str) -> String {
743 if !s.contains('&') {
744 return s.to_string();
745 }
746 let mut out = String::with_capacity(s.len());
747 let bytes = s.as_bytes();
748 let mut i = 0;
749 while i < bytes.len() {
750 if bytes[i] == b'&' {
751 if let Some(end) = s[i + 1..].find(';') {
752 let entity = &s[i + 1..i + 1 + end];
753 if let Some(decoded) = decode_one_entity(entity) {
754 out.push_str(&decoded);
755 i += 1 + end + 1;
756 continue;
757 }
758 }
759 out.push('&');
761 i += 1;
762 } else {
763 out.push(bytes[i] as char);
764 i += 1;
765 }
766 }
767 out
768}
769
770fn decode_one_entity(entity: &str) -> Option<String> {
772 match entity {
773 "lt" => Some("<".to_string()),
774 "gt" => Some(">".to_string()),
775 "amp" => Some("&".to_string()),
776 "quot" => Some("\"".to_string()),
777 "apos" => Some("'".to_string()),
778 _ => {
779 if let Some(hex) = entity.strip_prefix("#x") {
780 let code = u32::from_str_radix(hex, 16).ok()?;
781 char::from_u32(code).map(|c| c.to_string())
782 } else if let Some(dec) = entity.strip_prefix('#') {
783 let code = dec.parse::<u32>().ok()?;
784 char::from_u32(code).map(|c| c.to_string())
785 } else {
786 None
787 }
788 }
789 }
790}