From 9f6808e90c01cf74f48b2d5ea3698549bce3a991 Mon Sep 17 00:00:00 2001 From: Zakaria El Orche Date: Sun, 9 Aug 2026 21:41:57 +0000 Subject: [PATCH] feat(core): add TLS/mTLS support with multi-listener and SNI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flash can now serve HTTPS and WSS, on one or many listeners per app: - FlashConfiguration gains an optional `tls` field for the single default listener, and a `listeners` list for apps that bind multiple ports (each independently plain or TLS). - New dev.relism.flash.tls package: TlsConfig.keystore(path, password) builds an SSLContext from a PKCS12/JKS keystore, with SNI-based certificate selection for free when the keystore holds more than one alias (matched by SAN/CN, pure JDK APIs). TlsConfig.ofContext(sslContext) is a full escape hatch — Flash never calls setSSLParameters on that path, so caller-set protocols/cipher suites/ALPN survive untouched. TlsConfig.clientAuth(...) adds optional/required mTLS on either path. - HttpServer moves from a single ServerSocket to a list of bound listeners; the per-request hot path (RequestParser, routing, response writing) is untouched — TLS only changes which bytes come out of accept(), so WSS needs no separate code path from WS. - process()'s catch is widened to log non-IOException failures (e.g. a misbehaving custom KeyManager/TrustManager on the ofContext path) instead of swallowing them silently; the failure was already isolated to the one connection via the existing try-with-resources/executor-submission boundary — this only fixes the missing log line. - Fixes a pre-existing gap where FlashConfiguration#host was accepted but never used to bind (listeners always bound to the wildcard address). Co-Authored-By: Claude Sonnet 5 --- .../java/dev/relism/flash/HttpServer.java | 106 ++++-- .../dev/relism/flash/extension/FlashApp.java | 6 +- .../flash/extension/FlashConfiguration.java | 30 ++ .../java/dev/relism/flash/tls/ClientAuth.java | 14 + .../dev/relism/flash/tls/SniKeyManager.java | 126 ++++++++ .../java/dev/relism/flash/tls/TlsConfig.java | 107 ++++++ .../dev/relism/flash/HttpServerTlsTest.java | 304 ++++++++++++++++++ .../dev/relism/flash/tls/TestKeystores.java | 67 ++++ .../dev/relism/flash/tls/TlsConfigTest.java | 123 +++++++ 9 files changed, 855 insertions(+), 28 deletions(-) create mode 100644 flash/src/main/java/dev/relism/flash/tls/ClientAuth.java create mode 100644 flash/src/main/java/dev/relism/flash/tls/SniKeyManager.java create mode 100644 flash/src/main/java/dev/relism/flash/tls/TlsConfig.java create mode 100644 flash/src/test/java/dev/relism/flash/HttpServerTlsTest.java create mode 100644 flash/src/test/java/dev/relism/flash/tls/TestKeystores.java create mode 100644 flash/src/test/java/dev/relism/flash/tls/TlsConfigTest.java diff --git a/flash/src/main/java/dev/relism/flash/HttpServer.java b/flash/src/main/java/dev/relism/flash/HttpServer.java index c354ee5..358e06f 100644 --- a/flash/src/main/java/dev/relism/flash/HttpServer.java +++ b/flash/src/main/java/dev/relism/flash/HttpServer.java @@ -10,6 +10,7 @@ import dev.relism.flash.models.RequestHandler; import dev.relism.flash.models.Response; import dev.relism.flash.routing.AbstractRouter; import dev.relism.flash.routing.AbstractWsRouter; +import dev.relism.flash.tls.TlsConfig; import dev.relism.flash.websocket.WebSocketFrame; import dev.relism.flash.websocket.WebSocketHandler; import dev.relism.flash.websocket.WebSocketSession; @@ -17,6 +18,8 @@ import dev.relism.fpr.core.ByteView; import lombok.extern.slf4j.Slf4j; +import javax.net.ssl.SSLServerSocket; + import java.io.*; import java.net.InetSocketAddress; import java.net.ServerSocket; @@ -24,15 +27,23 @@ import java.net.Socket; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.Set; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** - * Pure I/O transport layer. Owns the {@link ServerSocket}, the virtual-thread - * executor, and the keep-alive accept loop. Routing is delegated to HTTP and WS routers. + * Pure I/O transport layer. Owns one {@link ServerSocket} per configured listener (plain or + * TLS), the virtual-thread executor, and the keep-alive accept loop. Routing is delegated to + * HTTP and WS routers — identically, regardless of which listener accepted the connection. + * + *

TLS is a transport-level concern only: once a {@link BoundListener} is bound, an accepted + * {@link Socket} is either plain or an {@code SSLSocket} indistinguishably from here on — + * {@link #process} never branches on it. This is also why WSS needs no separate code path from + * WS: the WebSocket upgrade happens over whatever transport {@link #process} was handed. * *

Allocation model (unchanged)

*