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 createWhenHostHeader(URI hostUri, Map serverNames = new LinkedList<>(); serverNames.add(serverName); diff --git a/tests/e2e-tls/pom.xml b/tests/e2e-tls/pom.xml index 5c6b7fd8600..eaa7198fd6f 100644 --- a/tests/e2e-tls/pom.xml +++ b/tests/e2e-tls/pom.xml @@ -44,6 +44,7 @@ ${skip.e2e} true + Host ssl.debug true @@ -109,6 +110,11 @@ jersey-jdk-connector test + + org.glassfish.jersey.connectors + jersey-jnh-connector + test + org.glassfish.jersey.security oauth1-signature diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java index 1e8d89396ad..ddd740093be 100644 --- a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java +++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java @@ -23,9 +23,10 @@ import org.glassfish.jersey.client.HttpUrlConnectorProvider; import org.glassfish.jersey.client.spi.ConnectorProvider; import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.jnh.connector.JavaNetHttpClientProperties; +import org.glassfish.jersey.jnh.connector.JavaNetHttpConnectorProvider; import org.glassfish.jersey.netty.connector.NettyConnectorProvider; import org.glassfish.jersey.tests.e2e.tls.explorer.SSLCapabilities; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -39,13 +40,9 @@ import java.net.InetAddress; import java.net.Socket; import java.net.SocketTimeoutException; -import java.net.URI; import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class SniTest { @@ -54,6 +51,9 @@ public class SniTest { static { +// Debug +// System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); +// System.setProperty("jdk.httpclient.allowRestrictedHeaders", "Host"); // JDK specific settings System.setProperty("jdk.net.hosts.file", SniTest.class.getResource("/hosts").getPath()); } @@ -64,7 +64,8 @@ public static ConnectorProvider[] getConnectors() { new ApacheConnectorProvider(), new Apache5ConnectorProvider(), new JdkConnectorProvider(), - new HttpUrlConnectorProvider() + new HttpUrlConnectorProvider(), + new JavaNetHttpConnectorProvider() }; } @@ -73,6 +74,7 @@ public static ConnectorProvider[] getConnectors() { public void server1Test(ConnectorProvider provider) { ClientConfig clientConfig = new ClientConfig(); clientConfig.connectorProvider(provider); + clientConfig.property(JavaNetHttpClientProperties.SNI_HOST_NAME, "www.host1.com"); serverTest(clientConfig, "www.host1.com"); } @@ -99,6 +101,7 @@ public void filter(ClientRequestContext requestContext) throws IOException { } }) .target("https://" + (newHostName.equals(LOCALHOST) ? LOCALHOST : "www.host0.com") + ":" + PORT) + //.target("https://127.0.0.1:" + PORT) .path("host") .request() .header(HttpHeaders.HOST, hostName + ":8080") @@ -111,7 +114,7 @@ public void filter(ClientRequestContext requestContext) throws IOException { && TimeoutException.class.isInstance(cause)) { cause = cause.getCause(); } - if (cause == null && /*IOE*/ !e.getMessage().contains("Stream closed")) { + if ((!e.getMessage().contains("Stream closed")) && !e.getMessage().contains("timed out")) { throw e; } }