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.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
@@ -105,6 +107,12 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-reactive-httpclient</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@
|
||||
<properties>
|
||||
<httpclient.version>4.5.7</httpclient.version>
|
||||
<httpcore.version>4.4.11</httpcore.version>
|
||||
<jetty-client.version>1.0.3</jetty-client.version>
|
||||
<netty.version>4.1.36.Final</netty.version>
|
||||
<okhttp3.version>3.14.2</okhttp3.version>
|
||||
<jackson.version>2.9.9</jackson.version>
|
||||
@@ -105,6 +106,13 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-reactive-httpclient</artifactId>
|
||||
<version>${jetty-client.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,13 +6,18 @@ Spring Vault supports various HTTP clients to access Vault's HTTP API. Spring Va
|
||||
Dedicated client support originates from <<vault.client-ssl,customized SSL configuration>>
|
||||
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]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.netty</groupId>
|
||||
<artifactId>reactor-netty</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
====
|
||||
|
||||
.Jetty
|
||||
====
|
||||
[source, xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-reactive-httpclient</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
====
|
||||
|
||||
[[vault.client-ssl]]
|
||||
== Vault Client SSL configuration
|
||||
|
||||
|
||||
@@ -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 <<vault.authentication.steps,composable authentication steps>> 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 <<vault.authentication.steps,composable authentication steps>> 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
|
||||
|
||||
Reference in New Issue
Block a user