feat(core): add TLS/mTLS support with multi-listener and SNI
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
524bdeb28b
commit
9f6808e90c
@@ -0,0 +1,304 @@
|
||||
package dev.relism.flash;
|
||||
|
||||
import dev.relism.flash.extension.FlashApp;
|
||||
import dev.relism.flash.extension.FlashConfiguration;
|
||||
import dev.relism.flash.tls.ClientAuth;
|
||||
import dev.relism.flash.tls.TestKeystores;
|
||||
import dev.relism.flash.tls.TlsConfig;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SNIHostName;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509ExtendedKeyManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyStore;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* End-to-end TLS coverage over real sockets — same style as {@link HttpServerTest}, just with
|
||||
* an {@link SSLSocketFactory} client instead of a raw one. Certificates are generated per test
|
||||
* via {@link TestKeystores} (JDK {@code keytool}, no fixture files, no extra crypto dependency).
|
||||
*/
|
||||
class HttpServerTlsTest {
|
||||
|
||||
private static final int SOCKET_TIMEOUT_MS = 5000;
|
||||
|
||||
private FlashApp app;
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (app != null) app.stop();
|
||||
}
|
||||
|
||||
private static int freePort() throws IOException {
|
||||
try (ServerSocket s = new ServerSocket(0)) { return s.getLocalPort(); }
|
||||
}
|
||||
|
||||
private static String httpGet(SSLSocket socket, String path) throws IOException {
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||
socket.startHandshake();
|
||||
OutputStream out = socket.getOutputStream();
|
||||
out.write(("GET " + path + " HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
out.flush();
|
||||
return new String(socket.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
// ── Basic HTTPS ──────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void httpsRequest_servedOverModernTls(@TempDir java.nio.file.Path dir) throws Exception {
|
||||
var ks = TestKeystores.build(dir, "id.p12", "changeit", TestKeystores.Entry.of("srv", "localhost"));
|
||||
int port = freePort();
|
||||
app = FlashApp.create(FlashConfiguration.builder()
|
||||
.port(port).host("127.0.0.1")
|
||||
.tls(TlsConfig.keystore(ks, "changeit"))
|
||||
.build());
|
||||
app.get("/ping", (req, res) -> "pong");
|
||||
app.start();
|
||||
|
||||
SSLSocketFactory factory = TestKeystores.trustAllClientContext().getSocketFactory();
|
||||
try (SSLSocket socket = (SSLSocket) factory.createSocket("127.0.0.1", port)) {
|
||||
String response = httpGet(socket, "/ping");
|
||||
assertTrue(response.startsWith("HTTP/1.1 200 OK"));
|
||||
assertTrue(response.endsWith("pong"));
|
||||
assertTrue(java.util.List.of("TLSv1.2", "TLSv1.3").contains(socket.getSession().getProtocol()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── SNI: one keystore, two domains, two certificates ────────────────────
|
||||
|
||||
@Test
|
||||
void sni_servesCertificateMatchingRequestedHostname(@TempDir java.nio.file.Path dir) throws Exception {
|
||||
var ks = TestKeystores.build(dir, "sni.p12", "changeit",
|
||||
TestKeystores.Entry.of("a", "a.test"),
|
||||
TestKeystores.Entry.of("b", "b.test"));
|
||||
int port = freePort();
|
||||
app = FlashApp.create(FlashConfiguration.builder()
|
||||
.port(port).host("127.0.0.1")
|
||||
.tls(TlsConfig.keystore(ks, "changeit"))
|
||||
.build());
|
||||
app.get("/ping", (req, res) -> "pong");
|
||||
app.start();
|
||||
|
||||
assertEquals("a.test", peerCn(port, "a.test"));
|
||||
assertEquals("b.test", peerCn(port, "b.test"));
|
||||
// No/unknown SNI falls back to the first keystore entry ("a") — same convention as
|
||||
// nginx/HAProxy's default_server.
|
||||
assertEquals("a.test", peerCn(port, null));
|
||||
}
|
||||
|
||||
private static String peerCn(int port, String sniHostname) throws Exception {
|
||||
SSLContext ctx = TestKeystores.trustAllClientContext();
|
||||
try (SSLSocket socket = (SSLSocket) ctx.getSocketFactory().createSocket("127.0.0.1", port)) {
|
||||
if (sniHostname != null) {
|
||||
SSLParameters params = socket.getSSLParameters();
|
||||
params.setServerNames(java.util.List.of(new SNIHostName(sniHostname)));
|
||||
socket.setSSLParameters(params);
|
||||
}
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||
socket.startHandshake();
|
||||
X509Certificate cert = (X509Certificate) socket.getSession().getPeerCertificates()[0];
|
||||
String dn = cert.getSubjectX500Principal().getName();
|
||||
for (String part : dn.split(",")) {
|
||||
part = part.trim();
|
||||
if (part.regionMatches(true, 0, "CN=", 0, 3)) return part.substring(3);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── mTLS ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void mTls_requireRejectsClientWithNoCertificate(@TempDir java.nio.file.Path dir) throws Exception {
|
||||
var ks = TestKeystores.build(dir, "id.p12", "changeit", TestKeystores.Entry.of("srv", "localhost"));
|
||||
int port = freePort();
|
||||
app = FlashApp.create(FlashConfiguration.builder()
|
||||
.port(port).host("127.0.0.1")
|
||||
.tls(TlsConfig.keystore(ks, "changeit").clientAuth(ClientAuth.REQUIRE))
|
||||
.build());
|
||||
app.get("/ping", (req, res) -> "pong");
|
||||
app.start();
|
||||
|
||||
SSLSocketFactory factory = TestKeystores.trustAllClientContext().getSocketFactory();
|
||||
try (SSLSocket socket = (SSLSocket) factory.createSocket("127.0.0.1", port)) {
|
||||
// TLS 1.3 validates the (here: empty) client certificate chain only after the
|
||||
// client's Finished message — startHandshake() alone can return cleanly from the
|
||||
// client's point of view. The server's fatal alert only surfaces on the next I/O,
|
||||
// so the round trip (not the handshake call itself) is what must throw.
|
||||
assertThrows(IOException.class, () -> httpGet(socket, "/ping"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void mTls_requireAcceptsClientWithTrustedCertificate(@TempDir java.nio.file.Path dir) throws Exception {
|
||||
var serverKs = TestKeystores.build(dir, "server.p12", "changeit", TestKeystores.Entry.of("srv", "localhost"));
|
||||
var clientKs = TestKeystores.build(dir, "client.p12", "changeit", TestKeystores.Entry.of("cli", "test-client"));
|
||||
|
||||
// Escape hatch: mutual trust needs a trust store on both sides, which the keystore()
|
||||
// convenience path deliberately doesn't expose (see TlsConfig javadoc) — this is
|
||||
// exactly the case ofContext() exists for.
|
||||
KeyStore serverIdentity = load(serverKs, "changeit");
|
||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(serverIdentity, "changeit".toCharArray());
|
||||
|
||||
KeyStore clientTrust = load(clientKs, "changeit"); // client's own cert acts as its trust anchor here
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(clientTrust);
|
||||
|
||||
SSLContext serverCtx = SSLContext.getInstance("TLS");
|
||||
serverCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
|
||||
|
||||
int port = freePort();
|
||||
app = FlashApp.create(FlashConfiguration.builder()
|
||||
.port(port).host("127.0.0.1")
|
||||
.tls(TlsConfig.ofContext(serverCtx).clientAuth(ClientAuth.REQUIRE))
|
||||
.build());
|
||||
app.get("/ping", (req, res) -> "pong");
|
||||
app.start();
|
||||
|
||||
KeyStore clientIdentity = load(clientKs, "changeit");
|
||||
KeyManagerFactory clientKmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
clientKmf.init(clientIdentity, "changeit".toCharArray());
|
||||
SSLContext clientCtx = SSLContext.getInstance("TLS");
|
||||
clientCtx.init(clientKmf.getKeyManagers(), new javax.net.ssl.TrustManager[] { trustAll() }, null);
|
||||
|
||||
try (SSLSocket socket = (SSLSocket) clientCtx.getSocketFactory().createSocket("127.0.0.1", port)) {
|
||||
String response = httpGet(socket, "/ping");
|
||||
assertTrue(response.startsWith("HTTP/1.1 200 OK"));
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyStore load(java.nio.file.Path path, String password) throws Exception {
|
||||
KeyStore store = KeyStore.getInstance("PKCS12");
|
||||
try (InputStream in = java.nio.file.Files.newInputStream(path)) { store.load(in, password.toCharArray()); }
|
||||
return store;
|
||||
}
|
||||
|
||||
private static javax.net.ssl.X509TrustManager trustAll() {
|
||||
return new javax.net.ssl.X509TrustManager() {
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
|
||||
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
|
||||
};
|
||||
}
|
||||
|
||||
// ── Multiple listeners, one app ──────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void multipleListeners_plainAndTlsServeTheSameApp(@TempDir java.nio.file.Path dir) throws Exception {
|
||||
var ks = TestKeystores.build(dir, "id.p12", "changeit", TestKeystores.Entry.of("srv", "localhost"));
|
||||
int plainPort = freePort();
|
||||
int tlsPort = freePort();
|
||||
app = FlashApp.create(FlashConfiguration.builder()
|
||||
.listener(new FlashConfiguration.Listener(plainPort, "127.0.0.1", null))
|
||||
.listener(new FlashConfiguration.Listener(tlsPort, "127.0.0.1", TlsConfig.keystore(ks, "changeit")))
|
||||
.build());
|
||||
app.get("/ping", (req, res) -> "pong");
|
||||
app.start();
|
||||
|
||||
try (Socket socket = new Socket("127.0.0.1", plainPort)) {
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||
socket.getOutputStream().write("GET /ping HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
String response = new String(socket.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertTrue(response.startsWith("HTTP/1.1 200 OK"));
|
||||
}
|
||||
|
||||
SSLSocketFactory factory = TestKeystores.trustAllClientContext().getSocketFactory();
|
||||
try (SSLSocket socket = (SSLSocket) factory.createSocket("127.0.0.1", tlsPort)) {
|
||||
String response = httpGet(socket, "/ping");
|
||||
assertTrue(response.startsWith("HTTP/1.1 200 OK"));
|
||||
}
|
||||
}
|
||||
|
||||
// ── KeyManager failure isolation ──────────────────────────────────────────
|
||||
|
||||
/** Throws on its first invocation (any keyType), then delegates normally — simulates a
|
||||
* one-off failure in a custom KeyManager (e.g. a failed DB lookup or on-demand cert
|
||||
* issuance) without permanently breaking the listener. */
|
||||
private static final class FlakyKeyManager extends X509ExtendedKeyManager {
|
||||
private final X509ExtendedKeyManager delegate;
|
||||
private final AtomicInteger calls = new AtomicInteger();
|
||||
|
||||
FlakyKeyManager(X509ExtendedKeyManager delegate) { this.delegate = delegate; }
|
||||
|
||||
private void maybeFail() {
|
||||
if (calls.getAndIncrement() == 0) throw new RuntimeException("simulated KeyManager failure");
|
||||
}
|
||||
|
||||
@Override public String chooseEngineServerAlias(String keyType, Principal[] issuers, javax.net.ssl.SSLEngine engine) {
|
||||
maybeFail();
|
||||
return delegate.chooseEngineServerAlias(keyType, issuers, engine);
|
||||
}
|
||||
@Override public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
|
||||
maybeFail();
|
||||
return delegate.chooseServerAlias(keyType, issuers, socket);
|
||||
}
|
||||
@Override public String[] getClientAliases(String keyType, Principal[] issuers) { return delegate.getClientAliases(keyType, issuers); }
|
||||
@Override public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { return delegate.chooseClientAlias(keyType, issuers, socket); }
|
||||
@Override public String[] getServerAliases(String keyType, Principal[] issuers) { return delegate.getServerAliases(keyType, issuers); }
|
||||
@Override public X509Certificate[] getCertificateChain(String alias) { return delegate.getCertificateChain(alias); }
|
||||
@Override public PrivateKey getPrivateKey(String alias) { return delegate.getPrivateKey(alias); }
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyManagerFailure_isolatedToOneConnection_listenerServesTheNext(@TempDir java.nio.file.Path dir) throws Exception {
|
||||
var ks = TestKeystores.build(dir, "id.p12", "changeit", TestKeystores.Entry.of("srv", "localhost"));
|
||||
KeyStore store = load(ks, "changeit");
|
||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(store, "changeit".toCharArray());
|
||||
|
||||
KeyManager[] managers = kmf.getKeyManagers();
|
||||
for (int i = 0; i < managers.length; i++) {
|
||||
if (managers[i] instanceof X509ExtendedKeyManager x509) managers[i] = new FlakyKeyManager(x509);
|
||||
}
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(managers, null, null);
|
||||
|
||||
int port = freePort();
|
||||
app = FlashApp.create(FlashConfiguration.builder()
|
||||
.port(port).host("127.0.0.1")
|
||||
.tls(TlsConfig.ofContext(ctx))
|
||||
.build());
|
||||
app.get("/ping", (req, res) -> "pong");
|
||||
app.start();
|
||||
|
||||
SSLSocketFactory factory = TestKeystores.trustAllClientContext().getSocketFactory();
|
||||
|
||||
// Connection 1: the KeyManager throws — handshake must fail, but must not take the
|
||||
// listener down with it.
|
||||
try (SSLSocket socket = (SSLSocket) factory.createSocket("127.0.0.1", port)) {
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||
assertThrows(IOException.class, socket::startHandshake);
|
||||
}
|
||||
|
||||
// Connection 2: same listener, no reconfiguration — must succeed and serve normally.
|
||||
try (SSLSocket socket = (SSLSocket) factory.createSocket("127.0.0.1", port)) {
|
||||
String response = httpGet(socket, "/ping");
|
||||
assertTrue(response.startsWith("HTTP/1.1 200 OK"));
|
||||
assertTrue(response.endsWith("pong"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package dev.relism.flash.tls;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Test-only: builds self-signed PKCS12 keystores via the JDK's own {@code keytool} so TLS
|
||||
* tests need no external certificate fixtures and no extra crypto test-dependency.
|
||||
*/
|
||||
public final class TestKeystores {
|
||||
private TestKeystores() {}
|
||||
|
||||
public record Entry(String alias, String cn, String... sans) {
|
||||
public static Entry of(String alias, String cn, String... sans) { return new Entry(alias, cn, sans); }
|
||||
}
|
||||
|
||||
public static Path build(Path dir, String fileName, String password, Entry... entries)
|
||||
throws IOException, InterruptedException {
|
||||
Path keystore = dir.resolve(fileName);
|
||||
String keytool = Path.of(System.getProperty("java.home"), "bin", "keytool").toString();
|
||||
for (Entry e : entries) {
|
||||
List<String> cmd = new ArrayList<>(List.of(
|
||||
keytool, "-genkeypair", "-noprompt",
|
||||
"-alias", e.alias(),
|
||||
"-keyalg", "RSA", "-keysize", "2048",
|
||||
"-validity", "3650",
|
||||
"-keystore", keystore.toString(),
|
||||
"-storetype", "PKCS12",
|
||||
"-storepass", password,
|
||||
"-dname", "CN=" + e.cn()));
|
||||
if (e.sans().length > 0) {
|
||||
StringBuilder san = new StringBuilder();
|
||||
for (String s : e.sans()) {
|
||||
if (!san.isEmpty()) san.append(',');
|
||||
san.append("dns:").append(s);
|
||||
}
|
||||
cmd.add("-ext");
|
||||
cmd.add("SAN=" + san);
|
||||
}
|
||||
Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
|
||||
String output = new String(p.getInputStream().readAllBytes());
|
||||
if (p.waitFor() != 0) throw new IOException("keytool failed: " + output);
|
||||
}
|
||||
return keystore;
|
||||
}
|
||||
|
||||
/** A client-side {@link SSLContext} that trusts any server certificate — self-signed test certs only. */
|
||||
public static SSLContext trustAllClientContext() throws NoSuchAlgorithmException, KeyManagementException {
|
||||
TrustManager trustAll = new X509TrustManager() {
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
|
||||
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
|
||||
};
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(null, new TrustManager[] { trustAll }, null);
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package dev.relism.flash.tls;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLServerSocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TlsConfigTest {
|
||||
|
||||
private static SSLServerSocket unboundSocket(TlsConfig tls) throws IOException {
|
||||
return (SSLServerSocket) tls.serverSocketFactory().createServerSocket();
|
||||
}
|
||||
|
||||
@Test
|
||||
void keystore_pinsProtocolsToTls12And13(@TempDir Path dir) throws Exception {
|
||||
Path ks = TestKeystores.build(dir, "id.p12", "changeit",
|
||||
TestKeystores.Entry.of("only", "single.test"));
|
||||
TlsConfig tls = TlsConfig.keystore(ks, "changeit");
|
||||
|
||||
try (SSLServerSocket socket = unboundSocket(tls)) {
|
||||
tls.applyTo(socket);
|
||||
List<String> protocols = Arrays.asList(socket.getSSLParameters().getProtocols());
|
||||
assertTrue(protocols.contains("TLSv1.2"));
|
||||
assertTrue(protocols.contains("TLSv1.3"));
|
||||
assertFalse(protocols.contains("SSLv3"));
|
||||
assertFalse(protocols.contains("TLSv1"));
|
||||
assertFalse(protocols.contains("TLSv1.1"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofContext_appliesNoParameterOverlay() throws Exception {
|
||||
SSLContext ctx = TestKeystores.trustAllClientContext(); // any valid SSLContext will do here
|
||||
TlsConfig tls = TlsConfig.ofContext(ctx);
|
||||
|
||||
try (SSLServerSocket socket = unboundSocket(tls)) {
|
||||
SSLParameters before = socket.getSSLParameters();
|
||||
String[] protocolsBefore = before.getProtocols();
|
||||
|
||||
tls.applyTo(socket);
|
||||
|
||||
assertArrayEquals(protocolsBefore, socket.getSSLParameters().getProtocols(),
|
||||
"ofContext must not narrow/override protocols set on the caller's SSLContext");
|
||||
assertFalse(socket.getNeedClientAuth());
|
||||
assertFalse(socket.getWantClientAuth());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofContext_preservesCallerConfiguredAlpnAndProtocols() throws Exception {
|
||||
// Mirrors the real use case this escape hatch exists for (TLS-ALPN-01 / RFC 8737):
|
||||
// the caller sets its own ALPN protocol list — and, to make the point unambiguous,
|
||||
// a protocol list *narrower* than what Flash's own keystore() path would pin — directly
|
||||
// on the socket. applyTo() must not touch either. There is no SSLContext#setDefault-
|
||||
// SSLParameters in the public JSSE API, so this per-socket SSLParameters is the only
|
||||
// place such configuration can live; this test is the contract that makes it safe to
|
||||
// rely on.
|
||||
SSLContext ctx = TestKeystores.trustAllClientContext();
|
||||
TlsConfig tls = TlsConfig.ofContext(ctx).clientAuth(ClientAuth.OPTIONAL);
|
||||
|
||||
try (SSLServerSocket socket = unboundSocket(tls)) {
|
||||
SSLParameters custom = socket.getSSLParameters();
|
||||
custom.setApplicationProtocols(new String[] { "acme-tls/1", "http/1.1" });
|
||||
custom.setProtocols(new String[] { "TLSv1.3" });
|
||||
socket.setSSLParameters(custom);
|
||||
|
||||
tls.applyTo(socket);
|
||||
|
||||
SSLParameters after = socket.getSSLParameters();
|
||||
assertArrayEquals(new String[] { "acme-tls/1", "http/1.1" }, after.getApplicationProtocols(),
|
||||
"ofContext must not touch ALPN protocols the caller configured on its own socket");
|
||||
assertArrayEquals(new String[] { "TLSv1.3" }, after.getProtocols(),
|
||||
"ofContext must not widen/override the caller's own protocol list");
|
||||
// clientAuth still applies — it is the caller's own explicit instruction through
|
||||
// this API, not a Flash-imposed default. See TlsConfig's class Javadoc.
|
||||
assertTrue(socket.getWantClientAuth());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientAuth_none_makesNoClientAuthCall() throws Exception {
|
||||
SSLContext ctx = TestKeystores.trustAllClientContext();
|
||||
TlsConfig tls = TlsConfig.ofContext(ctx);
|
||||
|
||||
try (SSLServerSocket socket = unboundSocket(tls)) {
|
||||
tls.applyTo(socket);
|
||||
assertFalse(socket.getNeedClientAuth());
|
||||
assertFalse(socket.getWantClientAuth());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientAuth_require_setsNeedClientAuth() throws Exception {
|
||||
SSLContext ctx = TestKeystores.trustAllClientContext();
|
||||
TlsConfig tls = TlsConfig.ofContext(ctx).clientAuth(ClientAuth.REQUIRE);
|
||||
|
||||
try (SSLServerSocket socket = unboundSocket(tls)) {
|
||||
tls.applyTo(socket);
|
||||
assertTrue(socket.getNeedClientAuth());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientAuth_optional_setsWantClientAuth() throws Exception {
|
||||
SSLContext ctx = TestKeystores.trustAllClientContext();
|
||||
TlsConfig tls = TlsConfig.ofContext(ctx).clientAuth(ClientAuth.OPTIONAL);
|
||||
|
||||
try (SSLServerSocket socket = unboundSocket(tls)) {
|
||||
tls.applyTo(socket);
|
||||
assertTrue(socket.getWantClientAuth());
|
||||
assertFalse(socket.getNeedClientAuth());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user