From 6169d5ddcc3c69750830b326c07c05fac4de0c1f Mon Sep 17 00:00:00 2001 From: Jon Marius Venstad Date: Tue, 23 Mar 2021 12:53:38 +0100 Subject: Add builder for synchronous hc5 clients --- .../util/http/hc5/HttpToHttpsRoutePlanner.java | 35 ++++++++++ .../util/http/hc5/VespaAsyncHttpClientBuilder.java | 30 -------- .../util/http/hc5/VespaHttpClientBuilder.java | 80 ++++++++++++++++++++++ 3 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 http-utils/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java create mode 100644 http-utils/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java (limited to 'http-utils/src/main') diff --git a/http-utils/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java b/http-utils/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java new file mode 100644 index 00000000000..672a2fd3918 --- /dev/null +++ b/http-utils/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java @@ -0,0 +1,35 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc5; + +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; +import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** + * {@link HttpRoutePlanner} that changes assumes requests specify the HTTP scheme, + * and then changes this to HTTPS, keeping the other host parameters. + * + * @author jonmv + */ +class HttpToHttpsRoutePlanner implements HttpRoutePlanner { + + @Override + public HttpRoute determineRoute(HttpHost target, HttpContext context) throws HttpException { + if ( ! target.getSchemeName().equals("http")) + throw new IllegalArgumentException("Scheme must be 'http' when using HttpToHttpsRoutePlanner"); + + if (target.getPort() == -1) + throw new IllegalArgumentException("Port must be set when using HttpToHttpsRoutePlanner"); + + if (HttpClientContext.adapt(context).getRequestConfig().getProxy() != null) + throw new IllegalArgumentException("Proxies are not supported with HttpToHttpsRoutePlanner"); + + return new HttpRoute(new HttpHost("https", target.getAddress(), target.getHostName(), target.getPort())); + } + +} diff --git a/http-utils/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java b/http-utils/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java index ed28f328839..219f1707589 100644 --- a/http-utils/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java +++ b/http-utils/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java @@ -4,19 +4,12 @@ package ai.vespa.util.http.hc5; import com.yahoo.security.tls.MixedMode; import com.yahoo.security.tls.TlsContext; import com.yahoo.security.tls.TransportSecurityUtils; -import org.apache.hc.client5.http.HttpRoute; -import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; -import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; -import org.apache.hc.client5.http.routing.HttpRoutePlanner; import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; -import org.apache.hc.core5.http.protocol.HttpContext; import javax.net.ssl.SSLParameters; @@ -70,27 +63,4 @@ public class VespaAsyncHttpClientBuilder { return clientBuilder; } - private static class HttpToHttpsRoutePlanner implements HttpRoutePlanner { - - private final DefaultRoutePlanner defaultPlanner = new DefaultRoutePlanner(new DefaultSchemePortResolver()); - - @Override - public HttpRoute determineRoute(HttpHost target, HttpContext context) throws HttpException { - HttpRoute originalRoute = defaultPlanner.determineRoute(target, context); - HttpHost originalHost = originalRoute.getTargetHost(); - String originalScheme = originalHost.getSchemeName(); - String rewrittenScheme = originalScheme.equalsIgnoreCase("http") ? "https" : originalScheme; - boolean rewrittenSecure = target.getSchemeName().equalsIgnoreCase("https"); - HttpHost rewrittenHost = new HttpHost( - rewrittenScheme, originalHost.getAddress(), originalHost.getHostName(), originalHost.getPort()); - return new HttpRoute( - rewrittenHost, - originalRoute.getLocalAddress(), - originalRoute.getProxyHost(), - rewrittenSecure, - originalRoute.getTunnelType(), - originalRoute.getLayerType()); - } - } - } diff --git a/http-utils/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java b/http-utils/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java new file mode 100644 index 00000000000..2824a1b801d --- /dev/null +++ b/http-utils/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java @@ -0,0 +1,80 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc5; + +import com.yahoo.security.tls.TransportSecurityUtils; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.socket.ConnectionSocketFactory; +import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.core5.http.config.Registry; +import org.apache.hc.core5.http.config.RegistryBuilder; + +import javax.net.ssl.SSLParameters; + +import static com.yahoo.security.tls.MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER; +import static com.yahoo.security.tls.TransportSecurityUtils.getInsecureMixedMode; +import static com.yahoo.security.tls.TransportSecurityUtils.getSystemTlsContext; +import static com.yahoo.security.tls.TransportSecurityUtils.isTransportSecurityEnabled; + +/** + * Sync HTTP client builder for internal Vespa communications over http/https. + * + * Configures Vespa mTLS and handles TLS mixed mode automatically. + * Custom connection managers must be configured through {@link #create(HttpClientConnectionManagerFactory)}. + * + * @author jonmv + */ +public class VespaHttpClientBuilder { + + public interface HttpClientConnectionManagerFactory { + HttpClientConnectionManager create(Registry socketFactories); + } + + public static HttpClientBuilder create() { + return create(PoolingHttpClientConnectionManager::new); + } + + public static HttpClientBuilder create(HttpClientConnectionManagerFactory connectionManagerFactory) { + HttpClientBuilder builder = HttpClientBuilder.create(); + addSslSocketFactory(builder, connectionManagerFactory); + addHttpsRewritingRoutePlanner(builder); + + builder.disableConnectionState(); // Share connections between subsequent requests. + builder.disableCookieManagement(); + builder.disableAuthCaching(); + builder.disableRedirectHandling(); + + return builder; + } + + private static void addSslSocketFactory(HttpClientBuilder builder, HttpClientConnectionManagerFactory connectionManagerFactory) { + getSystemTlsContext().ifPresent(tlsContext -> { + SSLParameters parameters = tlsContext.parameters(); + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(tlsContext.context(), + parameters.getProtocols(), + parameters.getCipherSuites(), + new NoopHostnameVerifier()); + builder.setConnectionManager(connectionManagerFactory.create(createRegistry(socketFactory))); + // Workaround that allows re-using https connections, see https://stackoverflow.com/a/42112034/1615280 for details. + // Proper solution would be to add a request interceptor that adds a x500 principal as user token, + // but certificate subject CN is not accessible through the TlsContext currently. + builder.setUserTokenHandler((route, context) -> null); + }); + } + + private static Registry createRegistry(SSLConnectionSocketFactory sslSocketFactory) { + return RegistryBuilder.create() + .register("https", sslSocketFactory) + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .build(); + } + + private static void addHttpsRewritingRoutePlanner(HttpClientBuilder builder) { + if (isTransportSecurityEnabled() && getInsecureMixedMode() != PLAINTEXT_CLIENT_MIXED_SERVER) + builder.setRoutePlanner(new HttpToHttpsRoutePlanner()); + } + +} -- cgit v1.2.3