From 540d2f39d11342813abcd095f6e3fdf8d0ca4309 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 13 Jul 2026 21:10:26 +0000 Subject: [PATCH] jsonrpc: optional TLS termination (HTTP/2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [jsonrpc].tls = { cert, key } makes the endpoint terminate TLS itself, which also enables HTTP/2 (ALPN), so traffic is encrypted right up to fedserv — for when it runs on a different host from the site, or the browser's TLS should reach it directly. Absent = plain HTTP for a reverse-proxy front. Reuses the same cert/key shape as [grpc]; a crypto provider is pinned before building the config (the tree carries more than one). Verified the TLS config builds against a real cert without a provider panic. --- Cargo.lock | 42 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/config.rs | 4 ++++ src/jsonrpc.rs | 35 ++++++++++++++++++++++++++++------- 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1585cba..9595812 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + [[package]] name = "argon2" version = "0.5.3" @@ -152,6 +161,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-server" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9" +dependencies = [ + "arc-swap", + "bytes", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "base64" version = "0.22.1" @@ -292,6 +323,7 @@ dependencies = [ "anyhow", "argon2", "axum", + "axum-server", "base64", "fedserv-api", "fedserv-botserv", @@ -400,6 +432,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs_extra" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index f9b883f..ac869f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ tokio-rustls = "0.26.4" rustls-pemfile = "2.2.0" tonic = { version = "0.12", features = ["tls"] } axum = "0.7" +axum-server = { version = "0.7", features = ["tls-rustls"] } prost = "0.13" tokio-stream = "0.1" diff --git a/src/config.rs b/src/config.rs index 34b2483..68d38f7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -105,6 +105,10 @@ pub struct JsonRpc { // ["https://tchatou.fr", "https://swaygo.fr"]. Empty = no browser access. #[serde(default)] pub origins: Vec, + // Terminate TLS here (enabling HTTP/2). Absent = plain HTTP, for when a + // reverse proxy does TLS. Reuses the same cert/key shape as [grpc]. + #[serde(default)] + pub tls: Option, } #[derive(Debug, Deserialize, Clone)] diff --git a/src/jsonrpc.rs b/src/jsonrpc.rs index 95c5488..03938ba 100644 --- a/src/jsonrpc.rs +++ b/src/jsonrpc.rs @@ -38,18 +38,39 @@ pub async fn run(engine: Shared, cfg: JsonRpcCfg) { if cfg.token.is_empty() { return tracing::error!("jsonrpc token is empty; refusing to start an unauthenticated stats endpoint"); } + let tls = cfg.tls.clone(); let state = AppState { engine, token: cfg.token, origins: Arc::new(cfg.origins) }; let app = Router::new() .route("/", post(handle).options(preflight)) .layer(DefaultBodyLimit::max(64 * 1024)) .with_state(state); - let listener = match tokio::net::TcpListener::bind(addr).await { - Ok(l) => l, - Err(e) => return tracing::error!(%e, %addr, "jsonrpc bind failed"), - }; - tracing::info!(%addr, "jsonrpc stats API listening"); - if let Err(e) = axum::serve(listener, app).await { - tracing::error!(%e, "jsonrpc server exited"); + + match tls { + // TLS terminated here → HTTP/2 (ALPN) plus HTTP/1.1, encrypted to fedserv. + Some(t) => { + // The dependency tree carries more than one rustls crypto provider, so + // pin one before building any TLS config (ignored if already set). + let _ = tokio_rustls::rustls::crypto::aws_lc_rs::default_provider().install_default(); + let config = match axum_server::tls_rustls::RustlsConfig::from_pem_file(&t.cert, &t.key).await { + Ok(c) => c, + Err(e) => return tracing::error!(%e, "jsonrpc TLS cert/key unreadable"), + }; + tracing::info!(%addr, "jsonrpc stats API listening (TLS, HTTP/2)"); + if let Err(e) = axum_server::bind_rustls(addr, config).serve(app.into_make_service()).await { + tracing::error!(%e, "jsonrpc server exited"); + } + } + // Plain HTTP, for when a reverse proxy terminates TLS/HTTP-2/HTTP-3. + None => { + let listener = match tokio::net::TcpListener::bind(addr).await { + Ok(l) => l, + Err(e) => return tracing::error!(%e, %addr, "jsonrpc bind failed"), + }; + tracing::info!(%addr, "jsonrpc stats API listening"); + if let Err(e) = axum::serve(listener, app).await { + tracing::error!(%e, "jsonrpc server exited"); + } + } } }