From e8bc15433914cde376c9d63fe1eba08689b6692b Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Sat, 21 Feb 2026 07:38:06 +0100 Subject: [PATCH] Enforce ALPN when forcing HTTP/2 over TLS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When endpoint policy is FORCE_HTTP_2, require strict ALPN negotiation on TLS sessions and fail if 'h2' is not negotiated (missing/empty/other protocol). RFC 9113 §3.2, §3.3: HTTP/2 over TLS MUST use protocol negotiation in TLS (ALPN). --- .../ClientHttpProtocolNegotiationStarter.java | 7 ++-- ...tClientHttpProtocolNegotiationStarter.java | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientHttpProtocolNegotiationStarter.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientHttpProtocolNegotiationStarter.java index 4ba1562c4..178fb9a6a 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientHttpProtocolNegotiationStarter.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientHttpProtocolNegotiationStarter.java @@ -94,8 +94,11 @@ public HttpConnectionEventHandler createHandler(final ProtocolIOSession ioSessio ioSession.registerProtocol(ApplicationProtocol.HTTP_2.id, new ClientH2UpgradeHandler(http2StreamHandlerFactory, exceptionCallback)); switch (endpointPolicy) { - case FORCE_HTTP_2: - return new ClientH2PrefaceHandler(ioSession, http2StreamHandlerFactory, false, exceptionCallback); + case FORCE_HTTP_2: { + // In forced HTTP/2 mode, require ALPN negotiation on TLS sessions. + final boolean strictAlpn = ioSession.getTlsDetails() != null; + return new ClientH2PrefaceHandler(ioSession, http2StreamHandlerFactory, strictAlpn, exceptionCallback); + } case FORCE_HTTP_1: return new ClientHttp1IOEventHandler(http1StreamHandlerFactory.create(ioSession)); default: diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttpProtocolNegotiationStarter.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttpProtocolNegotiationStarter.java index c0c6101bc..6cb911215 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttpProtocolNegotiationStarter.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttpProtocolNegotiationStarter.java @@ -40,6 +40,7 @@ import org.apache.hc.core5.http2.ssl.ApplicationProtocol; import org.apache.hc.core5.reactor.EndpointParameters; import org.apache.hc.core5.reactor.ProtocolIOSession; +import org.apache.hc.core5.reactor.ssl.TlsDetails; import org.apache.hc.core5.util.Timeout; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -116,4 +117,38 @@ void negotiateUsesProtocolNegotiator() { Assertions.assertTrue(handler instanceof HttpProtocolNegotiator); } + @Test + void forceHttp2TlsMissingAlpnFailsStrictHandshake() throws Exception { + final ClientHttpProtocolNegotiationStarter starter = new ClientHttpProtocolNegotiationStarter( + http1Factory(), + http2Factory(), + HttpVersionPolicy.FORCE_HTTP_2, + null, + null, + null); + + final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class); + Mockito.when(ioSession.getTlsDetails()).thenReturn(new TlsDetails(null, null)); + final ClientH2PrefaceHandler handler = (ClientH2PrefaceHandler) starter.createHandler(ioSession, null); + + Assertions.assertThrows(ProtocolNegotiationException.class, () -> handler.connected(ioSession)); + } + + @Test + void forceHttp2TlsUnexpectedAlpnFailsHandshake() throws Exception { + final ClientHttpProtocolNegotiationStarter starter = new ClientHttpProtocolNegotiationStarter( + http1Factory(), + http2Factory(), + HttpVersionPolicy.FORCE_HTTP_2, + null, + null, + null); + + final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class); + Mockito.when(ioSession.getTlsDetails()).thenReturn(new TlsDetails(null, ApplicationProtocol.HTTP_1_1.id)); + final ClientH2PrefaceHandler handler = (ClientH2PrefaceHandler) starter.createHandler(ioSession, null); + + Assertions.assertThrows(ProtocolNegotiationException.class, () -> handler.connected(ioSession)); + } + }