8371887: HttpClient: SSLParameters with no protocols configured disable HTTP2+ support

Reviewed-by: jpai, dfuchs
This commit is contained in:
Daniel Jeliński 2025-11-27 09:27:02 +00:00
parent 86aae125f1
commit 1f417e7761
7 changed files with 81 additions and 52 deletions

View File

@ -74,7 +74,7 @@ public final class QuicTLSContext {
/**
* {@return {@code true} if protocols of the given {@code parameters} support QUIC TLS, {@code false} otherwise}
*/
public static boolean isQuicCompatible(SSLParameters parameters) {
private static boolean isQuicCompatible(SSLParameters parameters) {
String[] protocols = parameters.getProtocols();
return protocols != null && Arrays.asList(protocols).contains("TLSv1.3");
}

View File

@ -53,6 +53,7 @@ import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@ -126,6 +127,8 @@ final class HttpClientImpl extends HttpClient implements Trackable {
// Defaults to value used for HTTP/1 Keep-Alive Timeout. Can be overridden by jdk.httpclient.keepalive.timeout.h2 property.
static final long IDLE_CONNECTION_TIMEOUT_H2 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT);
static final long IDLE_CONNECTION_TIMEOUT_H3 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h3", IDLE_CONNECTION_TIMEOUT_H2);
private final boolean hasRequiredH3TLS;
private final boolean hasRequiredH2TLS;
static final UseVTForSelector USE_VT_FOR_SELECTOR =
Utils.useVTForSelector("jdk.internal.httpclient.tcp.selector.useVirtualThreads", "default");
@ -477,10 +480,17 @@ final class HttpClientImpl extends HttpClient implements Trackable {
"HTTP3 is not supported"));
}
sslParams = requireNonNullElseGet(builder.sslParams, sslContext::getDefaultSSLParameters);
boolean sslParamsSupportedForH3 = sslParams.getProtocols() == null
|| sslParams.getProtocols().length == 0
|| isQuicCompatible(sslParams);
if (version == Version.HTTP_3 && !sslParamsSupportedForH3) {
String[] sslProtocols = sslParams.getProtocols();
if (sslProtocols == null) {
sslProtocols = requireNonNullElseGet(sslContext.getDefaultSSLParameters().getProtocols(),
() -> new String[0]);
}
// HTTP/3 MUST use TLS version 1.3 or higher
hasRequiredH3TLS = Arrays.asList(sslProtocols).contains("TLSv1.3");
// HTTP/2 MUST use TLS version 1.2 or higher for HTTP/2 over TLS
hasRequiredH2TLS = hasRequiredH3TLS || Arrays.asList(sslProtocols).contains("TLSv1.2");
if (version == Version.HTTP_3 && !hasRequiredH3TLS) {
throw new UncheckedIOException(new UnsupportedProtocolVersionException(
"HTTP3 is not supported - TLSv1.3 isn't configured on SSLParameters"));
}
@ -507,7 +517,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
debug.log("proxySelector is %s (user-supplied=%s)",
this.proxySelector, userProxySelector != null);
authenticator = builder.authenticator;
boolean h3Supported = sslCtxSupportedForH3 && sslParamsSupportedForH3;
boolean h3Supported = sslCtxSupportedForH3 && hasRequiredH3TLS;
registry = new AltServicesRegistry(id);
connections = new ConnectionPool(id);
client2 = new Http2ClientImpl(this);
@ -530,6 +540,22 @@ final class HttpClientImpl extends HttpClient implements Trackable {
assert facadeRef.get() != null;
}
/**
* Returns true if the SSL parameter protocols contains at
* least one TLS version that HTTP/3 requires.
*/
boolean hasRequiredHTTP3TLSVersion() {
return hasRequiredH3TLS;
}
/**
* Returns true if the SSL parameter protocols contains at
* least one TLS version that HTTP/2 requires.
*/
boolean hasRequiredHTTP2TLSVersion() {
return hasRequiredH2TLS;
}
// called when the facade is GC'ed.
// Just wakes up the selector to cleanup...
void facadeCleanup() {

View File

@ -32,7 +32,6 @@ import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.channels.NetworkChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
@ -43,8 +42,6 @@ import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpHeaders;
@ -285,23 +282,6 @@ abstract class HttpConnection implements Closeable {
*/
abstract HttpPublisher publisher();
// HTTP/2 MUST use TLS version 1.2 or higher for HTTP/2 over TLS
private static final Predicate<String> testRequiredHTTP2TLSVersion = proto ->
proto.equals("TLSv1.2") || proto.equals("TLSv1.3");
/**
* Returns true if the given client's SSL parameter protocols contains at
* least one TLS version that HTTP/2 requires.
*/
private static final boolean hasRequiredHTTP2TLSVersion(HttpClient client) {
String[] protos = client.sslParameters().getProtocols();
if (protos != null) {
return Arrays.stream(protos).filter(testRequiredHTTP2TLSVersion).findAny().isPresent();
} else {
return false;
}
}
/**
* Factory for retrieving HttpConnections. A connection can be retrieved
* from the connection pool, or a new one created if none available.
@ -359,7 +339,7 @@ abstract class HttpConnection implements Closeable {
} else {
assert !request.isHttp3Only(version); // should have failed before
String[] alpn = null;
if (version == HTTP_2 && hasRequiredHTTP2TLSVersion(client)) {
if (version == HTTP_2 && client.hasRequiredHTTP2TLSVersion()) {
// We only come here after we have checked the HTTP/2 connection pool.
// We will not negotiate HTTP/2 if we don't have the appropriate TLS version
alpn = new String[] { Alpns.H2, Alpns.HTTP_1_1 };

View File

@ -31,11 +31,9 @@ import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketOption;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpConnectTimeoutException;
import java.nio.channels.NetworkChannel;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@ -43,7 +41,6 @@ import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLParameters;
@ -81,9 +78,6 @@ abstract class HttpQuicConnection extends HttpConnection {
// the alt-service which was advertised, from some origin, for this connection co-ordinates.
// can be null, which indicates this wasn't created because of an alt-service
private final AltService sourceAltService;
// HTTP/2 MUST use TLS version 1.3 or higher for HTTP/3 over TLS
private static final Predicate<String> testRequiredHTTP3TLSVersion = proto ->
proto.equals("TLSv1.3");
HttpQuicConnection(Origin originServer, InetSocketAddress address, HttpClientImpl client,
@ -178,19 +172,6 @@ abstract class HttpQuicConnection extends HttpConnection {
return quicConnection;
}
/**
* Returns true if the given client's SSL parameter protocols contains at
* least one TLS version that HTTP/3 requires.
*/
private static boolean hasRequiredHTTP3TLSVersion(HttpClient client) {
String[] protos = client.sslParameters().getProtocols();
if (protos != null) {
return Arrays.stream(protos).anyMatch(testRequiredHTTP3TLSVersion);
} else {
return false;
}
}
/**
* Called when the HTTP/3 connection is established, either successfully or
* unsuccessfully
@ -260,7 +241,7 @@ abstract class HttpQuicConnection extends HttpConnection {
// to using HTTP/2
var debug = h3client.debug();
var where = "HttpQuicConnection.getHttpQuicConnection";
if (proxy != null || !hasRequiredHTTP3TLSVersion(client)) {
if (proxy != null || !client.hasRequiredHTTP3TLSVersion()) {
if (debug.on())
debug.log("%s: proxy required or SSL version mismatch", where);
return null;

View File

@ -31,6 +31,8 @@ import java.net.http.HttpResponse;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.test.lib.net.SimpleSSLContext;
@ -49,7 +51,7 @@ import jdk.test.lib.security.SecurityUtils;
/*
* @test
* @bug 8239594
* @bug 8239594 8371887
* @summary This test verifies that the TLS version handshake respects ssl context
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext TlsContextTest
@ -101,9 +103,25 @@ public class TlsContextTest implements HttpServerAdapters {
* Tests various scenarios between client and server tls handshake with valid http
*/
@Test(dataProvider = "scenarios")
public void testVersionProtocols(SSLContext context,
public void testVersionProtocolsNoParams(SSLContext context,
Version version,
String expectedProtocol) throws Exception {
runTest(context, version, expectedProtocol, false);
}
/**
* Tests various scenarios between client and server tls handshake with valid http,
* but with empty SSLParameters
*/
@Test(dataProvider = "scenarios")
public void testVersionProtocolsEmptyParams(SSLContext context,
Version version,
String expectedProtocol) throws Exception {
runTest(context, version, expectedProtocol, true);
}
private void runTest(SSLContext context, Version version, String expectedProtocol,
boolean setEmptyParams) throws Exception {
// for HTTP/3 we won't accept to set the version to HTTP/3 on the
// client if we don't have TLSv1.3; We will set the version
// on the request instead in that case.
@ -111,8 +129,11 @@ public class TlsContextTest implements HttpServerAdapters {
: HttpClient.newBuilder().version(version);
var reqBuilder = HttpRequest.newBuilder(new URI(https2URI));
if (setEmptyParams) {
builder.sslParameters(new SSLParameters());
}
HttpClient client = builder.sslContext(context)
.build();
.build();
if (version == HTTP_3) {
// warmup to obtain AltService
client.send(reqBuilder.version(HTTP_2).GET().build(), ofString());

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -41,7 +41,7 @@ import jdk.httpclient.test.lib.http2.Http2Handler;
/*
* @test
* @bug 8150769 8157107
* @bug 8150769 8157107 8371887
* @library /test/jdk/java/net/httpclient/lib
* @build jdk.httpclient.test.lib.http2.Http2TestServer
* @summary Checks that SSL parameters can be set for HTTP/2 connection
@ -135,6 +135,12 @@ public class TLSConnection {
success &= checkCipherSuite(handler.getSSLSession(),
"TLS_RSA_WITH_AES_128_CBC_SHA");
success &= expectSuccess(
"---\nTest #5: empty SSL parameters, "
+ "expect successful connection",
() -> connect(uriString, new SSLParameters()));
success &= checkProtocol(handler.getSSLSession(), expectedTLSVersion(null));
if (success) {
System.out.println("Test passed");
} else {

View File

@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/*
* @test
* @bug 8371887
* @summary Tests that a HttpClient configured with SSLParameters that doesn't include TLSv1.3
* cannot be used for HTTP3
* @library /test/lib /test/jdk/java/net/httpclient/lib
@ -75,4 +76,18 @@ public class H3UnsupportedSSLParametersTest {
.version(HttpClient.Version.HTTP_3).build();
assertNotNull(client, "HttpClient is null");
}
/**
* Builds a HttpClient with SSLParameters without protocol versions
* and expects the build() to succeed and return a HttpClient instance
*/
@Test
public void testDefault() throws Exception {
final SSLParameters params = new SSLParameters();
final HttpClient client = HttpServerAdapters.createClientBuilderForH3()
.proxy(HttpClient.Builder.NO_PROXY)
.sslParameters(params)
.version(HttpClient.Version.HTTP_3).build();
assertNotNull(client, "HttpClient is null");
}
}