use axum::{
routing::{any, get},
Router,
};
use mesofact_core::proxy::cache::ResponseCache;
use mesofact_core::proxy::config::Config;
use mesofact_core::proxy::manifest_loader::{load_from_file, watch_manifest};
use mesofact_core::proxy::metrics::Metrics;
use mesofact_core::proxy::router::{handle, metrics_handler, AppState, SharedState};
use mesofact_core::proxy::session::{CookieSessionResolver, SessionResolver};
use mesofact_core::proxy::source_gen::Generations;
use mesofact_core::proxy::worker_pool::{rolling_reload, WorkerPool};
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::{watch, RwLock};
use tracing::{info, warn};
use tracing_subscriber::EnvFilter;
pub async fn run(cfg: Config) -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let support = cfg.policy_support();
assert_declared_policy_is_enforced(&cfg.manifest, &support).await?;
let manifest = Arc::new(load_from_file(&cfg.manifest).await?);
info!(
build_id = %manifest.build_id,
routes = manifest.routes.len(),
enforced = ?support.enforced().map(|p| p.field()).collect::<Vec<_>>(),
delegated = ?support.delegated().map(|p| p.field()).collect::<Vec<_>>(),
"manifest loaded"
);
let manifest_json = serde_json::to_vec(&*manifest)?;
let pool = WorkerPool::spawn_with_config(
&manifest_json,
cfg.worker_entry.clone(),
cfg.worker_count(),
cfg.sources_config.clone(),
)
.await?;
let generations = Arc::new(match &cfg.sources_config {
Some(path) => Generations::from_config_file(path)?,
None => Generations::empty(),
});
let session = build_session_resolver(&cfg);
let metrics = Arc::new(Metrics::new());
pool.attach_metrics(metrics.clone());
let mut app_state = AppState::new(
manifest.clone(),
pool,
cfg.cdn_base_url.clone(),
cfg.fallback_dir.clone(),
)
.with_cache(Arc::new(ResponseCache::with_capacity(cfg.cache_capacity)))
.with_generations(generations)
.with_login_url(cfg.login_url.clone())
.with_metrics(metrics.clone());
if let Some(resolver) = session {
app_state = app_state.with_session(resolver);
}
let state: SharedState = Arc::new(RwLock::new(app_state));
let (tx, mut rx) = watch::channel(manifest.clone());
watch_manifest(cfg.manifest.clone(), tx);
{
let state = state.clone();
let worker_entry = cfg.worker_entry.clone();
let n = cfg.worker_count();
let metrics = metrics.clone();
let manifest_path = cfg.manifest.clone();
let support = support.clone();
tokio::spawn(async move {
loop {
if rx.changed().await.is_err() {
break;
}
if let Err(e) = assert_declared_policy_is_enforced(&manifest_path, &support).await {
tracing::error!("{e}; keeping the previous manifest");
continue;
}
let new_manifest = rx.borrow().clone();
let json = match serde_json::to_vec(&*new_manifest) {
Ok(j) => j,
Err(e) => {
tracing::error!("failed to serialise new manifest: {e}");
continue;
}
};
let old_pool = state.read().await.pool.clone();
match rolling_reload(old_pool, &json, worker_entry.clone(), n).await {
Ok(new_pool) => {
new_pool.attach_metrics(metrics.clone());
let mut st = state.write().await;
st.pool = new_pool;
st.manifest = new_manifest;
st.matcher = mesofact_core::proxy::router::build_matcher(&st.manifest);
drop(st);
info!("rolling reload complete");
}
Err(e) => {
tracing::error!("new pool failed to start, keeping old manifest: {e}");
}
}
}
});
}
let app = Router::new()
.route("/metrics", get(metrics_handler))
.route("/{*path}", any(handle))
.route("/", any(handle))
.with_state(state);
let listener = TcpListener::bind(&cfg.bind).await?;
info!(addr = %cfg.bind, "listening");
axum::serve(listener, app).await?;
Ok(())
}
async fn assert_declared_policy_is_enforced(
manifest: &std::path::Path,
support: &mesofact_core::PolicySupport,
) -> anyhow::Result<()> {
let raw = tokio::fs::read(manifest).await.map_err(|e| {
anyhow::anyhow!(
"refusing to serve: cannot read {} to check for declared policy this binary does \
not enforce: {e}",
manifest.display(),
)
})?;
mesofact_core::check_manifest(&raw, support).map_err(|e| anyhow::anyhow!("{e}"))
}
fn build_session_resolver(cfg: &Config) -> Option<Arc<dyn SessionResolver>> {
let env_name = cfg.session_secret_env.as_ref()?;
match std::env::var(env_name) {
Ok(secret) if !secret.is_empty() => {
info!(cookie = %cfg.session_cookie, "session resolver enabled");
Some(Arc::new(CookieSessionResolver::new(
cfg.session_cookie.clone(),
secret.into_bytes(),
)))
}
_ => {
warn!(
env = %env_name,
"session secret env var is unset/empty — sessions disabled"
);
None
}
}
}