From 22ca10c4414fc36f6822124dc33c1e7feaf3724b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 28 Jun 2019 17:12:36 +0200 Subject: [PATCH] Add support for JettyClientHttpConnector. We now support Jetty's reactive HttpClient next to Reactor Netty if jetty-reactive-httpclient is available on the class path. Closes gh-438. --- spring-vault-core/pom.xml | 10 +- .../client/ClientHttpConnectorFactory.java | 149 ++++++++++++++++-- .../ClientHttpRequestFactoryFactory.java | 24 +-- ...tHttpConnectorFactoryIntegrationTests.java | 75 +++++++++ spring-vault-dependencies/pom.xml | 8 + src/main/asciidoc/new-features.adoc | 1 + .../asciidoc/reference/client-support.adoc | 29 +++- .../asciidoc/reference/reactive-template.adoc | 2 +- 8 files changed, 271 insertions(+), 27 deletions(-) create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/client/ClientHttpConnectorFactoryIntegrationTests.java diff --git a/spring-vault-core/pom.xml b/spring-vault-core/pom.xml index ec1ce943..cd80ef3e 100644 --- a/spring-vault-core/pom.xml +++ b/spring-vault-core/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 @@ -105,6 +107,12 @@ true + + org.eclipse.jetty + jetty-reactive-httpclient + true + + io.netty netty-all diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java index b15f032f..c5ecc07b 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java @@ -17,13 +17,21 @@ package org.springframework.vault.client; import java.io.IOException; import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import io.netty.channel.ChannelOption; import io.netty.handler.ssl.SslContextBuilder; +import org.eclipse.jetty.util.ssl.SslContextFactory; import reactor.netty.http.client.HttpClient; import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.vault.support.ClientOptions; import org.springframework.vault.support.SslConfiguration; @@ -33,13 +41,39 @@ import static org.springframework.vault.client.ClientHttpRequestFactoryFactory.h /** * Factory for {@link ClientHttpConnector} that supports - * {@link ReactorClientHttpConnector}. + * {@link ReactorClientHttpConnector} and {@link JettyClientHttpConnector}. + * + * This factory configures a {@link ClientHttpConnector} depending on the available + * dependencies. * * @author Mark Paluch * @since 2.2 */ public class ClientHttpConnectorFactory { + private static final boolean REACTOR_NETTY_PRESENT = isPresent("reactor.netty.http.client.HttpClient"); + + private static final boolean JETTY_PRESENT = isPresent("org.eclipse.jetty.client.HttpClient"); + + /** + * Checks for presence of all {@code classNames} using this class' classloader. + * + * @param classNames + * @return {@literal true} if all classes are present; {@literal false} if at least + * one class cannot be found. + */ + private static boolean isPresent(String... classNames) { + + for (String className : classNames) { + if (!ClassUtils.isPresent(className, + ClientHttpConnectorFactory.class.getClassLoader())) { + return false; + } + } + + return true; + } + /** * Create a {@link ClientHttpConnector} for the given {@link ClientOptions} and * {@link SslConfiguration}. @@ -51,23 +85,19 @@ public class ClientHttpConnectorFactory { public static ClientHttpConnector create(ClientOptions options, SslConfiguration sslConfiguration) { - HttpClient client = HttpClient.create(); + Assert.notNull(options, "ClientOptions must not be null"); + Assert.notNull(sslConfiguration, "SslConfiguration must not be null"); - if (hasSslConfiguration(sslConfiguration)) { - - SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); - configureSsl(sslConfiguration, sslContextBuilder); - - client = client.secure(builder -> { - builder.sslContext(sslContextBuilder); - }); + if (REACTOR_NETTY_PRESENT) { + return ReactorNetty.usingReactorNetty(options, sslConfiguration); } - client = client.tcpConfiguration(it -> it.option( - ChannelOption.CONNECT_TIMEOUT_MILLIS, - Math.toIntExact(options.getConnectionTimeout().toMillis()))); + if (JETTY_PRESENT) { + return JettyClient.usingJetty(options, sslConfiguration); + } - return new ReactorClientHttpConnector(client); + throw new IllegalStateException( + "No supported Reactive Http Client library available (Reactor Netty, Jetty)"); } private static void configureSsl(SslConfiguration sslConfiguration, @@ -90,4 +120,95 @@ public class ClientHttpConnectorFactory { throw new IllegalStateException(e); } } + + /** + * {@link ClientHttpConnector} for Reactor Netty. + * + * @author Mark Paluch + */ + static class ReactorNetty { + + static ClientHttpConnector usingReactorNetty(ClientOptions options, + SslConfiguration sslConfiguration) { + HttpClient client = HttpClient.create(); + + if (hasSslConfiguration(sslConfiguration)) { + + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + configureSsl(sslConfiguration, sslContextBuilder); + + client = client.secure(builder -> { + builder.sslContext(sslContextBuilder); + }); + } + + client = client.tcpConfiguration(it -> it.option( + ChannelOption.CONNECT_TIMEOUT_MILLIS, + Math.toIntExact(options.getConnectionTimeout().toMillis()))); + + return new ReactorClientHttpConnector(client); + } + } + + static class JettyClient { + static ClientHttpConnector usingJetty(ClientOptions options, + SslConfiguration sslConfiguration) { + + try { + return new JettyClientHttpConnector(configureClient( + getHttpClient(sslConfiguration), options)); + } + catch (GeneralSecurityException | IOException e) { + throw new IllegalStateException(e); + } + } + + private static org.eclipse.jetty.client.HttpClient configureClient( + org.eclipse.jetty.client.HttpClient httpClient, ClientOptions options) { + + httpClient.setConnectTimeout(options.getConnectionTimeout().toMillis()); + httpClient.setAddressResolutionTimeout(options.getConnectionTimeout() + .toMillis()); + + return httpClient; + } + + private static org.eclipse.jetty.client.HttpClient getHttpClient( + SslConfiguration sslConfiguration) throws KeyStoreException, IOException, + NoSuchAlgorithmException, CertificateException { + + if (hasSslConfiguration(sslConfiguration)) { + + SslContextFactory sslContextFactory = new SslContextFactory(); + + if (sslConfiguration.getKeyStoreConfiguration().isPresent()) { + KeyStore keyStore = ClientHttpRequestFactoryFactory + .getKeyStore(sslConfiguration.getKeyStoreConfiguration()); + sslContextFactory.setKeyStore(keyStore); + } + + if (sslConfiguration.getTrustStoreConfiguration().isPresent()) { + KeyStore keyStore = ClientHttpRequestFactoryFactory + .getKeyStore(sslConfiguration.getTrustStoreConfiguration()); + sslContextFactory.setTrustStore(keyStore); + } + + SslConfiguration.KeyConfiguration keyConfiguration = sslConfiguration + .getKeyConfiguration(); + + if (keyConfiguration.getKeyAlias() != null) { + sslContextFactory.setCertAlias(keyConfiguration.getKeyAlias()); + } + + if (keyConfiguration.getKeyPassword() != null) { + sslContextFactory.setKeyManagerPassword(new String(keyConfiguration + .getKeyPassword())); + } + + return new org.eclipse.jetty.client.HttpClient(sslContextFactory); + } + + return new org.eclipse.jetty.client.HttpClient(); + } + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java index 2136cc58..9a3d6183 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java @@ -181,11 +181,7 @@ public class ClientHttpRequestFactoryFactory { KeyStoreConfiguration keyStoreConfiguration, KeyConfiguration keyConfiguration) throws GeneralSecurityException, IOException { - KeyStore keyStore = KeyStore.getInstance(StringUtils - .hasText(keyStoreConfiguration.getStoreType()) ? keyStoreConfiguration - .getStoreType() : KeyStore.getDefaultType()); - - loadKeyStore(keyStoreConfiguration, keyStore); + KeyStore keyStore = getKeyStore(keyStoreConfiguration); KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); @@ -206,15 +202,23 @@ public class ClientHttpRequestFactoryFactory { return keyManagerFactory; } + static KeyStore getKeyStore(KeyStoreConfiguration keyStoreConfiguration) + throws KeyStoreException, IOException, NoSuchAlgorithmException, + CertificateException { + + KeyStore keyStore = KeyStore.getInstance(StringUtils + .hasText(keyStoreConfiguration.getStoreType()) ? keyStoreConfiguration + .getStoreType() : KeyStore.getDefaultType()); + + loadKeyStore(keyStoreConfiguration, keyStore); + return keyStore; + } + static TrustManagerFactory createTrustManagerFactory( KeyStoreConfiguration keyStoreConfiguration) throws GeneralSecurityException, IOException { - KeyStore trustStore = KeyStore.getInstance(StringUtils - .hasText(keyStoreConfiguration.getStoreType()) ? keyStoreConfiguration - .getStoreType() : KeyStore.getDefaultType()); - - loadKeyStore(keyStoreConfiguration, trustStore); + KeyStore trustStore = getKeyStore(keyStoreConfiguration); TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/client/ClientHttpConnectorFactoryIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/client/ClientHttpConnectorFactoryIntegrationTests.java new file mode 100644 index 00000000..31af79d4 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/client/ClientHttpConnectorFactoryIntegrationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.client; + +import org.junit.Test; + +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.util.Settings; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.vault.client.ClientHttpConnectorFactory.JettyClient; +import static org.springframework.vault.client.ClientHttpConnectorFactory.ReactorNetty; + +/** + * Integration tests for {@link ClientHttpConnectorFactory}. + * + * @author Mark Paluch + */ +public class ClientHttpConnectorFactoryIntegrationTests { + + private final String url = new VaultEndpoint().createUriString("sys/health"); + + @Test + public void reactorNettyClientShouldWork() { + + ClientHttpConnector factory = ReactorNetty.usingReactorNetty(new ClientOptions(), + Settings.createSslConfiguration()); + + WebClient webClient = WebClient.builder().clientConnector(factory).build(); + + String response = request(webClient); + + assertThat(response).isNotNull().contains("initialized"); + } + + @Test + public void jettyClientShouldWork() { + + ClientHttpConnector factory = JettyClient.usingJetty(new ClientOptions(), + Settings.createSslConfiguration()); + + WebClient webClient = WebClient.builder().clientConnector(factory).build(); + + String response = request(webClient); + + assertThat(response).isNotNull().contains("initialized"); + } + + private String request(WebClient webClient) { + + // Uninitialized and sealed can cause status 500 + try { + return webClient.get().uri(url).retrieve().bodyToMono(String.class).block(); + } + catch (WebClientResponseException e) { + return e.getResponseBodyAsString(); + } + } +} diff --git a/spring-vault-dependencies/pom.xml b/spring-vault-dependencies/pom.xml index 8299d701..be6a5f2b 100644 --- a/spring-vault-dependencies/pom.xml +++ b/spring-vault-dependencies/pom.xml @@ -59,6 +59,7 @@ 4.5.7 4.4.11 + 1.0.3 4.1.36.Final 3.14.2 2.9.9 @@ -105,6 +106,13 @@ true + + org.eclipse.jetty + jetty-reactive-httpclient + ${jetty-client.version} + true + + io.netty netty-all diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index ef0601a0..618ac8f8 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -5,6 +5,7 @@ === What's new in Spring Vault 2.2 * Support for Key-Value v2 (versioned backend) secrets through `@VaultPropertySource`. * SpEL support in `@Secret`. +* Add support for Jetty as reactive HttpClient. [[new-features.2-1-0]] === What's new in Spring Vault 2.1 diff --git a/src/main/asciidoc/reference/client-support.adoc b/src/main/asciidoc/reference/client-support.adoc index 39d5a0b2..fd3bc1f0 100644 --- a/src/main/asciidoc/reference/client-support.adoc +++ b/src/main/asciidoc/reference/client-support.adoc @@ -6,13 +6,18 @@ Spring Vault supports various HTTP clients to access Vault's HTTP API. Spring Va Dedicated client support originates from <> that is scoped only to Spring Vault's client components. -Spring Vault supports following HTTP clients: +Spring Vault supports following HTTP imperative clients: * Java's builtin `HttpURLConnection` (default client) * Apache Http Components * Netty * OkHttp 3 +Spring Vault's reactive integration supports the following reactive HTTP clients: + +* Reactor Netty +* Jetty + Using a specific client requires the according dependency to be available on the classpath so Spring Vault can use the available client for communicating with Vault. @@ -65,6 +70,28 @@ dependencies to your project. You can omit the version number if using ---- ==== +.Reactor Netty +==== +[source, xml] +---- + + io.projectreactor.netty + reactor-netty + +---- +==== + +.Jetty +==== +[source, xml] +---- + + org.eclipse.jetty + jetty-reactive-httpclient + +---- +==== + [[vault.client-ssl]] == Vault Client SSL configuration diff --git a/src/main/asciidoc/reference/reactive-template.adoc b/src/main/asciidoc/reference/reactive-template.adoc index e0e45a24..69ad94a0 100644 --- a/src/main/asciidoc/reference/reactive-template.adoc +++ b/src/main/asciidoc/reference/reactive-template.adoc @@ -16,7 +16,7 @@ too slow the data repository can also slow down or stop completely until network == Reactive Vault Client -Spring Vault's reactive client support is built on top of <> and Spring's functional `WebClient` via Reactor Netty, which features a fully non-blocking, event-driven HTTP client. +Spring Vault's reactive client support is built on top of <> and Spring's functional `WebClient` via Reactor Netty or Jetty, which feature both a fully non-blocking, event-driven HTTP client. It exposes `VaultTokenSupplier` as supplier of `VaultToken` to authenticate HTTP requests and `ReactiveVaultOperations` as the primary entry point. The core configuration of