# soap-server
A WSDL-driven SOAP 1.1/1.2 server library for Rust, built on top of [axum](https://docs.rs/axum).
Provide a WSDL file, register async handler closures for each operation, and get a
SOAP 1.1/1.2 endpoint with no boilerplate envelope handling. `soap-server` is a
transport + dispatch layer (not a code generator or full XSD validator) — see
[Capabilities & Limitations](https://navistau.github.io/soap-server/capabilities.html)
for exactly what it does and does not do.
[](https://crates.io/crates/soap-server)
[](https://docs.rs/soap-server)
[](https://crates.io/crates/soap-server)
[](https://github.com/NavistAu/soap-server#license)
---
## Features
- **SOAP 1.1 and 1.2** — auto-detects version from the `Content-Type` header and envelope
namespace; responds in the same version as the incoming request.
- **WSDL-driven dispatch** — operations are discovered from the WSDL at server build time.
Registering a handler for an operation name that does not exist in the WSDL causes
`.build()` to return `Err` — no runtime panics on misnamed operations.
- **WS-Security (UsernameToken)** — supports `PasswordDigest` and `PasswordText`
authentication with nonce replay detection and timestamp freshness checks.
- **XSD structural validation** — required elements in the request body are validated against
the WSDL/XSD schema before the handler is called.
---
## Installation
```sh
cargo add soap-server
cargo add tokio --features full
```
`soap-server` runs on a [Tokio](https://tokio.rs) runtime; adding `tokio` with the
`full` feature is the simplest way to provide one.
---
## Quick Start
```rust,no_run
use soap_server::{FnHandler, ServerBuilder, SoapFault};
use bytes::Bytes;
#[tokio::main]
async fn main() {
let svc = ServerBuilder::from_wsdl_file("path/to/service.wsdl")
.handler(
"MyOperation",
FnHandler::new(|_body: Bytes| async move {
// Parse body, call business logic, return response XML bytes.
Ok::<Bytes, SoapFault>(Bytes::from(
r#"<MyOperationResponse xmlns="urn:example"/>"#,
))
}),
)
.build()
.expect("WSDL build failed");
let router = svc.into_router();
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, router).await.unwrap();
}
```
- `ServerBuilder::from_wsdl_file` loads and parses the WSDL. The builder also accepts raw
bytes or a custom `WsdlLoader` implementation.
- `.handler("MyOperation", ...)` registers an async handler. The `Bytes` your closure
receives is the SOAP Body's first child element as self-contained XML (ancestor namespace
declarations are re-emitted on the fragment root). Return `Ok(Bytes)` with the response
body element (no enclosing envelope needed) or `Err(SoapFault)`.
- `.build()` validates all registered operation names against the WSDL and returns
`Result<SoapService, BuildError>`.
- `svc.into_router()` returns an `axum::Router` mounted at the URL from the WSDL
`<service><port address>` element.
---
## WS-Security
Call `.auth(...)` on the builder to require `UsernameToken` authentication on all
operations:
```rust,no_run
use soap_server::{FnHandler, ServerBuilder, SoapFault};
use bytes::Bytes;
#[tokio::main]
async fn main() {
let svc = ServerBuilder::from_wsdl_file("path/to/service.wsdl")
.auth(|username: &str| -> Option<String> {
match username {
"admin" => Some("s3cr3t".to_string()),
_ => None,
}
})
.auth_bypass(["GetSystemDateAndTime"])
.handler(
"MyOperation",
FnHandler::new(|_body: Bytes| async move {
Ok::<Bytes, SoapFault>(Bytes::from(
r#"<MyOperationResponse xmlns="urn:example"/>"#,
))
}),
)
.build()
.expect("WSDL build failed");
let router = svc.into_router();
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, router).await.unwrap();
}
```
- The `.auth` closure receives the username from `<wsse:UsernameToken>` and returns the
expected plaintext password (`None` to deny). Digest comparison is performed internally
with constant-time equality.
- Both `PasswordText` and `PasswordDigest` (`Base64(SHA-1(nonce + created + password))`)
are accepted.
- Nonce replay detection uses a rotating in-memory cache with a default window of 300 s.
Timestamp freshness is enforced to ±300 s.
- `.auth_bypass(["..."])` exempts named operations from the security header requirement
(useful for clock-sync or discovery operations).
- Operations with a missing or invalid `<wsse:Security>` header receive a `Sender` fault.
---
## Multi-WSDL / Multi-Service
Build each service separately and merge the resulting `axum::Router` instances. Each router
mounts at its own path derived from the respective WSDL:
```rust,no_run
use soap_server::ServerBuilder;
async fn example() -> Result<(), Box<dyn std::error::Error>> {
let svc_a = ServerBuilder::from_wsdl_file("a.wsdl").build()?.into_router();
let svc_b = ServerBuilder::from_wsdl_file("b.wsdl").build()?.into_router();
let app = svc_a.merge(svc_b);
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}
```
---
## Documentation
- **API reference:** <https://docs.rs/soap-server>
- **User guide (mdBook):** <https://navistau.github.io/soap-server/>
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
---
## License
Licensed under either of:
- [MIT License](LICENSE-MIT)
- [Apache License, Version 2.0](LICENSE-APACHE)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for
inclusion in this crate by you shall be dual-licensed as above, without any additional
terms or conditions.
Copyright Joshua Hogendorn / NavistAu.