diff --git a/connectors/jnh-connector/src/main/java/org/glassfish/jersey/jnh/connector/JavaNetHttpClientProperties.java b/connectors/jnh-connector/src/main/java/org/glassfish/jersey/jnh/connector/JavaNetHttpClientProperties.java index 3a48d2c69a7..bdd17c68153 100644 --- a/connectors/jnh-connector/src/main/java/org/glassfish/jersey/jnh/connector/JavaNetHttpClientProperties.java +++ b/connectors/jnh-connector/src/main/java/org/glassfish/jersey/jnh/connector/JavaNetHttpClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -57,24 +57,49 @@ public class JavaNetHttpClientProperties { public static final String SSL_PARAMETERS = "jersey.config.jnh.client.sslParameters"; /** - * An instance of the {@link java.net.Authenticator} class that should be used to retrieve - * credentials from a user. - * + *
+ * An instance of the {@link java.net.Authenticator} class that should be used to retrieve + * credentials from a user. + *
+ *+ * The name of the configuration property is {@value}. + *
*/ public static final String PREEMPTIVE_BASIC_AUTHENTICATION = "jersey.config.jnh.client.preemptiveBasicAuthentication"; /** - * A value of {@code false} indicates the client should handle cookies - * automatically using HttpClient's default cookie policy. A value - * of {@code false} will cause the client to ignore all cookies. - * - * The value MUST be an instance of {@link java.lang.Boolean}. - * If the property is absent the default value is {@code false} + *+ * A value of {@code false} indicates the client should handle cookies + * automatically using HttpClient's default cookie policy. A value + * of {@code false} will cause the client to ignore all cookies. + *
+ *+ * The value MUST be an instance of {@link java.lang.Boolean}. + * If the property is absent the default value is {@code false} + *
+ *+ * The name of the configuration property is {@value}. + *
*/ public static final String DISABLE_COOKIES = "jersey.config.jnh.client.disableCookies"; + /** + *+ * Java Net Http client sets SslProperties per client rather than per request. + * While most connectors support HOST header to set the SNIHostName, the headers cannot be set per client, + * and SNIHostName is set by this property. + *
+ *+ * The value MUST be an instance of {@link java.lang.Boolean}. + *
+ *+ * The name of the configuration property is {@value}. + *
+ * @since 3.1.2 + */ + public static final String SNI_HOST_NAME = "jersey.config.jnh.client.SniHostName"; /** * Prevent this class from instantiation. diff --git a/connectors/jnh-connector/src/main/java/org/glassfish/jersey/jnh/connector/JavaNetHttpConnector.java b/connectors/jnh-connector/src/main/java/org/glassfish/jersey/jnh/connector/JavaNetHttpConnector.java index 7530c4ec93d..d51ca7013bb 100644 --- a/connectors/jnh-connector/src/main/java/org/glassfish/jersey/jnh/connector/JavaNetHttpConnector.java +++ b/connectors/jnh-connector/src/main/java/org/glassfish/jersey/jnh/connector/JavaNetHttpConnector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -19,12 +19,14 @@ import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MultivaluedMap; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.client.innate.ClientProxy; import org.glassfish.jersey.client.innate.Expect100ContinueUsage; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; import org.glassfish.jersey.client.spi.AsyncConnectorCallback; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.internal.Version; @@ -49,14 +51,13 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; -import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.logging.Logger; -import java.util.zip.GZIPOutputStream; /** * Provides a Jersey client {@link Connector}, which internally uses Java's {@link HttpClient}. @@ -104,11 +105,14 @@ public JavaNetHttpConnector(final Client client, final Configuration configurati } else { httpClientBuilder.followRedirects(HttpClient.Redirect.NORMAL); } + SSLParameters sslParameters = getPropertyOrNull(configuration, JavaNetHttpClientProperties.SSL_PARAMETERS, SSLParameters.class); + sslParameters = new SniSslParameters(sslParameters).getSslParameters(client); if (sslParameters != null) { httpClientBuilder.sslParameters(sslParameters); } + final Authenticator preemptiveAuthenticator = getPropertyOrNull(configuration, JavaNetHttpClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, Authenticator.class); @@ -170,8 +174,20 @@ public OutputStream getOutputStream(int contentLength) throws IOException { * @return the {@link HttpRequest} instance for the {@link HttpClient} request */ private HttpRequest getHttpRequest(ClientRequest request) { + final String sniHostName = sniHostName(request.getConfiguration()); + final URI sniUri; + if (sniHostName != null) { + final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder() + .uri(request.getUri()) + .headers(Map.of(HttpHeaders.HOST, List.of(sniHostName))) + .build(); + sniUri = sniConfig.isSNIRequired() ? sniConfig.toIPRequestUri() : request.getUri(); + } else { + sniUri = request.getUri(); + } + HttpRequest.Builder builder = HttpRequest.newBuilder(); - builder.uri(request.getUri()); + builder.uri(sniUri); HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.noBody(); if (request.hasEntity()) { try { @@ -296,4 +312,41 @@ public CookieHandler getCookieHandler() { } return null; } + + private static class SniSslParameters { + private final SSLParameters sslParameters; + + private SniSslParameters(SSLParameters sslParameters) { + this.sslParameters = sslParameters; + } + + private SSLParameters getSslParameters(Client client) { + String sniHostName = sniHostName(client.getConfiguration()); + SSLParameters sslParameters = this.sslParameters; + if (sniHostName != null) { + if (sslParameters == null) { + sslParameters = new SSLParameters(); + } + SSLParamConfigurator sniConfig = SSLParamConfigurator.builder() + .headers(Collections.singletonMap(HttpHeaders.HOST, List.of(sniHostName))) + .build(); + sniConfig.setSNIServerName(sslParameters); + } + + return sslParameters; + } + } + + private static String sniHostName(Configuration configuration) { + final String sniHostname; + if (configuration.hasProperty(JavaNetHttpClientProperties.SNI_HOST_NAME)) { + sniHostname = (String) configuration.getProperty(JavaNetHttpClientProperties.SNI_HOST_NAME); + } else if (configuration.hasProperty(HttpHeaders.HOST)) { + sniHostname = (String) configuration.getProperty(HttpHeaders.HOST); + } else { + sniHostname = null; + } + return sniHostname; + } + } diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java index 33ef0b6a200..71018bddd3e 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java @@ -177,6 +177,15 @@ public void setSNIServerName(SSLSocket sslSocket) { sniConfigurator.ifPresent(sni -> sni.setServerNames(sslSocket)); } + /** + * Set {@link javax.net.ssl.SNIServerName} for the {@link SSLParameters} when SNI should be used + * (i.e. {@link jakarta.ws.rs.core.HttpHeaders#HOST} differs from HTTP request host name) + * @param parameters the {@link SSLParameters} to be set + */ + public void setSNIServerName(SSLParameters parameters) { + sniConfigurator.ifPresent(sni -> sni.updateSSLParameters(parameters)); + } + /** * Set setEndpointIdentificationAlgorithm to HTTPS. This is to prevent man-in-the-middle attacks. * @param sslEngine the {@link SSLEngine} the algorithm is set for. diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java index 39a339db6b2..1a48a3a25a6 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java @@ -69,9 +69,11 @@ static Optional