diff --git a/Cargo.lock b/Cargo.lock
index 1b7b822..6e9508c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -608,7 +608,7 @@ dependencies = [
"hyper-util",
"pin-project-lite",
"rustls 0.21.12",
- "rustls 0.23.33",
+ "rustls 0.23.34",
"rustls-native-certs 0.8.2",
"rustls-pki-types",
"tokio",
@@ -1501,6 +1501,7 @@ dependencies = [
"tower-http",
"tracing",
"tracing-subscriber",
+ "url",
"uuid",
]
@@ -2274,7 +2275,7 @@ dependencies = [
"http 1.3.1",
"hyper 1.7.0",
"hyper-util",
- "rustls 0.23.33",
+ "rustls 0.23.34",
"rustls-native-certs 0.8.2",
"rustls-pki-types",
"tokio",
@@ -3807,9 +3808,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.33"
+version = "0.23.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c"
+checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
dependencies = [
"aws-lc-rs",
"once_cell",
@@ -4633,7 +4634,7 @@ version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
- "rustls 0.23.33",
+ "rustls 0.23.34",
"tokio",
]
@@ -4872,9 +4873,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
-version = "1.0.19"
+version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
+checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]]
name = "unicode-segmentation"
diff --git a/Cargo.toml b/Cargo.toml
index c7f78d2..57331e7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,6 +44,7 @@ futures = "0.3.31"
mime = "0.3.17"
tower-http = { version = "0.6.6", features = ["trace", "cors", "timeout"] }
tower = { version = "0.5.2", features = ["limit"] }
+url = "2.5.7"
async-trait = "0.1.89"
async-stream = "0.3.6"
uuid = { version = "1.18.1", features = ["v4"] }
diff --git a/docs/topics/configuration.md b/docs/topics/configuration.md
index 0f1006a..fdb57d3 100644
--- a/docs/topics/configuration.md
+++ b/docs/topics/configuration.md
@@ -124,6 +124,7 @@ server:
max-upload-size: 50000000
request-timeout: 60000
graceful-shutdown: true
+ base-path: /
```
@@ -145,6 +146,9 @@ server:
Shutdowns will take no longer than the request timeout configured by server.http.request.timeout.
If disabled, the process is stopped immediately, which will potentially lead to incomplete responses for outstanding requests.
+
+ Sets the base path for all HTTP endpoints.
+
## DIMSE Server Config
diff --git a/src/api/aets.rs b/src/api/aets.rs
index 7caa0a4..f57bc4c 100644
--- a/src/api/aets.rs
+++ b/src/api/aets.rs
@@ -5,7 +5,7 @@ use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Json, Router};
-pub fn api() -> Router {
+pub fn routes() -> Router {
Router::new()
.route("/aets", get(all_aets))
.route("/aets/{aet}", get(aet_health))
diff --git a/src/api/home.rs b/src/api/home.rs
new file mode 100644
index 0000000..876bd75
--- /dev/null
+++ b/src/api/home.rs
@@ -0,0 +1,16 @@
+use crate::AppState;
+use axum::response::IntoResponse;
+use axum::routing::get;
+use axum::Router;
+
+pub fn routes() -> Router {
+ Router::new().route("/", get(index))
+}
+
+// TODO: Return HTML page for a quick user-friendly overview
+async fn index() -> impl IntoResponse {
+ format!(
+ "This server is running DICOM-RST (v{})",
+ env!("CARGO_PKG_VERSION")
+ )
+}
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 9e3d80e..8690e27 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -2,16 +2,26 @@ use crate::AppState;
use axum::Router;
mod aets;
+mod home;
pub mod qido;
pub mod stow;
pub mod wado;
-pub fn routes() -> Router {
- Router::new().merge(aets::api()).nest(
- "/aets/{aet}",
- Router::new()
- .merge(qido::routes())
- .merge(wado::routes())
- .merge(stow::routes()),
- )
+pub fn routes(base_path: &str) -> Router {
+ let router = Router::new()
+ .merge(home::routes())
+ .merge(aets::routes())
+ .nest(
+ "/aets/{aet}",
+ Router::new()
+ .merge(qido::routes())
+ .merge(wado::routes())
+ .merge(stow::routes()),
+ );
+
+ // axum no longer supports nesting at the root
+ match base_path {
+ "/" | "" => router,
+ base_path => Router::new().nest(base_path, router),
+ }
}
diff --git a/src/config/defaults.yaml b/src/config/defaults.yaml
index f210a46..99af723 100644
--- a/src/config/defaults.yaml
+++ b/src/config/defaults.yaml
@@ -9,6 +9,7 @@ server:
max-upload-size: 50000000
request-timeout: 60000
graceful-shutdown: true
+ base-path: /
dimse:
- aet: DICOM-RST
interface: 0.0.0.0
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 46e058e..2a59781 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -201,6 +201,10 @@ impl AppConfig {
))
.add_source(File::with_name("config.yaml").required(false))
.add_source(Environment::with_prefix("DICOM_RST").separator("_"))
+ .set_override_option(
+ "server.http.base-path",
+ std::env::var("DICOM_RST_SERVER_HTTP_BASE_PATH").ok(),
+ )?
.build()?
.try_deserialize()
}
@@ -232,6 +236,27 @@ pub struct HttpServerConfig {
pub max_upload_size: usize,
pub request_timeout: u64,
pub graceful_shutdown: bool,
+ pub base_path: String,
+}
+
+impl HttpServerConfig {
+ const WILDCARD_ADDRESSES: [&'static str; 3] =
+ ["0.0.0.0", "::", "0000:0000:0000:0000:0000:0000:0000:0000"];
+
+ pub fn base_url(&self) -> Result {
+ let origin = format!("http://{}:{}", self.interface, self.port);
+ let mut url = url::Url::parse(&origin)?;
+
+ if url
+ .host()
+ .is_some_and(|host| Self::WILDCARD_ADDRESSES.contains(&host.to_string().as_str()))
+ {
+ url.set_host(Some("127.0.0.1"))?;
+ }
+ let url = url.join(&self.base_path)?;
+
+ Ok(url)
+ }
}
impl Default for HttpServerConfig {
@@ -242,6 +267,7 @@ impl Default for HttpServerConfig {
graceful_shutdown: true,
max_upload_size: 50_000_000, // 50 MB
request_timeout: 60_000, // 1 min
+ base_path: String::from("/"),
}
}
}
diff --git a/src/main.rs b/src/main.rs
index 385898d..74413bb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -129,7 +129,7 @@ async fn run(config: AppConfig) -> anyhow::Result<()> {
});
}
- let app = api::routes()
+ let app = api::routes(&config.server.http.base_path)
.layer(CorsLayer::permissive())
.layer(axum::middleware::from_fn(add_common_headers))
.layer(
@@ -152,7 +152,12 @@ async fn run(config: AppConfig) -> anyhow::Result<()> {
let addr = SocketAddr::from((host, port));
let listener = TcpListener::bind(addr).await?;
- info!("Started DICOMweb server on http://{addr}");
+ info!(
+ server.address = addr.ip().to_string(),
+ server.port = addr.port(),
+ url.full = config.server.http.base_url()?.as_str(),
+ "Started DICOMweb server"
+ );
if config.server.http.graceful_shutdown {
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())