#![allow(
clippy::redundant_closure_for_method_calls,
clippy::ignored_unit_patterns,
clippy::map_unwrap_or
)]
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use hyper::body::Incoming;
use hyper::service::service_fn;
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder;
use tokio::net::TcpListener;
use csaf_core::config::AppConfig;
use csaf_core::storage::CsafStorage;
use csaf_models::db::DbPool;
use csaf_crud::api;
use csaf_crud::app_state::AppState;
use csaf_crud::router::{Router, SharedRouter, handler_fn};
use csaf_crud::routes;
use csaf_crud::static_files;
fn generate_self_signed_cert() -> anyhow::Result<(
Vec<rustls::pki_types::CertificateDer<'static>>,
rustls::pki_types::PrivateKeyDer<'static>,
)> {
use rcgen::{CertificateParams, SanType};
use std::net::{Ipv4Addr, Ipv6Addr};
let mut params = CertificateParams::new(vec!["localhost".to_owned()])?;
params.subject_alt_names = vec![
SanType::DnsName("localhost".try_into()?),
SanType::IpAddress(Ipv4Addr::LOCALHOST.into()),
SanType::IpAddress(Ipv6Addr::LOCALHOST.into()),
];
let now = time::OffsetDateTime::now_utc();
params.not_before = now;
params.not_after = now + time::Duration::days(45);
let key_pair = rcgen::KeyPair::generate()?;
let cert = params.self_signed(&key_pair)?;
let cert_der = rustls::pki_types::CertificateDer::from(cert);
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
.map_err(|e| anyhow::anyhow!("Failed to parse private key: {e}"))?;
Ok((vec![cert_der], key_der))
}
fn build_tls13_server_config(
cert_chain: Vec<rustls::pki_types::CertificateDer<'static>>,
key_der: rustls::pki_types::PrivateKeyDer<'static>,
alpn: Vec<Vec<u8>>,
) -> anyhow::Result<rustls::ServerConfig> {
let mut config =
rustls::ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
.with_no_client_auth()
.with_single_cert(cert_chain, key_der)?;
config.alpn_protocols = alpn;
Ok(config)
}
#[cfg(feature = "quic")]
async fn run_quic_server(
addr: SocketAddr,
router: SharedRouter,
cert_chain: Vec<rustls::pki_types::CertificateDer<'static>>,
key_der: rustls::pki_types::PrivateKeyDer<'static>,
) -> anyhow::Result<()> {
let mut tls_config = build_tls13_server_config(cert_chain, key_der, vec![b"h3".to_vec()])?;
tls_config.max_early_data_size = u32::MAX;
let server_config = quinn::ServerConfig::with_crypto(Arc::new(
quinn::crypto::rustls::QuicServerConfig::try_from(tls_config)?,
));
let endpoint = quinn::Endpoint::server(server_config, addr)?;
tracing::info!(%addr, "QUIC/HTTP3 endpoint listening (TLS 1.3)");
while let Some(incoming) = endpoint.accept().await {
let router = router.clone();
tokio::spawn(async move {
if let Err(e) = handle_quic_connection(incoming, router).await {
tracing::debug!(error = %e, "QUIC connection error");
}
});
}
Ok(())
}
#[cfg(feature = "quic")]
async fn handle_quic_connection(
incoming: quinn::Incoming,
router: SharedRouter,
) -> anyhow::Result<()> {
let connection = incoming.await?;
let mut h3_conn = h3::server::Connection::new(h3_quinn::Connection::new(connection)).await?;
loop {
match h3_conn.accept().await {
Ok(Some(resolver)) => {
let (req, mut stream) = resolver.resolve_request().await?;
let router = router.clone();
tokio::spawn(async move {
let (parts, _) = req.into_parts();
let response = router.handle_parts(parts).await;
let resp_headers = http::Response::builder()
.status(response.status())
.body(())
.unwrap_or_default();
if let Err(e) = stream.send_response(resp_headers).await {
tracing::debug!(error = %e, "Failed to send h3 response headers");
return;
}
let body_bytes = http_body_util::BodyExt::collect(response.into_body())
.await
.map(|b| b.to_bytes())
.unwrap_or_default();
if let Err(e) = stream.send_data(body_bytes).await {
tracing::debug!(error = %e, "Failed to send h3 response body");
}
let _ = stream.finish().await;
});
},
Ok(None) => break,
Err(e) => {
tracing::debug!(error = %e, "H3 accept error");
break;
},
}
}
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
if rustls::crypto::ring::default_provider()
.install_default()
.is_err()
{
return Err(anyhow::anyhow!(
"rustls crypto provider was already installed"
));
}
let config_path = std::env::args()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("config/default.toml"));
let config = AppConfig::load(&config_path)?;
tokio::fs::create_dir_all(&config.data_dir).await.ok();
let csaf_storage = CsafStorage::open(&config.redb_path())?;
let db_pool = DbPool::open(&config.sqlite_path())
.map_err(|e| anyhow::anyhow!("SQLite open failed: {e}"))?;
let settings = csaf_storage.get_settings().unwrap_or_default();
let state = AppState::new(csaf_storage, db_pool, config.clone(), settings);
let shared_router = build_router(state)?.into_shared();
let (certs, key) = generate_self_signed_cert()?;
let tcp_addr: SocketAddr = config.listen_address().parse()?;
let tcp_tls_config = build_tls13_server_config(
certs.clone(),
key.clone_key(),
vec![b"h2".to_vec(), b"http/1.1".to_vec()],
)?;
let tcp_tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(tcp_tls_config));
let listener = TcpListener::bind(tcp_addr).await?;
tracing::info!(%tcp_addr, "Listening (TLS 1.3 -- HTTP/1.1 + HTTP/2 over TCP)");
#[cfg(feature = "quic")]
{
let quic_port = config.listen_port + 1;
let quic_addr: SocketAddr = format!("{}:{quic_port}", config.listen_addr).parse()?;
let quic_router = shared_router.clone();
let quic_certs = certs;
let quic_key = key;
tokio::spawn(async move {
if let Err(e) = run_quic_server(quic_addr, quic_router, quic_certs, quic_key).await {
tracing::error!(error = %e, "QUIC server failed");
}
});
tracing::info!(%quic_addr, "QUIC/HTTP3 listener spawned (UDP, TLS 1.3)");
}
loop {
let (stream, remote_addr) = match listener.accept().await {
Ok(pair) => pair,
Err(e) => {
tracing::warn!(error = %e, "TCP accept error");
continue;
},
};
let router = shared_router.clone();
let acceptor = tcp_tls_acceptor.clone();
tokio::spawn(async move {
let tls_stream = match acceptor.accept(stream).await {
Ok(s) => s,
Err(e) => {
tracing::debug!(error = %e, %remote_addr, "TLS handshake failed");
return;
},
};
let io = TokioIo::new(tls_stream);
let service = service_fn(move |req: http::Request<Incoming>| {
let router = router.clone();
async move { Ok::<_, std::convert::Infallible>(router.handle(req).await) }
});
if let Err(e) = Builder::new(TokioExecutor::new())
.serve_connection(io, service)
.await
{
tracing::debug!(error = %e, %remote_addr, "Connection error");
}
});
}
}
fn build_router(state: AppState) -> Result<Router, matchit::InsertError> {
Router::new(state)
.get("/static/{*path}", handler_fn(static_files::serve_static))?
.get("/", handler_fn(routes::home::dashboard))?
.get("/csaf", handler_fn(routes::csaf::list))?
.get("/csaf/new", handler_fn(routes::csaf::new_form))?
.post("/csaf", handler_fn(routes::csaf::create))?
.get("/csaf/{id}", handler_fn(routes::csaf::view))?
.get("/csaf/{id}/edit", handler_fn(routes::csaf::edit_form))?
.post("/csaf/{id}/update", handler_fn(routes::csaf::update))?
.post("/csaf/{id}/delete", handler_fn(routes::csaf::delete))?
.get("/csaf/{id}/json", handler_fn(routes::csaf::json_download))?
.get("/admin/import", handler_fn(routes::admin::import_page))?
.post("/admin/import", handler_fn(routes::admin::import_execute))?
.get("/admin/export", handler_fn(routes::admin::export_page))?
.post("/admin/export", handler_fn(routes::admin::export_execute))?
.post("/admin/dump", handler_fn(routes::admin::dump_execute))?
.get("/settings", handler_fn(routes::settings::settings_form))?
.post("/settings", handler_fn(routes::settings::settings_update))?
.post(
"/settings/toggle-theme",
handler_fn(routes::settings::toggle_theme),
)?
.get("/info/about", handler_fn(routes::info::about))?
.get("/info/license", handler_fn(routes::info::license))?
.get("/info/system", handler_fn(routes::info::system_info))?
.get("/info/privacy", handler_fn(routes::info::privacy))?
.get("/info/security", handler_fn(routes::info::security))?
.get("/api/v1", handler_fn(api::v1::csaf::api_root))?
.get("/api/v1/csaf", handler_fn(api::v1::csaf::list_csaf))?
.post("/api/v1/csaf", handler_fn(api::v1::csaf::create_csaf))?
.get("/api/v1/csaf/{id}", handler_fn(api::v1::csaf::get_csaf))?
.put("/api/v1/csaf/{id}", handler_fn(api::v1::csaf::update_csaf))?
.delete("/api/v1/csaf/{id}", handler_fn(api::v1::csaf::delete_csaf))?
.get(
"/api/v1/csaf/{id}/validate",
handler_fn(api::v1::csaf::validate_csaf),
)?
.get(
"/api/v1/audit-log",
handler_fn(api::v1::audit_log::list_audit_log),
)?
.get(
"/api/v1/settings",
handler_fn(api::v1::settings::get_settings),
)?
.put(
"/api/v1/settings",
handler_fn(api::v1::settings::update_settings),
)?
.get(
"/api/v1/system/info",
handler_fn(api::v1::system::system_info),
)?
.get(
"/api/v1/system/health",
handler_fn(api::v1::system::health_check),
)
}