pub mod auth;
pub mod dto;
pub mod error;
pub mod routes;
use std::net::SocketAddr;
use std::time::Duration;
use axum::middleware;
use axum::Router;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tower_http::cors::CorsLayer;
use tower_http::limit::RequestBodyLimitLayer;
use tower_http::timeout::TimeoutLayer;
use tower_http::trace::TraceLayer;
use crate::config::BindHost;
use crate::error::{Error, Result};
use crate::service::ServiceHandle;
#[derive(Debug)]
pub struct HttpServerHandle {
pub addr: SocketAddr,
pub shutdown: CancellationToken,
pub task: JoinHandle<()>,
}
pub async fn start(handle: ServiceHandle) -> Result<HttpServerHandle> {
let cfg = handle.config();
let net = handle
.inner
.pool
.clone();
let _ = net;
let settings = crate::db::settings::load_all(&handle.inner.pool).await?;
let port = if cfg.http_port == 0 {
0
} else if settings.network.http_api_port != 0 {
settings.network.http_api_port
} else {
cfg.http_port
};
let bind_host = if settings.network.expose_on_lan {
BindHost::AllInterfaces
} else {
BindHost::Loopback
};
if matches!(bind_host, BindHost::AllInterfaces) {
tracing::warn!(
target: "postcrate::http",
"HTTP API bound to 0.0.0.0 — accessible from other devices on this network"
);
}
let bind = SocketAddr::new(bind_host.as_ip(), port);
let mut app = Router::new()
.merge(routes::router())
.with_state(handle.clone())
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.layer(RequestBodyLimitLayer::new(50 * 1024 * 1024))
.layer(TimeoutLayer::with_status_code(
axum::http::StatusCode::REQUEST_TIMEOUT,
Duration::from_secs(30),
));
if let Some(token) = settings.network.api_auth_token.clone().filter(|s| !s.is_empty()) {
tracing::info!(
target: "postcrate::http",
"HTTP API bearer-token auth enabled"
);
app = app.layer(middleware::from_fn(move |req, next| {
let token = token.clone();
async move { auth::require_bearer(&token, req, next).await }
}));
}
let listener = tokio::net::TcpListener::bind(bind)
.await
.map_err(|e| match e.kind() {
std::io::ErrorKind::AddrInUse => Error::PortInUse(port),
_ => Error::Io(e),
})?;
let local_addr = listener.local_addr()?;
let shutdown = CancellationToken::new();
let shutdown_child = shutdown.clone();
#[cfg(feature = "tls")]
let api_tls = settings.network.api_tls
&& cfg.tls.enabled
&& cfg.tls.cert_path.is_some()
&& cfg.tls.key_path.is_some();
#[cfg(not(feature = "tls"))]
let api_tls = false;
#[cfg(feature = "tls")]
let task = if api_tls {
let _ = tokio_rustls::rustls::crypto::ring::default_provider().install_default();
let cert = cfg.tls.cert_path.as_ref().unwrap().clone();
let key = cfg.tls.key_path.as_ref().unwrap().clone();
drop(listener);
let shutdown_child = shutdown_child.clone();
tokio::spawn(async move {
let tls_config = match axum_server::tls_rustls::RustlsConfig::from_pem_file(&cert, &key)
.await
{
Ok(c) => c,
Err(e) => {
tracing::error!(target: "postcrate::http", error = %e, "load TLS pem failed");
return;
}
};
let handle = axum_server::Handle::new();
let handle_for_shutdown = handle.clone();
tokio::spawn(async move {
shutdown_child.cancelled().await;
handle_for_shutdown.graceful_shutdown(Some(Duration::from_secs(5)));
});
if let Err(e) = axum_server::bind_rustls(bind, tls_config)
.handle(handle)
.serve(app.into_make_service())
.await
{
tracing::error!(target: "postcrate::http", error = %e, "https server exited");
}
})
} else {
tokio::spawn(async move {
let serve = axum::serve(listener, app)
.with_graceful_shutdown(async move {
shutdown_child.cancelled().await;
});
if let Err(e) = serve.await {
tracing::error!(target: "postcrate::http", error = %e, "http server exited");
}
})
};
#[cfg(not(feature = "tls"))]
let task = tokio::spawn(async move {
let serve = axum::serve(listener, app)
.with_graceful_shutdown(async move {
shutdown_child.cancelled().await;
});
if let Err(e) = serve.await {
tracing::error!(target: "postcrate::http", error = %e, "http server exited");
}
});
tracing::info!(
target: "postcrate::http",
addr = %local_addr,
tls = api_tls,
"http api listening"
);
Ok(HttpServerHandle {
addr: local_addr,
shutdown,
task,
})
}