From 73bdf5ff2deb24e72e7fd5c8dfa78017bb581387 Mon Sep 17 00:00:00 2001 From: jansupol Date: Mon, 21 Aug 2023 17:57:26 +0200 Subject: [PATCH] Allowing using SSLContext supplier per request by the NettyConnector Signed-off-by: jansupol --- .../connector/NettyClientProperties.java | 4 +- .../netty/connector/NettyConnector.java | 19 +- .../jersey/client/ClientProperties.java | 11 + .../glassfish/jersey/client/JerseyClient.java | 71 +++-- .../jersey/client/JerseyClientBuilder.java | 52 +--- .../client/SslContextClientBuilder.java | 258 ++++++++++++++++++ .../client/internal/HttpUrlConnector.java | 24 +- .../internal/util/collection/Value.java | 6 +- docs/src/main/docbook/appendix-properties.xml | 15 + docs/src/main/docbook/jersey.ent | 1 + .../e2e/tls/SslContextPerRequestTest.java | 210 ++++++++++++++ .../src/test/resources/client-truststore.jks | Bin 0 -> 1130 bytes .../src/test/resources/server-identity.jks | Bin 0 -> 2527 bytes 13 files changed, 566 insertions(+), 105 deletions(-) create mode 100644 core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java create mode 100644 tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java create mode 100644 tests/e2e-tls/src/test/resources/client-truststore.jks create mode 100644 tests/e2e-tls/src/test/resources/server-identity.jks diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java index 671b08ff25..186c6d4159 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 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 @@ -72,7 +72,7 @@ public class NettyClientProperties { /** * The maximal number of redirects during single request. *

- * Value is expected to be positive {@link Integer}. Default value is {@value #DEFAULT_MAX_REDIRECTS}. + * Value is expected to be positive {@link Integer}. Default value is 5. *

* HTTP redirection must be enabled by property {@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS}, * otherwise {@code MAX_REDIRECTS} is not applied. diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java index a2c675a223..b3289e1def 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java @@ -35,7 +35,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import javax.net.ssl.SSLContext; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.core.Configuration; @@ -197,8 +199,10 @@ protected void execute(final ClientRequest jerseyRequest, final Set redirec int port = requestUri.getPort() != -1 ? requestUri.getPort() : "https".equals(requestUri.getScheme()) ? 443 : 80; try { + final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder() + .request(jerseyRequest).setSNIAlways(true).build(); - String key = requestUri.getScheme() + "://" + host + ":" + port; + String key = requestUri.getScheme() + "://" + sslConfig.getSNIHostName() + ":" + port; ArrayList conns; synchronized (connections) { conns = connections.get(key); @@ -228,9 +232,8 @@ protected void execute(final ClientRequest jerseyRequest, final Set redirec } } - Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0); - if (chan == null) { + Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0); Bootstrap b = new Bootstrap(); // http proxy @@ -267,7 +270,7 @@ protected void initChannel(SocketChannel ch) throws Exception { if ("https".equals(requestUri.getScheme())) { // making client authentication optional for now; it could be extracted to configurable property JdkSslContext jdkSslContext = new JdkSslContext( - client.getSslContext(), + getSslContext(client, jerseyRequest), true, (Iterable) null, IdentityCipherSuiteFilter.INSTANCE, @@ -278,8 +281,7 @@ protected void initChannel(SocketChannel ch) throws Exception { ); final int port = requestUri.getPort(); - final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder() - .request(jerseyRequest).setSNIAlways(true).build(); + final SslHandler sslHandler = jdkSslContext.newHandler( ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService ); @@ -455,6 +457,11 @@ public void run() { } } + private SSLContext getSslContext(Client client, ClientRequest request) { + Supplier supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class); + return supplier == null ? client.getSslContext() : supplier.get(); + } + private String buildPathWithQueryParameters(URI requestUri) { if (requestUri.getRawQuery() != null) { return String.format("%s?%s", requestUri.getRawPath(), requestUri.getRawQuery()); diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java index a1a63717d6..7ba912a1d0 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java @@ -24,6 +24,7 @@ import org.glassfish.jersey.internal.util.PropertiesHelper; import org.glassfish.jersey.internal.util.PropertyAlias; +import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; /** @@ -481,6 +482,16 @@ public final class ClientProperties { */ public static final String CONNECTOR_PROVIDER = "jersey.config.client.connector.provider"; + /** + *

The {@link javax.net.ssl.SSLContext} {@link java.util.function.Supplier} to be used to set ssl context in the current + * HTTP request. Has precedence over the {@link Client#getSslContext()}. + *

+ *

Currently supported by the default {@code HttpUrlConnector} and by {@code NettyConnector} only.

+ * @since 2.41 + * @see org.glassfish.jersey.client.SslContextClientBuilder + */ + public static final String SSL_CONTEXT_SUPPLIER = "jersey.config.client.ssl.context.supplier"; + private ClientProperties() { // prevents instantiation } diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java index f7e3916007..214625dc2b 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 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 @@ -20,13 +20,13 @@ import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.net.URI; -import java.util.Iterator; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -40,9 +40,7 @@ import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.client.internal.LocalizationMessages; import org.glassfish.jersey.client.spi.DefaultSslContextProvider; -import org.glassfish.jersey.internal.ServiceFinder; import org.glassfish.jersey.internal.util.collection.UnsafeValue; -import org.glassfish.jersey.internal.util.collection.Values; import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull; import static org.glassfish.jersey.internal.guava.Preconditions.checkState; @@ -67,7 +65,7 @@ public SSLContext getDefaultSslContext() { private final boolean isDefaultSslContext; private final ClientConfig config; private final HostnameVerifier hostnameVerifier; - private final UnsafeValue sslContext; + private final Supplier sslContext; private final LinkedBlockingDeque> shutdownHooks = new LinkedBlockingDeque>(); private final ReferenceQueue shReferenceQueue = new ReferenceQueue(); @@ -86,7 +84,7 @@ interface ShutdownHook { * Create a new Jersey client instance using a default configuration. */ protected JerseyClient() { - this(null, (UnsafeValue) null, null, null); + this(null, new SslContextClientBuilder(), null, null); } /** @@ -115,7 +113,9 @@ protected JerseyClient(final Configuration config, final SSLContext sslContext, final HostnameVerifier verifier, final DefaultSslContextProvider defaultSslContextProvider) { - this(config, sslContext == null ? null : Values.unsafe(sslContext), verifier, + this(config, + sslContext == null ? new SslContextClientBuilder() : new SslContextClientBuilder().sslContext(sslContext), + verifier, defaultSslContextProvider); } @@ -145,32 +145,32 @@ protected JerseyClient(final Configuration config, final UnsafeValue sslContextProvider, final HostnameVerifier verifier, final DefaultSslContextProvider defaultSslContextProvider) { - this.config = config == null ? new ClientConfig(this) : new ClientConfig(this, config); - - if (sslContextProvider == null) { - this.isDefaultSslContext = true; - - if (defaultSslContextProvider != null) { - this.sslContext = createLazySslContext(defaultSslContextProvider); - } else { - final DefaultSslContextProvider lookedUpSslContextProvider; - - final Iterator iterator = - ServiceFinder.find(DefaultSslContextProvider.class).iterator(); - - if (iterator.hasNext()) { - lookedUpSslContextProvider = iterator.next(); - } else { - lookedUpSslContextProvider = DEFAULT_SSL_CONTEXT_PROVIDER; - } + this(config, + sslContextProvider == null + ? new SslContextClientBuilder() + : new SslContextClientBuilder().sslContext(sslContextProvider.get()), + verifier, + defaultSslContextProvider + ); + } - this.sslContext = createLazySslContext(lookedUpSslContextProvider); - } - } else { - this.isDefaultSslContext = false; - this.sslContext = Values.lazy(sslContextProvider); + /** + * Create a new Jersey client instance. + * + * @param config jersey client configuration. + * @param sslContextClientBuilder jersey client SSL context builder. The builder is expected to + * return non-default value. + * @param verifier jersey client host name verifier. + * @param defaultSslContextProvider default SSL context provider. + */ + JerseyClient(final Configuration config, final SslContextClientBuilder sslContextClientBuilder, + final HostnameVerifier verifier, final DefaultSslContextProvider defaultSslContextProvider) { + if (defaultSslContextProvider != null) { + sslContextClientBuilder.defaultSslContextProvider(defaultSslContextProvider); } - + this.config = config == null ? new ClientConfig(this) : new ClientConfig(this, config); + this.isDefaultSslContext = sslContextClientBuilder.isDefaultSslContext(); + this.sslContext = sslContextClientBuilder; this.hostnameVerifier = verifier; } @@ -195,15 +195,6 @@ private void release() { } } - private UnsafeValue createLazySslContext(final DefaultSslContextProvider provider) { - return Values.lazy(new UnsafeValue() { - @Override - public SSLContext get() { - return provider.getDefaultSslContext(); - } - }); - } - /** * Register a new client shutdown hook. * diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java index 03d4860d74..f56c3aae84 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java @@ -32,16 +32,12 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; -import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.client.innate.inject.NonInjectionManager; -import org.glassfish.jersey.client.internal.LocalizationMessages; import org.glassfish.jersey.client.spi.ClientBuilderListener; import org.glassfish.jersey.client.spi.ConnectorProvider; import org.glassfish.jersey.internal.ServiceFinder; import org.glassfish.jersey.internal.config.ExternalPropertiesConfigurationFactory; import org.glassfish.jersey.internal.util.ReflectionHelper; -import org.glassfish.jersey.internal.util.collection.UnsafeValue; -import org.glassfish.jersey.internal.util.collection.Values; import org.glassfish.jersey.model.internal.RankedComparator; import org.glassfish.jersey.model.internal.RankedProvider; @@ -54,8 +50,7 @@ public class JerseyClientBuilder extends ClientBuilder { private final ClientConfig config; private HostnameVerifier hostnameVerifier; - private SslConfigurator sslConfigurator; - private SSLContext sslContext; + private final SslContextClientBuilder sslContextClientBuilder = new SslContextClientBuilder(); private static final List CLIENT_BUILDER_LISTENERS; @@ -113,41 +108,19 @@ private static void init(ClientBuilder builder) { @Override public JerseyClientBuilder sslContext(SSLContext sslContext) { - if (sslContext == null) { - throw new NullPointerException(LocalizationMessages.NULL_SSL_CONTEXT()); - } - this.sslContext = sslContext; - sslConfigurator = null; + sslContextClientBuilder.sslContext(sslContext); return this; } @Override public JerseyClientBuilder keyStore(KeyStore keyStore, char[] password) { - if (keyStore == null) { - throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE()); - } - if (password == null) { - throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE_PASWORD()); - } - if (sslConfigurator == null) { - sslConfigurator = SslConfigurator.newInstance(); - } - sslConfigurator.keyStore(keyStore); - sslConfigurator.keyPassword(password); - sslContext = null; + sslContextClientBuilder.keyStore(keyStore, password); return this; } @Override public JerseyClientBuilder trustStore(KeyStore trustStore) { - if (trustStore == null) { - throw new NullPointerException(LocalizationMessages.NULL_TRUSTSTORE()); - } - if (sslConfigurator == null) { - sslConfigurator = SslConfigurator.newInstance(); - } - sslConfigurator.trustStore(trustStore); - sslContext = null; + sslContextClientBuilder.trustStore(trustStore); return this; } @@ -194,22 +167,7 @@ public JerseyClient build() { ExternalPropertiesConfigurationFactory.configure(this.config); setConnectorFromProperties(); - if (sslContext != null) { - return new JerseyClient(config, sslContext, hostnameVerifier, null); - } else if (sslConfigurator != null) { - final SslConfigurator sslConfiguratorCopy = sslConfigurator.copy(); - return new JerseyClient( - config, - Values.lazy(new UnsafeValue() { - @Override - public SSLContext get() { - return sslConfiguratorCopy.createSSLContext(); - } - }), - hostnameVerifier); - } else { - return new JerseyClient(config, (UnsafeValue) null, hostnameVerifier); - } + return new JerseyClient(config, sslContextClientBuilder, hostnameVerifier, null); } private void setConnectorFromProperties() { diff --git a/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java b/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java new file mode 100644 index 0000000000..ca271b2a4d --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 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 + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.client.spi.DefaultSslContextProvider; +import org.glassfish.jersey.internal.ServiceFinder; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; + +import javax.net.ssl.SSLContext; +import javax.ws.rs.client.WebTarget; +import java.security.KeyStore; +import java.util.Iterator; +import java.util.function.Supplier; + +/** + *

The class that builds {@link SSLContext} for the client from keystore, truststore. Provides a cached + * {@link Supplier} from the built or user provided {@link SSLContext}.

+ * + *

The class is used internally by {@link JerseyClientBuilder}, or it can be used by connectors supporting setting + * the {@link SSLContext} per request.

+ * + * @see javax.ws.rs.client.ClientBuilder#keyStore(KeyStore, char[]) + * @see javax.ws.rs.client.ClientBuilder#keyStore(KeyStore, String) + * @see javax.ws.rs.client.ClientBuilder#sslContext(SSLContext) + */ +public final class SslContextClientBuilder implements Supplier { + private SslConfigurator sslConfigurator = null; + private SSLContext sslContext = null; + private DefaultSslContextProvider defaultSslContextProvider = null; + private final Supplier suppliedValue = Values.lazy((Value) () -> supply()); + + private static final DefaultSslContextProvider DEFAULT_SSL_CONTEXT_PROVIDER = new DefaultSslContextProvider() { + @Override + public SSLContext getDefaultSslContext() { + return SslConfigurator.getDefaultContext(); + } + }; + + /** + * Set the SSL context that will be used when creating secured transport connections + * to server endpoints from {@link WebTarget web targets} created by the client + * instance that is using this SSL context. The SSL context is expected to have all the + * security infrastructure initialized, including the key and trust managers. + *

+ * Setting a SSL context instance resets any {@link #keyStore(java.security.KeyStore, char[]) + * key store} or {@link #trustStore(java.security.KeyStore) trust store} values previously + * specified. + *

+ * + * @param sslContext secure socket protocol implementation which acts as a factory + * for secure socket factories or {@link javax.net.ssl.SSLEngine + * SSL engines}. Must not be {@code null}. + * @return an updated ssl client context builder instance. + * @throws NullPointerException in case the {@code sslContext} parameter is {@code null}. + * @see #keyStore(java.security.KeyStore, char[]) + * @see #keyStore(java.security.KeyStore, String) + * @see #trustStore + */ + public SslContextClientBuilder sslContext(SSLContext sslContext) { + if (sslContext == null) { + throw new NullPointerException(LocalizationMessages.NULL_SSL_CONTEXT()); + } + this.sslContext = sslContext; + sslConfigurator = null; + return this; + } + + /** + * Set the client-side key store. Key store contains client's private keys, and the certificates with their + * corresponding public keys. + *

+ * Setting a key store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance} + * value previously specified. + *

+ *

+ * Note that for improved security of working with password data and avoid storing passwords in Java string + * objects, the {@link #keyStore(java.security.KeyStore, char[])} version of the method can be utilized. + * Also note that a custom key store is only required if you want to enable a custom setup of a 2-way SSL + * connections (client certificate authentication). + *

+ * + * @param keyStore client-side key store. Must not be {@code null}. + * @param password client key password. Must not be {@code null}. + * @return an updated ssl client context builder instance. + * @throws NullPointerException in case any of the supplied parameters is {@code null}. + * @see #sslContext + * @see #keyStore(java.security.KeyStore, char[]) + * @see #trustStore + */ + public SslContextClientBuilder keyStore(KeyStore keyStore, char[] password) { + if (keyStore == null) { + throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE()); + } + if (password == null) { + throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE_PASWORD()); + } + if (sslConfigurator == null) { + sslConfigurator = SslConfigurator.newInstance(); + } + sslConfigurator.keyStore(keyStore); + sslConfigurator.keyPassword(password); + sslContext = null; + return this; + } + + /** + * Set the client-side trust store. Trust store is expected to contain certificates from other parties + * the client is you expect to communicate with, or from Certificate Authorities that are trusted to + * identify other parties. + *

+ * Setting a trust store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance} + * value previously specified. + *

+ *

+ * In case a custom trust store or custom SSL context is not specified, the trust management will be + * configured to use the default Java runtime settings. + *

+ * + * @param trustStore client-side trust store. Must not be {@code null}. + * @return an updated ssl client context builder instance. + * @throws NullPointerException in case the supplied trust store parameter is {@code null}. + * @see #sslContext + * @see #keyStore(java.security.KeyStore, char[]) + * @see #keyStore(java.security.KeyStore, String) + */ + public SslContextClientBuilder trustStore(KeyStore trustStore) { + if (trustStore == null) { + throw new NullPointerException(LocalizationMessages.NULL_TRUSTSTORE()); + } + if (sslConfigurator == null) { + sslConfigurator = SslConfigurator.newInstance(); + } + sslConfigurator.trustStore(trustStore); + sslContext = null; + return this; + } + + /** + * Set the client-side key store. Key store contains client's private keys, and the certificates with their + * corresponding public keys. + *

+ * Setting a key store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance} + * value previously specified. + *

+ *

+ * Note that for improved security of working with password data and avoid storing passwords in Java string + * objects, the {@link #keyStore(java.security.KeyStore, char[])} version of the method can be utilized. + * Also note that a custom key store is only required if you want to enable a custom setup of a 2-way SSL + * connections (client certificate authentication). + *

+ * + * @param keyStore client-side key store. Must not be {@code null}. + * @param password client key password. Must not be {@code null}. + * @return an updated ssl client context builder instance. + * @throws NullPointerException in case any of the supplied parameters is {@code null}. + * @see #sslContext + * @see #keyStore(java.security.KeyStore, char[]) + * @see #trustStore + */ + public SslContextClientBuilder keyStore(final KeyStore keyStore, final String password) { + return keyStore(keyStore, password.toCharArray()); + } + + /** + * Get information about used {@link SSLContext}. + * + * @return {@code true} when used {@code SSLContext} is acquired from {@link SslConfigurator#getDefaultContext()}, + * {@code false} otherwise. + */ + public boolean isDefaultSslContext() { + return sslContext == null && sslConfigurator == null; + } + + /** + * Supply SSLContext from this builder. + * @return {@link SSLContext} + */ + @Override + public SSLContext get() { + return suppliedValue.get(); + } + + /** + * Build SSLContext from the Builder. + * @return {@link SSLContext} + */ + public SSLContext build() { + return suppliedValue.get(); + } + + /** + * Set the default SSL context provider. + * @param defaultSslContextProvider the default SSL context provider. + * @return an updated ssl client context builder instance. + */ + protected SslContextClientBuilder defaultSslContextProvider(DefaultSslContextProvider defaultSslContextProvider) { + this.defaultSslContextProvider = defaultSslContextProvider; + return this; + } + + /** + * Supply the {@link SSLContext} to the supplier. Can throw illegal state exception when there is a problem with creating or + * obtaining default SSL context. + * @return SSLContext + */ + private SSLContext supply() { + final SSLContext providedValue; + if (sslContext != null) { + providedValue = sslContext; + } else if (sslConfigurator != null) { + final SslConfigurator sslConfiguratorCopy = sslConfigurator.copy(); + providedValue = sslConfiguratorCopy.createSSLContext(); + } else { + providedValue = null; + } + + final SSLContext returnValue; + if (providedValue == null) { + if (defaultSslContextProvider != null) { + returnValue = defaultSslContextProvider.getDefaultSslContext(); + } else { + final DefaultSslContextProvider lookedUpSslContextProvider; + + final Iterator iterator = + ServiceFinder.find(DefaultSslContextProvider.class).iterator(); + + if (iterator.hasNext()) { + lookedUpSslContextProvider = iterator.next(); + } else { + lookedUpSslContextProvider = DEFAULT_SSL_CONTEXT_PROVIDER; + } + + returnValue = lookedUpSslContextProvider.getDefaultSslContext(); + } + } else { + returnValue = providedValue; + } + + return returnValue; + } +} diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java index 3e29e2be8a..2054dcf079 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java @@ -39,12 +39,14 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.ws.rs.ProcessingException; @@ -111,7 +113,7 @@ public class HttpUrlConnector implements Connector { private final boolean fixLengthStreaming; private final boolean setMethodWorkaround; private final boolean isRestrictedHeaderPropertySet; - private final LazyValue sslSocketFactory; + private LazyValue sslSocketFactory; private final ConnectorExtension connectorExtension = new HttpUrlExpect100ContinueConnectorExtension(); @@ -135,13 +137,6 @@ public HttpUrlConnector( final boolean fixLengthStreaming, final boolean setMethodWorkaround) { - sslSocketFactory = Values.lazy(new Value() { - @Override - public SSLSocketFactory get() { - return client.getSslContext().getSocketFactory(); - } - }); - this.connectionFactory = connectionFactory; this.chunkSize = chunkSize; this.fixLengthStreaming = fixLengthStreaming; @@ -331,6 +326,7 @@ protected void secureConnection(final JerseyClient client, final HttpURLConnecti */ private void secureConnection( final ClientRequest clientRequest, final HttpURLConnection uc, final SSLParamConfigurator sniConfig) { + setSslContextFactory(clientRequest.getClient(), clientRequest); secureConnection(clientRequest.getClient(), uc); // keep this for compatibility if (sniConfig.isSNIRequired() && uc instanceof HttpsURLConnection) { // set SNI @@ -341,6 +337,18 @@ private void secureConnection( } } + private void setSslContextFactory(Client client, ClientRequest request) { + final Supplier supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class); + + sslSocketFactory = Values.lazy(new Value() { + @Override + public SSLSocketFactory get() { + final SSLContext ctx = supplier == null ? client.getSslContext() : supplier.get(); + return ctx.getSocketFactory(); + } + }); + } + private ClientResponse _apply(final ClientRequest request) throws IOException { final HttpURLConnection uc; final Optional proxy = ClientProxy.proxyFromRequest(request); diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java index ee2380073c..891ec3a706 100644 --- a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java +++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 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 @@ -16,13 +16,15 @@ package org.glassfish.jersey.internal.util.collection; +import java.util.function.Supplier; + /** * A generic value provider. * * @param value type. * @author Marek Potociar */ -public interface Value { +public interface Value extends Supplier { /** * Get the stored value. * diff --git a/docs/src/main/docbook/appendix-properties.xml b/docs/src/main/docbook/appendix-properties.xml index dd3cc080e1..622a34b085 100644 --- a/docs/src/main/docbook/appendix-properties.xml +++ b/docs/src/main/docbook/appendix-properties.xml @@ -1076,6 +1076,21 @@ + + &jersey.client.ClientProperties.SSL_CONTEXT_SUPPLIER; + jersey.config.client.ssl.context.supplier + + + The javax.net.ssl.SSLContext java.util.function.Supplier to be used to set ssl + context in the current HTTP request. Has precedence over the + Client#getSslContext(). + + + Currently supported by the default HttpUrlConnector and by + NettyConnector only. + + + &jersey.client.ClientProperties.USE_ENCODING; jersey.config.client.useEncoding diff --git a/docs/src/main/docbook/jersey.ent b/docs/src/main/docbook/jersey.ent index 990f12a01b..d4653a10ed 100644 --- a/docs/src/main/docbook/jersey.ent +++ b/docs/src/main/docbook/jersey.ent @@ -351,6 +351,7 @@ ClientProperties.READ_TIMEOUT" > ClientProperties.REQUEST_ENTITY_PROCESSING" > ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION" > +ClientProperties.SSL_CONTEXT_SUPPLIER" > ClientProperties.USE_ENCODING" > ClientProperties.DIGESTAUTH_URI_CACHE_SIZELIMIT" > ClientProperties.EXPECT_100_CONTINUE" > diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java new file mode 100644 index 0000000000..9ae957e9a7 --- /dev/null +++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 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 + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.tls; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.SslContextClientBuilder; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.netty.connector.NettyClientProperties; +import org.glassfish.jersey.netty.connector.NettyConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.UriBuilder; +import java.io.InputStream; +import java.net.URI; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class SslContextPerRequestTest extends JerseyTest { + + private SSLContext serverSslContext; + private SSLParameters serverSslParameters; + private static final String MESSAGE = "Message for Netty with SSL"; + + @Override + protected TestContainerFactory getTestContainerFactory() { + return new GrizzlyTestContainerFactory(); + } + + @Path("secure") + public static class TestResource { + @GET + public String get(@Context HttpHeaders headers) { + return MESSAGE; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(TestResource.class); + } + + @Override + protected URI getBaseUri() { + return UriBuilder + .fromUri("https://localhost") + .port(getPort()) + .build(); + } + + @Override + protected Optional getSslContext() { + if (serverSslContext == null) { + serverSslContext = SslUtils.createServerSslContext(); + } + + return Optional.of(serverSslContext); + } + + @Override + protected Optional getSslParameters() { + if (serverSslParameters == null) { + serverSslParameters = new SSLParameters(); + serverSslParameters.setNeedClientAuth(false); + } + + return Optional.of(serverSslParameters); + } + + public static Stream connectorProviders() { + return Stream.of( + new HttpUrlConnectorProvider(), + new NettyConnectorProvider() + ); + } + + @ParameterizedTest + @MethodSource("connectorProviders") + public void sslOnRequestTest(ConnectorProvider connectorProvider) throws NoSuchAlgorithmException { + Supplier clientSslContext = SslUtils.createClientSslContext(); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(connectorProvider); + config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); + + Client client = ClientBuilder.newBuilder().withConfig(config).build(); + + WebTarget target = client.target(getBaseUri()).path("secure"); + + String s; + + s = target.request() + .property(ClientProperties.SSL_CONTEXT_SUPPLIER, clientSslContext) + .get(String.class); + Assertions.assertEquals(MESSAGE, s); + + try { + Invocation.Builder builder = target.request() + .property(ClientProperties.SSL_CONTEXT_SUPPLIER, + new SslContextClientBuilder().sslContext(SSLContext.getDefault())); + + if (NettyConnectorProvider.class.isInstance(connectorProvider)) { + builder = builder.header(HttpHeaders.HOST, "TestHost"); // New Netty channel without SSL yet + } + s = builder.get(String.class); + Assertions.fail("The SSL Exception has not been thrown"); + } catch (ProcessingException pe) { + // expected + } + + s = target.request() + .property(ClientProperties.SSL_CONTEXT_SUPPLIER, clientSslContext) + .get(String.class); + Assertions.assertEquals(MESSAGE, s); + } + + @ParameterizedTest + @MethodSource("connectorProviders") + public void testSslOnClient(ConnectorProvider connectorProvider) { + Supplier clientSslContext = SslUtils.createClientSslContext(); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(connectorProvider); + + Client client = ClientBuilder.newBuilder().withConfig(config) + .sslContext(clientSslContext.get()) + .build(); + + WebTarget target = client.target(getBaseUri()).path("secure"); + + String s = target.request().get(String.class); + Assertions.assertEquals(MESSAGE, s); + } + + private static class SslUtils { + + private static final String SERVER_IDENTITY_PATH = "server-identity.jks"; + private static final char[] SERVER_IDENTITY_PASSWORD = "secret".toCharArray(); + + private static final String CLIENT_TRUSTSTORE_PATH = "client-truststore.jks"; + private static final char[] CLIENT_TRUSTSTORE_PASSWORD = "secret".toCharArray(); + + private static final String KEYSTORE_TYPE = "PKCS12"; + + private SslUtils() {} + + public static SSLContext createServerSslContext() { + return new SslContextClientBuilder() + .keyStore(getKeyStore(SERVER_IDENTITY_PATH, SERVER_IDENTITY_PASSWORD), SERVER_IDENTITY_PASSWORD) + .get(); + } + + public static Supplier createClientSslContext() { + return new SslContextClientBuilder() + .trustStore(getKeyStore(CLIENT_TRUSTSTORE_PATH, CLIENT_TRUSTSTORE_PASSWORD)); + + } + + private static KeyStore getKeyStore(String path, char[] keyStorePassword) { + try (InputStream inputStream = getResource(path)) { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + keyStore.load(inputStream, keyStorePassword); + return keyStore; + } catch (Exception e) { + throw new ProcessingException(e); + } + } + + private static InputStream getResource(String path) { + return SslUtils.class.getClassLoader().getResourceAsStream(path); + } + } +} diff --git a/tests/e2e-tls/src/test/resources/client-truststore.jks b/tests/e2e-tls/src/test/resources/client-truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..539185fda405fced8994213e92b7254f74bc7faf GIT binary patch literal 1130 zcmV-w1eNkR`gFl8^^zGK7iuf`!jm-hwrz1ruQ=q#^LPTM|;MpeojaXI~+z0hK3U(`TZT$ zovxX2)U-}cxvvgP>ik~qZFl!Oyz%9BtXi1z2!?KMd2=2rv$vYipCEQW3oN=bW)$?w z$vBl3*Cx=z^W7wUhBEhI@E@4sSse)(ef1dEBvZs|yo8E!G8e<_XDlBJ1CZVHKt&3f z<;+@6^XHasVwg4{$KdH>5@Eee9;urP9hy3-|BuC|%=ts@#>E>j#-sMoqnnoI`V;Jsr2- z+WbTHCe?VAHf)7{f)C3RS%#&)N0aeMnB=N!KNHTLpdfOqi4LfQVx2~SLwqrv7tAl9T|I&t+5^cXiS2o!`plHPC zb>;6pJtJSt5ukSCb$qb*XlZ-l$;X!I_6xnrYIjF{ak*>AutIB1uG5%0vZJX1Qc~V wF*#3M9R(wPWRxOmhIDvoJ3<5$=PE*Ro*gZud=UbF3;1PWJp_5~0s{etpjzVsg#Z8m literal 0 HcmV?d00001 diff --git a/tests/e2e-tls/src/test/resources/server-identity.jks b/tests/e2e-tls/src/test/resources/server-identity.jks new file mode 100644 index 0000000000000000000000000000000000000000..76a21aaedd2de3664469543220ff583e3ed21a4e GIT binary patch literal 2527 zcmY+EXE+-Q7smzRrZ%x_)a@{v#GbV)h)tDNYm^p6%@!>Zu}h5DD~g&`HL6sN){4@W zp!SFowFNPPzP<1BzV|-&!#U@9{=ak1#~&00nxX~JK~W$q14uF&9(}+9UCmH21Nng|Am0|^eEts-?8QIGG$=;-xc#Y04)whPlclB51CkN%8B zQ77{y>u`GHLH#uG)?1Nh(VslimWPeoB_ow^9g zLz)FcY{^}7g&y~UY3K6Gj?3^{RyrS7U%w6>#|E=fnsW+|IUC;@@*0HmNK0FZ z)N;I?i+q2ql1b2|qXb%G-KyCwUMx)8eKkdW1+F2S4Vi+rA*_KN%*!HT2=;u> zHa2dpDjppz#2K0V!6beh7Lyf`b1HQ(oU_7vmgu?(D0+|@t0`hAUAEk&-4Fcku$xPF zyg=01z{Bsh*VHnvHz3>B*=Kx=e2PYTGe0KVDobYfL|WSSoz1FE+nbup-~-oZbwXXhdw!$SETgz$Fr)lLH%k=Li_lzrBA=``u_b3Kl)_ST&E$*_t1}g77nv2va7`rl zAY5N8?Z%J&ngVWEcKbO*?Ufd^_6dntc-g5efKu=3=F`&2eFu+noZ;#Q49R+vvrWX* zBtpM^+MZGKh|NBaJhMu;h%t*b8ED${^EFR1Sozp=?}ep`b5Es(w#>be0F!|?nwkns z@rxA><#h>+jH^CsRkz>NTXIIu{klj*UTub*V6pKOaOJ*2bD)W#DunakL1 zRyBVf`o66K;qtv$-b%nvh38^qw-J^sBqplbYQS;o{G}VYu{`19k`NNF$@cZ~l7vXR z(8h*KsI9~sA){{8R>$NoBbDfFK4EYnqM#&+2Ixq8&qJQ}scTYwhs4a#*vOW83S;@r zuQZ|6_)n)4_l*Z4}>^cx%vuiR;$$=RAE|B z!M{}r0>fB^nQ8oKTxtAhg8u5z3;#fF7?_^Z+TF`l6s8DOf~hDfC@Ux^sQi{E^WRI1 z3^)|sDHKI_L<^w#J;?tMz`qR3@Rwn+ggb1lTJlbsY}RoI4!L&U4&>^84I6=?dw2DL zkP{HH4HeSFF8ygdy(TEUO>XYImpxh7C*J#nzi&p2l!KmJ%Pq-lx#^Y~s7ZAu5`J z4BhZuB#}bVgIsnRgx6h5e=eWI^5}@6zY#NoN6(DvH7=RIJbAK**CvV^1;@u`r62Lm zx(*gCS2YDtnVoawa%F5xne29ZHRik$`_ni57Nd#Ev*1c@fQe5b{d-T3@TQ*?fK z|9quPk$5%3q)>p9p}lc#=M3GS&${@oW(#R?=;n~ zOR*?l$xT5UMZ_T@#7-g@MAF^MG@1_`g{xpPV$tt}>PE?_z?CBKWkG^YydA$tAuP*! z=B`JR-a7;1n0v4-c%Vbpy!~h9>dObWe-Zew34aX7J%*cTI565XFR(}-;qj#VXEnYH zkfolt>t{c!4r?ViA=!hOnIe1M&g|qZ^|a9Nw5NwvGweJoSF*@^10+m=+xRp?6XU|H zn`LAb_oQXlM0I$g+!P2cHy4JiJUdrty`RM4z329As7m5F1<<++8qkX+IBgPO2X{S z28=KF&GUS-NO{eYDn+H1#PQ9l*!c)2p?~?T*-;Q#(==@>4_&(6_uO>y5i$hac@T+-(9Z#6d)u}GqweqO61oFb4&HF5!tYx80E1&oKx2yx`nHIa8%YI z6)!Z4?A85v%7$jljOn#ZhcJlb9>A8%94u#EeBh*-Umj zzE+Cvz_S>x$b>0!wOdt1j^G8p