mod daemon;
mod jsonrpc;
mod middleware;
mod routes;
mod state;
mod webhooks;
use axum::middleware as axum_mw;
use clap::Parser;
use std::net::SocketAddr;
use tokio::net::TcpStream;
use tower_http::cors::CorsLayer;
use tracing_subscriber::EnvFilter;
#[derive(Parser)]
#[command(name = "signal-cli-api", about = "REST + WebSocket API for signal-cli")]
struct Cli {
#[arg(long)]
signal_cli: Option<String>,
#[arg(long, default_value = "127.0.0.1:8080")]
listen: String,
#[arg(long)]
tls_cert: Option<String>,
#[arg(long)]
tls_key: Option<String>,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.init();
let cli = Cli::parse();
let _managed_daemon; let signal_cli_addr = match cli.signal_cli {
Some(addr) => addr,
None => {
let d = daemon::spawn().await?;
let addr = d.addr.clone();
_managed_daemon = d;
addr
}
};
tracing::info!("Connecting to signal-cli at {signal_cli_addr}");
let stream = TcpStream::connect(&signal_cli_addr).await?;
let (reader, writer) = stream.into_split();
let (writer_tx, writer_rx) = tokio::sync::mpsc::channel::<String>(256);
tokio::spawn(jsonrpc::writer_loop(writer_rx, writer));
let app_state = state::AppState::new(writer_tx);
let broadcast_tx = app_state.broadcast_tx.clone();
let pending = app_state.pending.clone();
let metrics = app_state.metrics.clone();
tokio::spawn(jsonrpc::reader_loop(reader, broadcast_tx, pending, metrics));
let webhook_state = app_state.clone();
tokio::spawn(webhooks::dispatch_loop(webhook_state));
let app = routes::router(app_state)
.layer(axum_mw::from_fn(middleware::request_tracing))
.layer(CorsLayer::permissive());
let requested: SocketAddr = cli.listen.parse()?;
match (cli.tls_cert, cli.tls_key) {
(Some(cert), Some(key)) => {
let tls_config = axum_server::tls_rustls::RustlsConfig::from_pem_file(&cert, &key).await?;
let addr = match tokio::net::TcpListener::bind(requested).await {
Ok(probe) => { drop(probe); requested }
Err(_) => {
let fallback = SocketAddr::from(([127, 0, 0, 1], 0));
let probe = tokio::net::TcpListener::bind(fallback).await?;
let addr = probe.local_addr()?;
drop(probe);
tracing::warn!("Port {} busy, using {addr} instead", requested.port());
addr
}
};
tracing::info!("Listening on https://{addr} (TLS)");
tokio::select! {
result = axum_server::bind_rustls(addr, tls_config)
.serve(app.into_make_service()) => { result?; }
_ = shutdown_signal() => {
tracing::info!("Shutdown signal received, stopping...");
}
}
}
(None, None) => {
let listener = match tokio::net::TcpListener::bind(requested).await {
Ok(l) => l,
Err(_) => {
let fallback = SocketAddr::from(([127, 0, 0, 1], 0));
let l = tokio::net::TcpListener::bind(fallback).await?;
tracing::warn!(
"Port {} busy, using {} instead",
requested.port(),
l.local_addr()?
);
l
}
};
tracing::info!("Listening on http://{}", listener.local_addr()?);
tokio::select! {
result = axum::serve(listener, app) => { result?; }
_ = shutdown_signal() => {
tracing::info!("Shutdown signal received, stopping...");
}
}
}
_ => {
anyhow::bail!("Both --tls-cert and --tls-key must be provided together");
}
}
Ok(())
}
async fn shutdown_signal() {
let ctrl_c = tokio::signal::ctrl_c();
let mut sigterm =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to register SIGTERM handler");
tokio::select! {
_ = ctrl_c => {}
_ = sigterm.recv() => {}
}
}