use std::net::SocketAddr;
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
#[derive(Parser)]
#[command(
name = "mnem http",
version,
about = "HTTP JSON API for mnem.",
long_about = None
)]
struct Cli {
#[arg(long, short = 'R', default_value = ".")]
repo: PathBuf,
#[arg(long, default_value = "127.0.0.1:9876")]
bind: SocketAddr,
#[arg(long)]
in_memory: bool,
#[arg(long)]
metrics: bool,
#[arg(long, conflicts_with = "metrics")]
no_metrics: bool,
}
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
let cli = Cli::parse();
if !cli.bind.ip().is_loopback() && std::env::var_os("MNEM_HTTP_ALLOW_NON_LOOPBACK").is_none() {
eprintln!(
"mnem http: refusing to bind non-loopback address {} without an explicit opt-in.\n\
\n\
mnem http has NO authentication layer in v1. Binding to a non-loopback address\n\
(like 0.0.0.0 or a LAN IP) exposes every node, every retrieval, and every\n\
write endpoint to anyone who can reach the interface.\n\
\n\
If you really do want to bind publicly (e.g. behind a reverse proxy that\n\
adds auth), set MNEM_HTTP_ALLOW_NON_LOOPBACK=1 in the environment:\n\
\n\
\tMNEM_HTTP_ALLOW_NON_LOOPBACK=1 mnem http --bind {}\n\
\n\
Loopback (127.0.0.1, ::1) needs no flag.\n\
\n\
hint: see docs/RUNBOOK.md#4-auth-refused-on-non-loopback-bind for the full\n\
remediation walkthrough (proxy setup, opt-in env var, logging posture).",
cli.bind, cli.bind,
);
std::process::exit(2);
}
if !cli.bind.ip().is_loopback() {
eprintln!(
"mnem http: binding to non-loopback {} under MNEM_HTTP_ALLOW_NON_LOOPBACK. \
There is NO auth layer; front this with a reverse proxy that adds one.",
cli.bind
);
}
let metrics_enabled = !cli.no_metrics;
let _ = cli.metrics;
let app = mnem_http::app_with_options(
&cli.repo,
mnem_http::AppOptions {
allow_labels: None,
in_memory: cli.in_memory,
metrics_enabled,
},
)?;
let listener = tokio::net::TcpListener::bind(cli.bind).await?;
println!("mnem http listening on http://{}", cli.bind);
for (method, path, brief) in mnem_http::route_table(metrics_enabled) {
println!(" {method:<10} {path:<32} {brief}");
}
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
Ok(())
}
fn init_tracing() {
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "mnem_http=info,tower_http=warn".into());
let fmt = std::env::var("MNEM_LOG_FORMAT")
.unwrap_or_else(|_| "text".to_string())
.to_ascii_lowercase();
match fmt.as_str() {
"json" => {
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.json()
.with_current_span(true)
.with_span_list(true)
.init();
}
"text" => {
tracing_subscriber::fmt().with_env_filter(env_filter).init();
}
other => {
eprintln!(
"mnem http: unrecognised MNEM_LOG_FORMAT={other:?}; falling back to `text`. Valid values: text | json."
);
tracing_subscriber::fmt().with_env_filter(env_filter).init();
}
}
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("install Ctrl-C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => {},
() = terminate => {},
}
println!("mnem http: shutdown signal; draining...");
}