1use std::collections::{HashMap, HashSet};
5use std::sync::Arc;
6
7use axum::{
8 extract::{DefaultBodyLimit, MatchedPath, Query, State},
9 http::{HeaderMap, StatusCode},
10 response::{IntoResponse, Response},
11 routing::post,
12 Router,
13};
14use bytes::Bytes;
15use chrono::Utc;
16use tokio::sync::Mutex;
17
18use crate::dispatch::{self, DispatchError, DispatchTable};
19use crate::envelope::{
20 detect_soap_version, parse_envelope, response_content_type, serialize_envelope,
21};
22use crate::fault::SoapFault;
23use crate::handler::SoapHandler;
24use crate::qname::QName;
25use crate::wsdl::definitions::SoapVersion;
26use crate::wsdl::resolver::{
27 resolve_wsdl, rewrite_wsdl_address, rewrite_wsdl_address_for_service, WsdlLoader,
28};
29use crate::wssec::{nonce_cache::RotatingNonceCache, username_token::validate_username_token};
30use crate::xsd::types::TypeRegistry;
31
32const DEFAULT_NONCE_CACHE_HALF_WINDOW_SECS: u64 = 150;
34const DEFAULT_TIMESTAMP_TOLERANCE_SECS: i64 = 300;
36const DEFAULT_MAX_BODY_BYTES: usize = 2 * 1024 * 1024;
38
39type AuthFn = Option<Arc<dyn Fn(&str) -> Option<String> + Send + Sync + 'static>>;
41
42pub struct ServerBuilder {
46 wsdl_bytes: Option<Vec<u8>>,
47 wsdl_path: Option<std::path::PathBuf>,
48 custom_loader: Option<Arc<dyn WsdlLoader>>,
49 handlers: HashMap<String, Arc<dyn SoapHandler>>,
50 default_handler: Option<Arc<dyn SoapHandler>>,
51 auth_fn: AuthFn,
52 auth_bypass: HashSet<String>,
53 mount_path: String,
54 timestamp_tolerance_secs: i64,
55 nonce_cache_half_window_secs: u64,
56 max_body_bytes: usize,
57}
58
59impl ServerBuilder {
60 fn new() -> Self {
61 Self {
62 wsdl_bytes: None,
63 wsdl_path: None,
64 custom_loader: None,
65 handlers: HashMap::new(),
66 default_handler: None,
67 auth_fn: None,
68 auth_bypass: HashSet::new(),
69 mount_path: "/soap".to_string(),
70 timestamp_tolerance_secs: DEFAULT_TIMESTAMP_TOLERANCE_SECS,
71 nonce_cache_half_window_secs: DEFAULT_NONCE_CACHE_HALF_WINDOW_SECS,
72 max_body_bytes: DEFAULT_MAX_BODY_BYTES,
73 }
74 }
75
76 pub fn from_wsdl_file(path: impl Into<std::path::PathBuf>) -> Self {
78 let mut builder = Self::new();
79 builder.wsdl_path = Some(path.into());
80 builder
81 }
82
83 pub fn from_wsdl_bytes(bytes: impl Into<Vec<u8>>) -> Self {
86 let mut builder = Self::new();
87 builder.wsdl_bytes = Some(bytes.into());
88 builder
89 }
90
91 pub fn from_wsdl_bytes_with_loader(
94 bytes: impl Into<Vec<u8>>,
95 loader: impl WsdlLoader + 'static,
96 ) -> Self {
97 let mut builder = Self::new();
98 builder.wsdl_bytes = Some(bytes.into());
99 builder.custom_loader = Some(Arc::new(loader));
100 builder
101 }
102
103 pub fn handler(mut self, operation: impl Into<String>, handler: impl SoapHandler) -> Self {
105 self.handlers.insert(operation.into(), Arc::new(handler));
106 self
107 }
108
109 pub fn default_handler(mut self, handler: impl SoapHandler) -> Self {
112 self.default_handler = Some(Arc::new(handler));
113 self
114 }
115
116 pub fn auth<F>(mut self, f: F) -> Self
124 where
125 F: Fn(&str) -> Option<String> + Send + Sync + 'static,
126 {
127 self.auth_fn = Some(Arc::new(f));
128 self
129 }
130
131 pub fn auth_bypass<I, S>(mut self, ops: I) -> Self
134 where
135 I: IntoIterator<Item = S>,
136 S: Into<String>,
137 {
138 for op in ops {
139 self.auth_bypass.insert(op.into());
140 }
141 self
142 }
143
144 pub fn path(mut self, path: impl Into<String>) -> Self {
146 self.mount_path = path.into();
147 self
148 }
149
150 pub fn timestamp_tolerance_secs(mut self, secs: i64) -> Self {
152 self.timestamp_tolerance_secs = secs;
153 self
154 }
155
156 pub fn max_body_bytes(mut self, bytes: usize) -> Self {
161 self.max_body_bytes = bytes;
162 self
163 }
164
165 pub fn build(self) -> Result<SoapService, BuildError> {
168 let (wsdl_bytes, wsdl_file_path) = match (self.wsdl_bytes, self.wsdl_path) {
170 (Some(bytes), _) => (bytes, None),
171 (None, Some(path)) => {
172 let bytes = std::fs::read(&path).map_err(|e| BuildError::WsdlIo(e.to_string()))?;
173 (bytes, Some(path))
174 }
175 (None, None) => return Err(BuildError::MissingWsdl),
176 };
177
178 let mut visited = HashSet::new();
184 let resolved = if let Some(ref loader) = self.custom_loader {
185 resolve_wsdl(&wsdl_bytes, loader.as_ref(), &mut visited)
186 .map_err(|e| BuildError::WsdlParse(e.to_string()))?
187 } else if let Some(ref path) = wsdl_file_path {
188 let base_dir = path
189 .parent()
190 .unwrap_or_else(|| std::path::Path::new("."))
191 .to_path_buf();
192 let loader = FileWsdlLoader { base_dir };
193 resolve_wsdl(&wsdl_bytes, &loader, &mut visited)
194 .map_err(|e| BuildError::WsdlParse(e.to_string()))?
195 } else {
196 let loader = NoOpLoader;
197 resolve_wsdl(&wsdl_bytes, &loader, &mut visited)
198 .map_err(|e| BuildError::WsdlParse(e.to_string()))?
199 };
200
201 let service_names: Vec<String> = resolved.definition.services.keys().cloned().collect();
205 let is_multi_service = service_names.len() > 1;
206
207 let (dispatch_table, service_tables, service_path_names) = if is_multi_service {
208 let mut service_tables: HashMap<String, Arc<DispatchTable>> = HashMap::new();
212 let mut service_path_names: HashMap<String, String> = HashMap::new();
213 let mut all_ops_in_all_services: HashSet<String> = HashSet::new();
214
215 for svc_name in &service_names {
217 let svc = resolved
218 .definition
219 .services
220 .get(svc_name)
221 .ok_or_else(|| BuildError::UnknownService(svc_name.clone()))?;
222 for port in &svc.ports {
223 let binding_local = &port.binding.local_name;
224 if let Some(binding) = resolved.definition.bindings.get(binding_local) {
225 for binding_op in &binding.operations {
226 all_ops_in_all_services.insert(binding_op.name.clone());
227 }
228 }
229 }
230 }
231
232 for handler_name in self.handlers.keys() {
234 if !all_ops_in_all_services.contains(handler_name) {
235 return Err(BuildError::UnknownOperation(handler_name.clone()));
236 }
237 }
238
239 for svc_name in &service_names {
240 let svc = resolved
241 .definition
242 .services
243 .get(svc_name)
244 .ok_or_else(|| BuildError::UnknownService(svc_name.clone()))?;
245
246 let mut svc_op_names: Vec<String> = Vec::new();
248 for port in &svc.ports {
249 let binding_local = &port.binding.local_name;
250 if let Some(binding) = resolved.definition.bindings.get(binding_local) {
251 for binding_op in &binding.operations {
252 svc_op_names.push(binding_op.name.clone());
253 }
254 }
255 }
256
257 let mut svc_handlers: HashMap<String, Arc<dyn SoapHandler>> = HashMap::new();
259 for op_name in &svc_op_names {
260 if let Some(h) = self.handlers.get(op_name) {
261 svc_handlers.insert(op_name.clone(), h.clone());
262 }
263 }
264
265 let table = dispatch::build_dispatch_table_for_service(
266 svc_name,
267 &resolved,
268 svc_handlers,
269 &self.auth_bypass,
270 self.default_handler.clone(),
271 )
272 .map_err(|e| match e {
273 DispatchError::UnregisteredOperation(op) => {
274 BuildError::UnregisteredOperation(op)
275 }
276 DispatchError::UnknownOperation(op) => BuildError::UnknownOperation(op),
277 DispatchError::UnresolvableInputType { op, element, type_ref } => {
278 BuildError::WsdlParse(format!(
279 "Operation '{op}' input element '{element}' references unresolvable type '{type_ref}'"
280 ))
281 }
282 })?;
283
284 let path = svc
286 .ports
287 .first()
288 .map(|p| extract_path_from_url(&p.address))
289 .unwrap_or_else(|| format!("/{}", svc_name.to_lowercase()));
290
291 service_path_names.insert(path.clone(), svc_name.clone());
292 service_tables.insert(path, Arc::new(table));
293 }
294
295 let first_table = service_tables
299 .values()
300 .next()
301 .cloned()
302 .unwrap_or_else(|| Arc::new(DispatchTable::empty()));
303
304 (first_table, service_tables, service_path_names)
305 } else {
306 let table = dispatch::build_dispatch_table(
308 &resolved,
309 self.handlers,
310 &self.auth_bypass,
311 self.default_handler,
312 )
313 .map_err(|e| match e {
314 DispatchError::UnregisteredOperation(op) => BuildError::UnregisteredOperation(op),
315 DispatchError::UnknownOperation(op) => BuildError::UnknownOperation(op),
316 DispatchError::UnresolvableInputType { op, element, type_ref } => {
317 BuildError::WsdlParse(format!(
318 "Operation '{op}' input element '{element}' references unresolvable type '{type_ref}'"
319 ))
320 }
321 })?;
322 (Arc::new(table), HashMap::new(), HashMap::new())
323 };
324
325 let type_registry = Arc::new(resolved.type_registry);
326
327 Ok(SoapService {
328 dispatch_table,
329 service_tables,
330 service_path_names,
331 type_registry,
332 wsdl_raw: Arc::new(wsdl_bytes),
333 auth_fn: self.auth_fn,
334 nonce_cache: Arc::new(Mutex::new(RotatingNonceCache::new(
335 self.nonce_cache_half_window_secs,
336 ))),
337 timestamp_tolerance_secs: self.timestamp_tolerance_secs,
338 mount_path: self.mount_path,
339 max_body_bytes: self.max_body_bytes,
340 })
341 }
342}
343
344#[derive(Debug, thiserror::Error)]
348pub enum BuildError {
349 #[error("No WSDL source provided — call from_wsdl_bytes() or from_wsdl_file()")]
350 MissingWsdl,
351 #[error("Failed to read WSDL file: {0}")]
352 WsdlIo(String),
353 #[error("Failed to parse or resolve WSDL: {0}")]
354 WsdlParse(String),
355 #[error("WSDL operation '{0}' has no registered handler")]
356 UnregisteredOperation(String),
357 #[error("Registered handler '{0}' has no matching WSDL operation")]
358 UnknownOperation(String),
359 #[error("WSDL service '{0}' not found in resolved definition")]
360 UnknownService(String),
361}
362
363struct NoOpLoader;
366
367impl WsdlLoader for NoOpLoader {
368 fn load(&self, location: &str) -> Result<Vec<u8>, crate::wsdl::parser::WsdlError> {
369 Err(crate::wsdl::parser::WsdlError::MalformedXml(format!(
370 "External WSDL import '{location}' not supported in embedded mode"
371 )))
372 }
373}
374
375pub struct FileWsdlLoader {
378 base_dir: std::path::PathBuf,
379}
380
381impl WsdlLoader for FileWsdlLoader {
382 fn load(&self, location: &str) -> Result<Vec<u8>, crate::wsdl::parser::WsdlError> {
383 let raw_path = self.base_dir.join(location);
385 let path = normalize_path(&raw_path);
386 std::fs::read(&path).map_err(|e| {
387 crate::wsdl::parser::WsdlError::MalformedXml(format!(
388 "Failed to load WSDL import '{location}' from '{}': {e}",
389 path.display()
390 ))
391 })
392 }
393}
394
395fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
398 use std::path::Component;
399 let mut normalized = std::path::PathBuf::new();
400 for component in path.components() {
401 match component {
402 Component::ParentDir => {
403 normalized.pop();
404 }
405 Component::CurDir => {}
406 other => {
407 normalized.push(other);
408 }
409 }
410 }
411 normalized
412}
413
414fn extract_path_from_url(url: &str) -> String {
417 if let Some(after_scheme) = url.split_once("://").map(|x| x.1) {
418 if let Some(slash_pos) = after_scheme.find('/') {
419 return after_scheme[slash_pos..].to_string();
420 }
421 return "/".to_string();
422 }
423 if url.starts_with('/') {
424 url.to_string()
425 } else {
426 format!("/{url}")
427 }
428}
429
430pub struct SoapService {
434 dispatch_table: Arc<DispatchTable>,
435 service_tables: HashMap<String, Arc<DispatchTable>>,
439 service_path_names: HashMap<String, String>,
442 type_registry: Arc<TypeRegistry>,
443 wsdl_raw: Arc<Vec<u8>>,
444 auth_fn: AuthFn,
445 nonce_cache: Arc<Mutex<RotatingNonceCache>>,
446 timestamp_tolerance_secs: i64,
447 mount_path: String,
448 max_body_bytes: usize,
449}
450
451impl std::fmt::Debug for SoapService {
452 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453 f.debug_struct("SoapService")
454 .field("mount_path", &self.mount_path)
455 .finish_non_exhaustive()
456 }
457}
458
459impl SoapService {
460 pub fn into_router(self) -> Router {
466 let max_body = self.max_body_bytes;
467 if !self.service_tables.is_empty() {
468 let state = Arc::new(self);
470 let mut router = Router::new();
471 let routes: Vec<(String, Arc<DispatchTable>, String)> = state
474 .service_tables
475 .iter()
476 .map(|(path, table)| {
477 let svc_name = state
478 .service_path_names
479 .get(path)
480 .cloned()
481 .unwrap_or_default();
482 (path.clone(), table.clone(), svc_name)
483 })
484 .collect();
485 for (path, table, service_name) in routes {
486 let route_state = SoapServiceRoute {
487 svc: state.clone(),
488 table,
489 service_name,
490 };
491 router = router.route(
492 &path,
493 post(soap_post_handler_for_route)
494 .get(wsdl_get_handler_for_route)
495 .with_state(route_state),
496 );
497 }
498 router.layer(DefaultBodyLimit::max(max_body))
499 } else {
500 let mount_path = self.mount_path.clone();
502 let state = Arc::new(self);
503 Router::new()
504 .route(&mount_path, post(soap_post_handler).get(wsdl_get_handler))
505 .with_state(state)
506 .layer(DefaultBodyLimit::max(max_body))
507 }
508 }
509}
510
511#[derive(Clone)]
513struct SoapServiceRoute {
514 svc: Arc<SoapService>,
515 table: Arc<DispatchTable>,
516 service_name: String,
518}
519
520fn fault_response(fault: SoapFault, version: crate::wsdl::definitions::SoapVersion) -> Response {
523 let bytes = fault.to_xml_bytes_versioned(&version);
524 let ct = response_content_type(&version);
525 (
526 StatusCode::INTERNAL_SERVER_ERROR,
527 [("Content-Type", ct)],
528 bytes,
529 )
530 .into_response()
531}
532
533fn extract_body_qname(body_bytes: &[u8]) -> Result<QName, SoapFault> {
536 use quick_xml::events::Event;
537 use quick_xml::NsReader;
538
539 let mut reader = NsReader::from_reader(body_bytes);
540 reader.config_mut().trim_text(true);
541
542 loop {
543 match reader
544 .read_resolved_event()
545 .map_err(|e| SoapFault::sender(format!("XML parse error in body: {e}")))?
546 {
547 (_, Event::Eof) => {
548 return Err(SoapFault::sender("Empty SOAP Body element"));
549 }
550 (resolved_ns, Event::Start(e)) | (resolved_ns, Event::Empty(e)) => {
551 let local = std::str::from_utf8(e.local_name().as_ref())
552 .map_err(|e| SoapFault::sender(format!("Invalid UTF-8 in element name: {e}")))?
553 .to_string();
554 let ns = match resolved_ns {
555 quick_xml::name::ResolveResult::Bound(ns) => std::str::from_utf8(ns.0)
556 .map_err(|e| SoapFault::sender(format!("Invalid UTF-8 in namespace: {e}")))?
557 .to_string(),
558 _ => String::new(),
559 };
560 if ns.is_empty() {
561 return Ok(QName::local(&local));
562 } else {
563 return Ok(QName::new(&ns, &local));
564 }
565 }
566 _ => {}
567 }
568 }
569}
570
571const WSSE_NS: &str =
575 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
576
577fn find_security_header(header_children: &[Bytes]) -> Option<&Bytes> {
581 use quick_xml::events::Event;
582 use quick_xml::NsReader;
583
584 for child in header_children {
585 let mut reader = NsReader::from_reader(child.as_ref());
586 reader.config_mut().trim_text(true);
587 loop {
589 match reader.read_resolved_event() {
590 Ok((resolved_ns, Event::Start(e))) | Ok((resolved_ns, Event::Empty(e))) => {
591 let local = e.local_name();
592 let local_str = std::str::from_utf8(local.as_ref()).unwrap_or("");
593 if local_str == "Security" {
594 if let quick_xml::name::ResolveResult::Bound(ns) = resolved_ns {
595 if std::str::from_utf8(ns.0).unwrap_or("") == WSSE_NS {
596 return Some(child);
597 }
598 }
599 }
600 break; }
602 Ok((_, Event::Eof)) | Err(_) => break,
603 _ => {}
604 }
605 }
606 }
607 None
608}
609
610async fn soap_post_handler(
613 State(svc): State<Arc<SoapService>>,
614 headers: HeaderMap,
615 body: Bytes,
616) -> Response {
617 let content_type = headers
619 .get("content-type")
620 .and_then(|v| v.to_str().ok())
621 .unwrap_or("");
622
623 let soap_version = match detect_soap_version(content_type) {
624 Ok(v) => v,
625 Err(fault) => return fault_response(fault, crate::wsdl::definitions::SoapVersion::Soap12),
626 };
627
628 let envelope = match parse_envelope(&body) {
630 Ok(e) => e,
631 Err(fault) => return fault_response(fault, soap_version),
632 };
633
634 let body_qname = match extract_body_qname(&envelope.body_element) {
636 Ok(q) => q,
637 Err(fault) => return fault_response(fault, envelope.soap_version.clone()),
638 };
639
640 let soap_action = headers
642 .get("soapaction")
643 .or_else(|| headers.get("SOAPAction"))
644 .and_then(|v| v.to_str().ok())
645 .map(|s| s.trim_matches('"'));
646
647 let entry = match dispatch::route(&svc.dispatch_table, &body_qname, soap_action) {
648 Ok(e) => e,
649 Err(fault) => return fault_response(fault, envelope.soap_version.clone()),
650 };
651
652 if entry.auth_required && svc.auth_fn.is_some() {
657 match find_security_header(&envelope.header_children) {
658 None => {
659 return fault_response(
660 SoapFault::sender("WS-Security header required but not provided"),
661 envelope.soap_version.clone(),
662 );
663 }
664 Some(security_bytes) => {
665 let auth_fn = match &svc.auth_fn {
666 Some(f) => f.clone(),
667 None => {
668 return fault_response(
669 SoapFault::sender(
670 "Authentication required but no credential store configured",
671 ),
672 envelope.soap_version.clone(),
673 );
674 }
675 };
676 let mut nonce_cache = svc.nonce_cache.lock().await;
677 let now = Utc::now();
678 if let Err(fault) = validate_username_token(
679 security_bytes,
680 auth_fn.as_ref(),
681 &mut nonce_cache,
682 svc.timestamp_tolerance_secs,
683 now,
684 ) {
685 return fault_response(fault, envelope.soap_version.clone());
686 }
687 }
688 }
689 }
690
691 if let Err(fault) = dispatch::validate_request(
693 &envelope.body_element,
694 &svc.type_registry,
695 entry.validation_type.as_ref(),
696 ) {
697 return fault_response(fault, envelope.soap_version.clone());
698 }
699
700 let response_body = match entry
702 .handler
703 .handle_with_headers(envelope.body_element, &envelope.header_children)
704 .await
705 {
706 Ok(bytes) => bytes,
707 Err(fault) => return fault_response(fault, envelope.soap_version.clone()),
708 };
709
710 let response_version = envelope.soap_version.clone();
715 let envelope_bytes = serialize_envelope(response_body, response_version.clone());
716 let content_type_value = response_content_type(&response_version);
717
718 (
719 StatusCode::OK,
720 [("Content-Type", content_type_value)],
721 envelope_bytes,
722 )
723 .into_response()
724}
725
726async fn soap_post_handler_for_route(
729 State(route_state): State<SoapServiceRoute>,
730 headers: HeaderMap,
731 body: Bytes,
732) -> Response {
733 let svc = &route_state.svc;
734 let table = &route_state.table;
735
736 let content_type = headers
738 .get("content-type")
739 .and_then(|v| v.to_str().ok())
740 .unwrap_or("");
741
742 let soap_version = match detect_soap_version(content_type) {
743 Ok(v) => v,
744 Err(fault) => return fault_response(fault, SoapVersion::Soap12),
745 };
746
747 let envelope = match parse_envelope(&body) {
749 Ok(e) => e,
750 Err(fault) => return fault_response(fault, soap_version),
751 };
752
753 let body_qname = match extract_body_qname(&envelope.body_element) {
755 Ok(q) => q,
756 Err(fault) => return fault_response(fault, envelope.soap_version.clone()),
757 };
758
759 let soap_action = headers
761 .get("soapaction")
762 .or_else(|| headers.get("SOAPAction"))
763 .and_then(|v| v.to_str().ok())
764 .map(|s| s.trim_matches('"'));
765
766 let entry = match dispatch::route(table, &body_qname, soap_action) {
767 Ok(e) => e,
768 Err(fault) => return fault_response(fault, envelope.soap_version.clone()),
769 };
770
771 if entry.auth_required && svc.auth_fn.is_some() {
776 match find_security_header(&envelope.header_children) {
777 None => {
778 return fault_response(
779 SoapFault::sender("WS-Security header required but not provided"),
780 envelope.soap_version.clone(),
781 );
782 }
783 Some(security_bytes) => {
784 let auth_fn = match &svc.auth_fn {
785 Some(f) => f.clone(),
786 None => {
787 return fault_response(
788 SoapFault::sender(
789 "Authentication required but no credential store configured",
790 ),
791 envelope.soap_version.clone(),
792 );
793 }
794 };
795 let mut nonce_cache = svc.nonce_cache.lock().await;
796 let now = Utc::now();
797 if let Err(fault) = validate_username_token(
798 security_bytes,
799 auth_fn.as_ref(),
800 &mut nonce_cache,
801 svc.timestamp_tolerance_secs,
802 now,
803 ) {
804 return fault_response(fault, envelope.soap_version.clone());
805 }
806 }
807 }
808 }
809
810 if let Err(fault) = dispatch::validate_request(
812 &envelope.body_element,
813 &svc.type_registry,
814 entry.validation_type.as_ref(),
815 ) {
816 return fault_response(fault, envelope.soap_version.clone());
817 }
818
819 let response_body = match entry
821 .handler
822 .handle_with_headers(envelope.body_element, &envelope.header_children)
823 .await
824 {
825 Ok(bytes) => bytes,
826 Err(fault) => return fault_response(fault, envelope.soap_version.clone()),
827 };
828
829 let response_version = envelope.soap_version.clone();
833 let envelope_bytes = serialize_envelope(response_body, response_version.clone());
834 let content_type_value = response_content_type(&response_version);
835
836 (
837 StatusCode::OK,
838 [("Content-Type", content_type_value)],
839 envelope_bytes,
840 )
841 .into_response()
842}
843
844#[derive(serde::Deserialize)]
847struct WsdlQuery {
848 wsdl: Option<String>,
849}
850
851async fn wsdl_get_handler(
852 matched_path: Option<MatchedPath>,
853 State(svc): State<Arc<SoapService>>,
854 Query(params): Query<WsdlQuery>,
855 headers: HeaderMap,
856) -> Response {
857 if params.wsdl.is_none() {
859 return StatusCode::NOT_FOUND.into_response();
860 }
861
862 let host = headers
864 .get("x-forwarded-host")
865 .or_else(|| headers.get("host"))
866 .and_then(|v| v.to_str().ok())
867 .unwrap_or("localhost");
868
869 let path = matched_path
872 .as_ref()
873 .map(|mp| mp.as_str())
874 .unwrap_or(&svc.mount_path);
875
876 let server_url = format!("http://{host}{path}");
877
878 let rewritten = rewrite_wsdl_address(&svc.wsdl_raw, &server_url);
879
880 (
881 StatusCode::OK,
882 [("Content-Type", "text/xml; charset=utf-8")],
883 rewritten,
884 )
885 .into_response()
886}
887
888async fn wsdl_get_handler_for_route(
891 matched_path: Option<MatchedPath>,
892 State(route_state): State<SoapServiceRoute>,
893 Query(params): Query<WsdlQuery>,
894 headers: HeaderMap,
895) -> Response {
896 if params.wsdl.is_none() {
898 return StatusCode::NOT_FOUND.into_response();
899 }
900
901 let svc = &route_state.svc;
902
903 let host = headers
905 .get("x-forwarded-host")
906 .or_else(|| headers.get("host"))
907 .and_then(|v| v.to_str().ok())
908 .unwrap_or("localhost");
909
910 let path = matched_path
911 .as_ref()
912 .map(|mp| mp.as_str())
913 .unwrap_or(&svc.mount_path);
914
915 let server_url = format!("http://{host}{path}");
916
917 let rewritten =
921 rewrite_wsdl_address_for_service(&svc.wsdl_raw, &server_url, &route_state.service_name);
922
923 (
924 StatusCode::OK,
925 [("Content-Type", "text/xml; charset=utf-8")],
926 rewritten,
927 )
928 .into_response()
929}
930
931#[cfg(test)]
932mod tests {
933 use super::*;
934 use crate::fault::SoapFault;
935 use crate::handler::FnHandler;
936 use bytes::Bytes;
937
938 const MINIMAL_WSDL: &[u8] = br#"<?xml version="1.0" encoding="utf-8"?>
939<wsdl:definitions
940 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
941 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap12/"
942 xmlns:xs="http://www.w3.org/2001/XMLSchema"
943 xmlns:tns="http://example.com/test"
944 targetNamespace="http://example.com/test">
945 <wsdl:types>
946 <xs:schema targetNamespace="http://example.com/test" elementFormDefault="qualified">
947 <xs:element name="Ping">
948 <xs:complexType><xs:sequence/></xs:complexType>
949 </xs:element>
950 <xs:element name="PingResponse">
951 <xs:complexType><xs:sequence/></xs:complexType>
952 </xs:element>
953 </xs:schema>
954 </wsdl:types>
955 <wsdl:message name="PingRequest">
956 <wsdl:part name="parameters" element="tns:Ping"/>
957 </wsdl:message>
958 <wsdl:message name="PingResponse">
959 <wsdl:part name="parameters" element="tns:PingResponse"/>
960 </wsdl:message>
961 <wsdl:portType name="TestPortType">
962 <wsdl:operation name="Ping">
963 <wsdl:input message="tns:PingRequest"/>
964 <wsdl:output message="tns:PingResponse"/>
965 </wsdl:operation>
966 </wsdl:portType>
967 <wsdl:binding name="TestBinding" type="tns:TestPortType">
968 <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
969 <wsdl:operation name="Ping">
970 <soap:operation soapAction="http://example.com/test/Ping"/>
971 <wsdl:input><soap:body use="literal"/></wsdl:input>
972 <wsdl:output><soap:body use="literal"/></wsdl:output>
973 </wsdl:operation>
974 </wsdl:binding>
975 <wsdl:service name="TestService">
976 <wsdl:port name="TestPort" binding="tns:TestBinding">
977 <soap:address location="http://localhost/soap"/>
978 </wsdl:port>
979 </wsdl:service>
980</wsdl:definitions>"#;
981
982 #[test]
983 fn server_builder_builds_without_panic() {
984 let svc = ServerBuilder::from_wsdl_bytes(MINIMAL_WSDL)
985 .handler(
986 "Ping",
987 FnHandler::new(|_body: Bytes| async move {
988 Ok::<Bytes, SoapFault>(Bytes::from_static(b"<PingResponse/>"))
989 }),
990 )
991 .auth_bypass(["Ping"])
992 .build();
993 assert!(svc.is_ok(), "build should succeed: {:?}", svc.err());
994 }
995
996 #[test]
997 fn server_builder_into_router_returns_router() {
998 let svc = ServerBuilder::from_wsdl_bytes(MINIMAL_WSDL)
999 .handler(
1000 "Ping",
1001 FnHandler::new(|_body: Bytes| async move {
1002 Ok::<Bytes, SoapFault>(Bytes::from_static(b"<PingResponse/>"))
1003 }),
1004 )
1005 .auth_bypass(["Ping"])
1006 .build()
1007 .unwrap();
1008 let _router = svc.into_router();
1010 }
1011
1012 #[test]
1013 fn server_builder_fails_with_unregistered_operation() {
1014 let result = ServerBuilder::from_wsdl_bytes(MINIMAL_WSDL).build();
1016 assert!(result.is_err());
1017 match result.unwrap_err() {
1018 BuildError::UnregisteredOperation(op) => assert_eq!(op, "Ping"),
1019 other => panic!("Expected UnregisteredOperation, got: {other:?}"),
1020 }
1021 }
1022
1023 #[test]
1024 fn server_builder_fails_with_unknown_handler_name() {
1025 let result = ServerBuilder::from_wsdl_bytes(MINIMAL_WSDL)
1026 .handler(
1027 "Ping",
1028 FnHandler::new(|_body: Bytes| async move {
1029 Ok::<Bytes, SoapFault>(Bytes::from_static(b"<PingResponse/>"))
1030 }),
1031 )
1032 .handler(
1033 "NonExistentOp",
1034 FnHandler::new(|_body: Bytes| async move {
1035 Ok::<Bytes, SoapFault>(Bytes::from_static(b"<resp/>"))
1036 }),
1037 )
1038 .auth_bypass(["Ping"])
1039 .build();
1040 assert!(result.is_err());
1041 match result.unwrap_err() {
1042 BuildError::UnknownOperation(op) => assert_eq!(op, "NonExistentOp"),
1043 other => panic!("Expected UnknownOperation, got: {other:?}"),
1044 }
1045 }
1046
1047 #[test]
1048 fn fault_response_soap12_content_type() {
1049 use crate::wsdl::definitions::SoapVersion;
1050 let fault = SoapFault::sender("test");
1051 let response = fault_response(fault, SoapVersion::Soap12);
1052 let ct = response.headers().get("content-type").unwrap();
1053 assert_eq!(ct.to_str().unwrap(), "application/soap+xml; charset=utf-8");
1054 }
1055
1056 #[test]
1057 fn fault_response_soap11_content_type() {
1058 use crate::wsdl::definitions::SoapVersion;
1059 let fault = SoapFault::sender("test");
1060 let response = fault_response(fault, SoapVersion::Soap11);
1061 let ct = response.headers().get("content-type").unwrap();
1062 assert_eq!(ct.to_str().unwrap(), "text/xml; charset=utf-8");
1063 }
1064
1065 #[test]
1066 fn extract_body_qname_parses_namespaced_element() {
1067 let bytes = b"<tns:Ping xmlns:tns=\"http://example.com/test\"/>";
1068 let qname = extract_body_qname(bytes).unwrap();
1069 assert_eq!(qname.local_name, "Ping");
1070 assert_eq!(qname.namespace.as_deref(), Some("http://example.com/test"));
1071 }
1072
1073 #[test]
1074 fn extract_body_qname_parses_unnamespaced_element() {
1075 let bytes = b"<Ping/>";
1076 let qname = extract_body_qname(bytes).unwrap();
1077 assert_eq!(qname.local_name, "Ping");
1078 assert_eq!(qname.namespace, None);
1079 }
1080
1081 #[test]
1084 fn find_security_header_matches_wsse_namespace() {
1085 let wsse_header = Bytes::from_static(
1086 br#"<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:UsernameToken/></wsse:Security>"#,
1087 );
1088 let headers = [wsse_header.clone()];
1089 let result = find_security_header(&headers);
1090 assert!(
1091 result.is_some(),
1092 "Expected wsse:Security header to be found"
1093 );
1094 assert_eq!(result.unwrap(), &wsse_header);
1095 }
1096
1097 #[test]
1098 fn find_security_header_ignores_non_wsse_security_element() {
1099 let fake_security = Bytes::from_static(
1101 br#"<ns:Security xmlns:ns="http://example.com/other">some content</ns:Security>"#,
1102 );
1103 let headers = [fake_security];
1104 let result = find_security_header(&headers);
1105 assert!(
1106 result.is_none(),
1107 "Security element in non-WSSE namespace should NOT be selected"
1108 );
1109 }
1110
1111 #[test]
1112 fn find_security_header_ignores_element_containing_security_substring() {
1113 let unrelated = Bytes::from_static(
1115 br#"<ns:Header xmlns:ns="http://example.com/other">Security policy here</ns:Header>"#,
1116 );
1117 let headers = [unrelated];
1118 let result = find_security_header(&headers);
1119 assert!(
1120 result.is_none(),
1121 "Header containing 'Security' substring but wrong QName should NOT be selected"
1122 );
1123 }
1124
1125 #[test]
1126 fn find_security_header_returns_first_valid_wsse_header() {
1127 let fake_security = Bytes::from_static(
1128 br#"<ns:Security xmlns:ns="http://example.com/other">content</ns:Security>"#,
1129 );
1130 let real_security = Bytes::from_static(
1131 br#"<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"/>"#,
1132 );
1133 let headers = [fake_security, real_security.clone()];
1134 let result = find_security_header(&headers);
1135 assert!(result.is_some());
1136 assert_eq!(result.unwrap(), &real_security);
1137 }
1138
1139 #[test]
1142 fn server_builder_max_body_bytes_sets_field() {
1143 let svc = ServerBuilder::from_wsdl_bytes(MINIMAL_WSDL)
1145 .handler(
1146 "Ping",
1147 FnHandler::new(|_body: Bytes| async move {
1148 Ok::<Bytes, SoapFault>(Bytes::from_static(b"<PingResponse/>"))
1149 }),
1150 )
1151 .auth_bypass(["Ping"])
1152 .max_body_bytes(512 * 1024) .build()
1154 .expect("build should succeed");
1155 assert_eq!(svc.max_body_bytes, 512 * 1024);
1157 let _router = svc.into_router();
1159 }
1160
1161 #[test]
1162 fn server_builder_default_max_body_bytes_is_2mib() {
1163 let svc = ServerBuilder::from_wsdl_bytes(MINIMAL_WSDL)
1164 .handler(
1165 "Ping",
1166 FnHandler::new(|_body: Bytes| async move {
1167 Ok::<Bytes, SoapFault>(Bytes::from_static(b"<PingResponse/>"))
1168 }),
1169 )
1170 .auth_bypass(["Ping"])
1171 .build()
1172 .expect("build should succeed");
1173 assert_eq!(svc.max_body_bytes, 2 * 1024 * 1024);
1174 }
1175}