jsonrpc: optional TLS termination (HTTP/2)

[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.
This commit is contained in:
Jean Chevronnet 2026-07-13 21:10:26 +00:00
parent 92417e7a7c
commit 540d2f39d1
No known key found for this signature in database
4 changed files with 75 additions and 7 deletions

42
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -105,6 +105,10 @@ pub struct JsonRpc {
// ["https://tchatou.fr", "https://swaygo.fr"]. Empty = no browser access.
#[serde(default)]
pub origins: Vec<String>,
// 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<ServerTls>,
}
#[derive(Debug, Deserialize, Clone)]

View file

@ -38,11 +38,30 @@ 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);
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"),
@ -52,6 +71,8 @@ pub async fn run(engine: Shared, cfg: JsonRpcCfg) {
tracing::error!(%e, "jsonrpc server exited");
}
}
}
}
// Preflight (OPTIONS): tell the browser this origin may POST with an
// Authorization header. Only origins on the allowlist are answered.