use std::net::SocketAddr;
use std::sync::Arc;
use axum::extract::{ConnectInfo, Request, State};
use axum::middleware::{Next, from_fn_with_state};
use axum::response::Response;
use cratefield_core::{Config, Harness};
use thiserror::Error;
use tokio::net::TcpListener;
use crate::config::EnvConfig;
use crate::cron::{CronError, spawn_cron_scheduler};
use crate::ports::{client_ip, normalize_headers, trusted_proxy_headers};
use crate::runtime::{Native, clone_ports};
use crate::tracing_setup::install_tracing;
pub const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:8080";
#[derive(Debug, Error)]
pub enum ServeError {
#[error("LISTEN_ADDR {addr:?} is not a valid socket address")]
ListenAddr { addr: String },
#[error("failed to bind {addr}: {source}")]
Bind {
addr: String,
#[source]
source: std::io::Error,
},
#[error("invalid CRONS entry")]
Cron(#[from] CronError),
#[error("server failed: {0}")]
Serve(#[source] std::io::Error),
}
pub async fn serve(harness: Arc<Harness>, runtime: Native) -> Result<(), ServeError> {
install_tracing();
let config = Arc::new(EnvConfig);
let addr = listen_addr(&*config)?;
let listener = TcpListener::bind(addr)
.await
.map_err(|source| ServeError::Bind {
addr: addr.to_string(),
source,
})?;
serve_on(harness, runtime, listener).await
}
pub async fn serve_on(
harness: Arc<Harness>,
runtime: Native,
listener: TcpListener,
) -> Result<(), ServeError> {
install_tracing();
let config = Arc::new(EnvConfig);
let ports = runtime.ports();
let crons = cron_expressions(&*config);
if !crons.is_empty() {
spawn_cron_scheduler(&harness, &ports, &crons)?;
}
let app = harness
.router(clone_ports(&ports))
.layer(from_fn_with_state(
IpTrust {
trusted: trusted_proxy_headers(&*config),
},
client_ip_layer,
));
let addr = listener
.local_addr()
.map(|addr| addr.to_string())
.unwrap_or_default();
tracing::info!(%addr, "cratefield native runtime listening");
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal())
.await
.map_err(ServeError::Serve)
}
fn cron_expressions(config: &dyn Config) -> Vec<String> {
config
.get("CRONS")
.map(|raw| {
raw.split(',')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(str::to_owned)
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
fn listen_addr(config: &dyn Config) -> Result<SocketAddr, ServeError> {
let raw = config
.get("LISTEN_ADDR")
.unwrap_or_else(|| DEFAULT_LISTEN_ADDR.to_owned());
raw.parse::<SocketAddr>()
.map_err(|_| ServeError::ListenAddr { addr: raw })
}
#[derive(Clone)]
struct IpTrust {
trusted: Vec<String>,
}
async fn client_ip_layer(
State(trust): State<IpTrust>,
mut request: Request,
next: Next,
) -> Response {
let peer = request
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|ConnectInfo(addr)| addr.ip());
let resolved = client_ip(request.headers(), &trust.trusted, peer);
normalize_headers(request.headers_mut(), resolved);
next.run(request).await
}
pub async fn shutdown_signal() {
let ctrl_c = tokio::signal::ctrl_c();
#[cfg(unix)]
{
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("install SIGTERM handler");
tokio::select! {
_ = ctrl_c => {},
_ = sigterm.recv() => {},
}
}
#[cfg(not(unix))]
{
if ctrl_c.await.is_err() {
tracing::error!("ctrl_c handler failed; exiting");
}
}
tracing::info!("shutdown signal received; draining in-flight requests");
}
#[cfg(test)]
mod tests {
use super::*;
use cratefield_core::MapConfig;
#[test]
fn crons_default_to_empty_and_split_on_commas() {
assert!(cron_expressions(&MapConfig::default()).is_empty());
let cfg = MapConfig::from_pairs([("CRONS", "*/5 * * * * , 0 0 * * *,,")]);
assert_eq!(cron_expressions(&cfg), ["*/5 * * * *", "0 0 * * *"]);
}
#[test]
fn listen_addr_defaults_to_loopback() {
let addr = listen_addr(&MapConfig::default()).expect("default parses");
assert_eq!(addr.to_string(), "127.0.0.1:8080");
let cfg = MapConfig::from_pairs([("LISTEN_ADDR", "0.0.0.0:9999")]);
let addr = listen_addr(&cfg).expect("parses");
assert_eq!(addr.to_string(), "0.0.0.0:9999");
}
#[test]
fn bad_listen_addr_is_an_error() {
let cfg = MapConfig::from_pairs([("LISTEN_ADDR", "not-an-addr")]);
assert!(matches!(
listen_addr(&cfg),
Err(ServeError::ListenAddr { .. })
));
}
}