use std::net::SocketAddr;
use std::sync::Arc;
use postrust_proxy::config::{load_from_database, load_from_file, ProxyConfig};
use postrust_proxy::health::HealthChecker;
use postrust_proxy::ratelimit::RateLimiter;
use postrust_proxy::vendored::ProxyService;
use sqlx::postgres::PgPoolOptions;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "postrust_proxy=info".into()),
)
.init();
let config_path = std::env::args()
.nth(1)
.or_else(|| std::env::var("POSTRUST_PROXY_CONFIG").ok())
.ok_or("usage: postrust-proxy <config.toml> (or set POSTRUST_PROXY_CONFIG)")?;
let config: ProxyConfig = load_from_file(&config_path).await?;
let h2_port = config.server.http2_port;
let http_host = config.server.http_host.clone();
let https_host = config.server.https_host.clone();
let https_port = config.server.https_port;
let cert_file = config.tls.cert_file.clone();
let key_file = config.tls.key_file.clone();
let addr: SocketAddr = format!("{}:{}", config.server.http_host, config.server.http_port)
.parse()
.map_err(|e| format!("bad listen address in {config_path}: {e}"))?;
let database_url = std::env::var("DATABASE_URL").ok();
let pool = PgPoolOptions::new().connect_lazy(
database_url
.as_deref()
.unwrap_or("postgres://postgres:postgres@127.0.0.1:5432/postgres"),
)?;
let cancel = CancellationToken::new();
#[cfg(feature = "acme")]
let cancel_for_acme = cancel.clone();
let mut config = config;
if config.server.database_config && database_url.is_some() {
let (routes, upstreams) = load_from_database(&pool).await?;
tracing::info!(
routes = routes.len(),
upstreams = upstreams.len(),
"loaded configuration from the database"
);
merge_from_database(&mut config, routes, upstreams)?;
}
let config = config;
#[cfg(feature = "acme")]
let acme_worker = if config.tls.acme_enabled {
match &database_url {
Some(_) => {
Some(start_acme_worker(&config, pool.clone(), cancel_for_acme.clone()).await?)
}
None => {
tracing::warn!(
"tls.acme_enabled is set but DATABASE_URL is not; the ACME worker needs a database for the account, challenges and certificates, so it will not start"
);
None
}
}
} else {
None
};
let rate_limiter = Arc::new(RateLimiter::new(config.rate_limit.clone()));
let health_checker = Arc::new(HealthChecker::new());
let service = Arc::new(ProxyService::new(
Arc::new(RwLock::new(config)),
health_checker,
rate_limiter,
));
service.load_config().await;
{
let cancel = cancel.clone();
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
cancel.cancel();
});
}
let h2_addr: Option<SocketAddr> = match h2_port {
Some(port) => Some(
format!("{http_host}:{port}")
.parse()
.map_err(|e| format!("bad http2 listen address in {config_path}: {e}"))?,
),
None => None,
};
let tls = match (cert_file, key_file) {
(Some(cert), Some(key)) => {
let tls_config = postrust_proxy::tls::load_server_config(&cert, &key).await?;
let https_addr: SocketAddr = format!("{https_host}:{https_port}")
.parse()
.map_err(|e| format!("bad https listen address in {config_path}: {e}"))?;
let tls_service = service.clone();
let tls_cancel = cancel.clone();
Some(tokio::spawn(async move {
tls_service
.serve_https(https_addr, tls_config, tls_cancel)
.await
}))
}
(None, None) => None,
_ => return Err("tls.cert_file and tls.key_file must be set together".into()),
};
match h2_addr {
Some(h2_addr) => {
let h2_service = service.clone();
let h2_cancel = cancel.clone();
let h2 = tokio::spawn(async move { h2_service.serve_h2c(h2_addr, h2_cancel).await });
let main = service.serve_http(addr, cancel).await;
h2.await
.map_err(|e| format!("http2 listener panicked: {e}"))??;
main?;
}
None => service.serve_http(addr, cancel).await?,
}
if let Some(tls) = tls {
tls.await
.map_err(|e| format!("https listener panicked: {e}"))??;
}
#[cfg(feature = "acme")]
if let Some(worker) = acme_worker {
worker
.await
.map_err(|e| format!("ACME worker panicked: {e}"))?;
}
Ok(())
}
#[cfg(feature = "acme")]
async fn start_acme_worker(
config: &ProxyConfig,
pool: sqlx::PgPool,
cancel: CancellationToken,
) -> Result<tokio::task::JoinHandle<()>, Box<dyn std::error::Error>> {
use postrust_proxy::tls::{
AcmeIssuer, CertificateStore, LETS_ENCRYPT_PRODUCTION, LETS_ENCRYPT_STAGING,
};
let directory = config.tls.acme_directory_url.clone().unwrap_or_else(|| {
if config.tls.acme_staging {
LETS_ENCRYPT_STAGING.to_string()
} else {
LETS_ENCRYPT_PRODUCTION.to_string()
}
});
let cert_store = Arc::new(CertificateStore::new(pool.clone(), &config.tls.cert_dir).await?);
let mut issuer = AcmeIssuer::new(
directory,
config.tls.acme_email.clone(),
pool.clone(),
cert_store,
);
if let Some(root) = &config.tls.acme_root_pem {
issuer = issuer.with_root_certificate(root);
}
Ok(tokio::spawn(postrust_proxy::saas::ssl::run(
pool,
Arc::new(issuer),
cancel,
)))
}
fn merge_from_database(
config: &mut ProxyConfig,
routes: Vec<postrust_proxy::config::Route>,
upstreams: Vec<postrust_proxy::config::Upstream>,
) -> Result<(), String> {
for upstream in &upstreams {
if config.upstreams.iter().any(|u| u.name == upstream.name) {
return Err(format!(
"upstream {:?} is declared in both the config file and the database",
upstream.name
));
}
}
for route in &routes {
if config.routes.iter().any(|r| r.name == route.name) {
return Err(format!(
"route {:?} is declared in both the config file and the database",
route.name
));
}
}
config.upstreams.extend(upstreams);
config.routes.extend(routes);
Ok(())
}