diff options
Diffstat (limited to 'vespajlib/src/main/java/ai/vespa')
14 files changed, 721 insertions, 0 deletions
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/VespaHttpClientBuilder.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/VespaHttpClientBuilder.java new file mode 100644 index 00000000000..53bf7b866af --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/VespaHttpClientBuilder.java @@ -0,0 +1,145 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc4; + +import com.yahoo.security.tls.MixedMode; +import com.yahoo.security.tls.TlsContext; +import com.yahoo.security.tls.TransportSecurityUtils; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.UnsupportedSchemeException; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.conn.routing.HttpRoutePlanner; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.impl.conn.DefaultSchemePortResolver; +import org.apache.http.protocol.HttpContext; + +import javax.net.ssl.SSLParameters; +import java.net.InetAddress; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Http client builder for internal Vespa communications over http/https. + * + * Notes: + * - hostname verification is not enabled - CN/SAN verification is assumed to be handled by the underlying x509 trust manager. + * - custom connection managers must be configured through {@link #createBuilder(ConnectionManagerFactory)}. Do not call {@link HttpClientBuilder#setConnectionManager(HttpClientConnectionManager)}. + * + * @author bjorncs + */ +public class VespaHttpClientBuilder { + + private static final Logger log = Logger.getLogger(VespaHttpClientBuilder.class.getName()); + + public interface ConnectionManagerFactory { + HttpClientConnectionManager create(Registry<ConnectionSocketFactory> socketFactoryRegistry); + } + + private VespaHttpClientBuilder() {} + + /** + * Create a client builder with default connection manager. + */ + public static HttpClientBuilder create() { + return createBuilder(null); + } + + /** + * Create a client builder with a user specified connection manager. + */ + public static HttpClientBuilder create(ConnectionManagerFactory connectionManagerFactory) { + return createBuilder(connectionManagerFactory); + } + + /** + * Creates a client builder with a {@link BasicHttpClientConnectionManager} configured. + * This connection manager uses a single connection for all requests. See Javadoc for details. + */ + public static HttpClientBuilder createWithBasicConnectionManager() { + return createBuilder(BasicHttpClientConnectionManager::new); + } + + private static HttpClientBuilder createBuilder(ConnectionManagerFactory connectionManagerFactory) { + HttpClientBuilder builder = HttpClientBuilder.create(); + addSslSocketFactory(builder, connectionManagerFactory); + addHttpsRewritingRoutePlanner(builder); + return builder; + } + + private static void addSslSocketFactory(HttpClientBuilder builder, ConnectionManagerFactory connectionManagerFactory) { + TransportSecurityUtils.getSystemTlsContext() + .ifPresent(tlsContext -> { + log.log(Level.FINE, "Adding ssl socket factory to client"); + SSLConnectionSocketFactory socketFactory = createSslSocketFactory(tlsContext); + if (connectionManagerFactory != null) { + builder.setConnectionManager(connectionManagerFactory.create(createRegistry(socketFactory))); + } else { + builder.setSSLSocketFactory(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(context -> null); + }); + } + + private static void addHttpsRewritingRoutePlanner(HttpClientBuilder builder) { + if (TransportSecurityUtils.isTransportSecurityEnabled() + && TransportSecurityUtils.getInsecureMixedMode() != MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER) { + builder.setRoutePlanner(new HttpToHttpsRoutePlanner()); + } + } + + private static SSLConnectionSocketFactory createSslSocketFactory(TlsContext tlsContext) { + SSLParameters parameters = tlsContext.parameters(); + return new SSLConnectionSocketFactory(tlsContext.context(), parameters.getProtocols(), parameters.getCipherSuites(), new NoopHostnameVerifier()); + } + + private static Registry<ConnectionSocketFactory> createRegistry(SSLConnectionSocketFactory sslSocketFactory) { + return RegistryBuilder.<ConnectionSocketFactory>create() + .register("https", sslSocketFactory) + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .build(); + } + + + /** + * Reroutes requests using 'http' to 'https'. + * Implementation inspired by {@link org.apache.http.impl.conn.DefaultRoutePlanner}, but without proxy support. + */ + static class HttpToHttpsRoutePlanner implements HttpRoutePlanner { + + @Override + public HttpRoute determineRoute(HttpHost host, HttpRequest request, HttpContext context) throws HttpException { + HttpClientContext clientContext = HttpClientContext.adapt(context); + RequestConfig config = clientContext.getRequestConfig(); + InetAddress local = config.getLocalAddress(); + + HttpHost target = resolveTarget(host); + boolean secure = target.getSchemeName().equalsIgnoreCase("https"); + return new HttpRoute(target, local, secure); + } + + private HttpHost resolveTarget(HttpHost host) throws HttpException { + try { + String originalScheme = host.getSchemeName(); + String scheme = originalScheme.equalsIgnoreCase("http") ? "https" : originalScheme; + int port = DefaultSchemePortResolver.INSTANCE.resolve(host); + return new HttpHost(host.getHostName(), port, scheme); + } catch (UnsupportedSchemeException e) { + throw new HttpException(e.getMessage(), e); + } + } + } +} diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/package-info.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/package-info.java new file mode 100644 index 00000000000..823bfdcde4b --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package ai.vespa.util.http.hc4; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelaySupplier.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelaySupplier.java new file mode 100644 index 00000000000..b202966c412 --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelaySupplier.java @@ -0,0 +1,44 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc4.retry; + +import java.time.Duration; + +/** + * An abstraction that calculates the next delay based on the current retry count. + * + * @author bjorncs + */ +@FunctionalInterface +interface DelaySupplier { + Duration getDelay(int executionCount); + + class Fixed implements DelaySupplier { + private final Duration delay; + + Fixed(Duration delay) { + this.delay = delay; + } + + @Override + public Duration getDelay(int executionCount) { return delay; } + } + + class Exponential implements DelaySupplier { + private final Duration startDelay; + private final Duration maxDelay; + + Exponential(Duration startDelay, Duration maxDelay) { + this.startDelay = startDelay; + this.maxDelay = maxDelay; + } + + @Override + public Duration getDelay(int executionCount) { + Duration nextDelay = startDelay; + for (int i = 1; i < executionCount; ++i) { + nextDelay = nextDelay.multipliedBy(2); + } + return maxDelay.compareTo(nextDelay) > 0 ? nextDelay : maxDelay; + } + } +} diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedConnectionLevelRetryHandler.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedConnectionLevelRetryHandler.java new file mode 100644 index 00000000000..3ba92c08e30 --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedConnectionLevelRetryHandler.java @@ -0,0 +1,126 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc4.retry; + +import org.apache.http.annotation.Contract; +import org.apache.http.annotation.ThreadingBehavior; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; + +import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.function.Predicate; +import java.util.logging.Logger; + +/** + * A {@link HttpRequestRetryHandler} that supports delayed retries. + * + * @author bjorncs + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class DelayedConnectionLevelRetryHandler implements HttpRequestRetryHandler { + + private static final Logger log = Logger.getLogger(HttpRequestRetryHandler.class.getName()); + + private final DelaySupplier delaySupplier; + private final int maxRetries; + private final RetryPredicate<IOException> predicate; + private final RetryConsumer<IOException> retryConsumer; + private final RetryFailedConsumer<IOException> retryFailedConsumer; + private final Sleeper sleeper; + + private DelayedConnectionLevelRetryHandler( + DelaySupplier delaySupplier, + int maxRetries, + RetryPredicate<IOException> predicate, + RetryConsumer<IOException> retryConsumer, + RetryFailedConsumer<IOException> retryFailedConsumer, + Sleeper sleeper) { + this.delaySupplier = delaySupplier; + this.maxRetries = maxRetries; + this.predicate = predicate; + this.retryConsumer = retryConsumer; + this.retryFailedConsumer = retryFailedConsumer; + this.sleeper = sleeper; + } + + @Override + public boolean retryRequest(IOException exception, int executionCount, HttpContext ctx) { + log.fine(() -> String.format("retryRequest(exception='%s', executionCount='%d', ctx='%s'", + exception.getClass().getName(), executionCount, ctx)); + HttpClientContext clientCtx = HttpClientContext.adapt(ctx); + if (!predicate.test(exception, clientCtx)) { + log.fine(() -> String.format("Not retrying for '%s'", ctx)); + return false; + } + if (executionCount > maxRetries) { + log.fine(() -> String.format("Max retries exceeded for '%s'", ctx)); + retryFailedConsumer.onRetryFailed(exception, executionCount, clientCtx); + return false; + } + Duration delay = delaySupplier.getDelay(executionCount); + log.fine(() -> String.format("Retrying after %s for '%s'", delay, ctx)); + retryConsumer.onRetry(exception, delay, executionCount, clientCtx); + sleeper.sleep(delay); + return true; + } + + public static class Builder { + + private final DelaySupplier delaySupplier; + private final int maxRetries; + private RetryPredicate<IOException> predicate = (ioException, ctx) -> true; + private RetryConsumer<IOException> retryConsumer = (exception, delay, count, ctx) -> {}; + private RetryFailedConsumer<IOException> retryFailedConsumer = (exception, count, ctx) -> {}; + private Sleeper sleeper = new Sleeper.Default(); + + private Builder(DelaySupplier delaySupplier, int maxRetries) { + this.delaySupplier = delaySupplier; + this.maxRetries = maxRetries; + } + + public static Builder withFixedDelay(Duration delay, int maxRetries) { + return new Builder(new DelaySupplier.Fixed(delay), maxRetries); + } + + public static Builder withExponentialBackoff(Duration startDelay, Duration maxDelay, int maxRetries) { + return new Builder(new DelaySupplier.Exponential(startDelay, maxDelay), maxRetries); + } + + public Builder retryForExceptions(List<Class<? extends IOException>> exceptionTypes) { + this.predicate = (ioException, ctx) -> exceptionTypes.stream().anyMatch(type -> type.isInstance(ioException)); + return this; + } + + public Builder retryForExceptions(Predicate<IOException> predicate) { + this.predicate = (ioException, ctx) -> predicate.test(ioException); + return this; + } + + public Builder retryFor(RetryPredicate<IOException> predicate) { + this.predicate = predicate; + return this; + } + + public Builder onRetry(RetryConsumer<IOException> consumer) { + this.retryConsumer = consumer; + return this; + } + + public Builder onRetryFailed(RetryFailedConsumer<IOException> consumer) { + this.retryFailedConsumer = consumer; + return this; + } + + // For unit testing + Builder withSleeper(Sleeper sleeper) { + this.sleeper = sleeper; + return this; + } + + public DelayedConnectionLevelRetryHandler build() { + return new DelayedConnectionLevelRetryHandler(delaySupplier, maxRetries, predicate, retryConsumer, retryFailedConsumer, sleeper); + } + } +} diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedResponseLevelRetryHandler.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedResponseLevelRetryHandler.java new file mode 100644 index 00000000000..d4ceb44a3ab --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedResponseLevelRetryHandler.java @@ -0,0 +1,125 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc4.retry; + +import org.apache.http.HttpResponse; +import org.apache.http.annotation.Contract; +import org.apache.http.annotation.ThreadingBehavior; +import org.apache.http.client.ServiceUnavailableRetryStrategy; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; + +import java.time.Duration; +import java.util.List; +import java.util.function.Predicate; +import java.util.logging.Logger; + +/** + * A {@link ServiceUnavailableRetryStrategy} that supports delayed retries on any response types. + * + * @author bjorncs + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class DelayedResponseLevelRetryHandler implements ServiceUnavailableRetryStrategy { + + private static final Logger log = Logger.getLogger(DelayedResponseLevelRetryHandler.class.getName()); + + private final DelaySupplier delaySupplier; + private final int maxRetries; + private final RetryPredicate<HttpResponse> predicate; + private final RetryConsumer<HttpResponse> retryConsumer; + private final RetryFailedConsumer<HttpResponse> retryFailedConsumer; + private final ThreadLocal<Long> retryInterval = ThreadLocal.withInitial(() -> 0L); + + private DelayedResponseLevelRetryHandler( + DelaySupplier delaySupplier, + int maxRetries, + RetryPredicate<HttpResponse> predicate, + RetryConsumer<HttpResponse> retryConsumer, + RetryFailedConsumer<HttpResponse> retryFailedConsumer) { + + this.delaySupplier = delaySupplier; + this.maxRetries = maxRetries; + this.predicate = predicate; + this.retryConsumer = retryConsumer; + this.retryFailedConsumer = retryFailedConsumer; + } + + @Override + public boolean retryRequest(HttpResponse response, int executionCount, HttpContext ctx) { + log.fine(() -> String.format("retryRequest(responseCode='%s', executionCount='%d', ctx='%s'", + response.getStatusLine().getStatusCode(), executionCount, ctx)); + HttpClientContext clientCtx = HttpClientContext.adapt(ctx); + if (!predicate.test(response, clientCtx)) { + log.fine(() -> String.format("Not retrying for '%s'", ctx)); + return false; + } + if (executionCount > maxRetries) { + log.fine(() -> String.format("Max retries exceeded for '%s'", ctx)); + retryFailedConsumer.onRetryFailed(response, executionCount, clientCtx); + return false; + } + Duration delay = delaySupplier.getDelay(executionCount); + log.fine(() -> String.format("Retrying after %s for '%s'", delay, ctx)); + retryInterval.set(delay.toMillis()); + retryConsumer.onRetry(response, delay, executionCount, clientCtx); + return true; + } + + @Override + public long getRetryInterval() { + // Calls to getRetryInterval are always guarded by a call to retryRequest (using the same thread). + // A thread local allows this retry handler to be thread safe and support dynamic retry intervals + return retryInterval.get(); + } + + public static class Builder { + + private final DelaySupplier delaySupplier; + private final int maxRetries; + private RetryPredicate<HttpResponse> predicate = (response, ctx) -> true; + private RetryConsumer<HttpResponse> retryConsumer = (response, delay, count, ctx) -> {}; + private RetryFailedConsumer<HttpResponse> retryFailedConsumer = (response, count, ctx) -> {}; + + private Builder(DelaySupplier delaySupplier, int maxRetries) { + this.delaySupplier = delaySupplier; + this.maxRetries = maxRetries; + } + + public static Builder withFixedDelay(Duration delay, int maxRetries) { + return new Builder(new DelaySupplier.Fixed(delay), maxRetries); + } + + public static Builder withExponentialBackoff(Duration startDelay, Duration maxDelay, int maxRetries) { + return new Builder(new DelaySupplier.Exponential(startDelay, maxDelay), maxRetries); + } + + public Builder retryForStatusCodes(List<Integer> statusCodes) { + this.predicate = (response, ctx) -> statusCodes.contains(response.getStatusLine().getStatusCode()); + return this; + } + + public Builder retryForResponses(Predicate<HttpResponse> predicate) { + this.predicate = (response, ctx) -> predicate.test(response); + return this; + } + + public Builder retryFor(RetryPredicate<HttpResponse> predicate) { + this.predicate = predicate; + return this; + } + + public Builder onRetry(RetryConsumer<HttpResponse> consumer) { + this.retryConsumer = consumer; + return this; + } + + public Builder onRetryFailed(RetryFailedConsumer<HttpResponse> consumer) { + this.retryFailedConsumer = consumer; + return this; + } + + public DelayedResponseLevelRetryHandler build() { + return new DelayedResponseLevelRetryHandler(delaySupplier, maxRetries, predicate, retryConsumer, retryFailedConsumer); + } + } +} diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryConsumer.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryConsumer.java new file mode 100644 index 00000000000..c168f7d50c9 --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryConsumer.java @@ -0,0 +1,16 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc4.retry; + +import org.apache.http.client.protocol.HttpClientContext; + +import java.time.Duration; + +/** + * Invoked before performing a delay and retry. + * + * @author bjorncs + */ +@FunctionalInterface +public interface RetryConsumer<T> { + void onRetry(T data, Duration delay, int executionCount, HttpClientContext context); +} diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryFailedConsumer.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryFailedConsumer.java new file mode 100644 index 00000000000..801c8a5af2f --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryFailedConsumer.java @@ -0,0 +1,14 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc4.retry; + +import org.apache.http.client.protocol.HttpClientContext; + +/** + * Invoked after the last retry has failed. + * + * @author bjorncs + */ +@FunctionalInterface +public interface RetryFailedConsumer<T> { + void onRetryFailed(T response, int executionCount, HttpClientContext context); +} diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryPredicate.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryPredicate.java new file mode 100644 index 00000000000..45c5ef0d623 --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryPredicate.java @@ -0,0 +1,13 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc4.retry; + +import org.apache.http.client.protocol.HttpClientContext; + +import java.util.function.BiPredicate; + +/** + * A predicate that determines whether an operation should be retried. + * + * @author bjorncs + */ +public interface RetryPredicate<T> extends BiPredicate<T, HttpClientContext> {} diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/Sleeper.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/Sleeper.java new file mode 100644 index 00000000000..f593561888d --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/Sleeper.java @@ -0,0 +1,26 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.util.http.hc4.retry; + +import java.time.Duration; + +/** + * An abstraction used for mocking {@link Thread#sleep(long)} in unit tests. + * + * @author bjorncs + */ +public interface Sleeper { + void sleep(Duration duration); + + class Default implements Sleeper { + @Override + public void sleep(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + } +} + diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/package-info.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/package-info.java new file mode 100644 index 00000000000..f20247fe13e --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package ai.vespa.util.http.hc4.retry; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java b/vespajlib/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java new file mode 100644 index 00000000000..92cc35fc354 --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java @@ -0,0 +1,33 @@ +// 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.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") && ! target.getSchemeName().equals("https")) + throw new IllegalArgumentException("Scheme must be 'http' or 'https' when using HttpToHttpsRoutePlanner, was '" + target.getSchemeName() + "'"); + + 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/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java b/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java new file mode 100644 index 00000000000..50af29f92aa --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java @@ -0,0 +1,71 @@ +// 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.MixedMode; +import com.yahoo.security.tls.TlsContext; +import com.yahoo.security.tls.TransportSecurityUtils; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLParameters; + +/** + * Async http client builder for internal Vespa communications over http/https. + * Configures Vespa mTLS and handles TLS mixed mode automatically. + * Client should only be used for requests to Vespa services. + * + * Caveats: + * - custom connection manager must be configured through {@link #create(AsyncConnectionManagerFactory)}. + * + * @author bjorncs + */ +public class VespaAsyncHttpClientBuilder { + + public interface AsyncConnectionManagerFactory { + AsyncClientConnectionManager create(TlsStrategy tlsStrategy); + } + + public static HttpAsyncClientBuilder create() { + return create( + tlsStrategy -> PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(tlsStrategy) + .build()); + } + + public static HttpAsyncClientBuilder create(AsyncConnectionManagerFactory factory) { + return create(factory, new NoopHostnameVerifier()); + } + + public static HttpAsyncClientBuilder create(AsyncConnectionManagerFactory factory, HostnameVerifier hostnameVerifier) { + HttpAsyncClientBuilder clientBuilder = HttpAsyncClientBuilder.create(); + TlsContext vespaTlsContext = TransportSecurityUtils.getSystemTlsContext().orElse(null); + TlsStrategy tlsStrategy; + if (vespaTlsContext != null) { + SSLParameters vespaTlsParameters = vespaTlsContext.parameters(); + tlsStrategy = ClientTlsStrategyBuilder.create() + .setHostnameVerifier(hostnameVerifier) + .setSslContext(vespaTlsContext.context()) + .setTlsVersions(vespaTlsParameters.getProtocols()) + .setCiphers(vespaTlsParameters.getCipherSuites()) + .build(); + if (TransportSecurityUtils.getInsecureMixedMode() != MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER) { + clientBuilder.setRoutePlanner(new HttpToHttpsRoutePlanner()); + } + } else { + tlsStrategy = ClientTlsStrategyBuilder.create().build(); + } + clientBuilder.disableConnectionState(); // Share connections between subsequent requests + clientBuilder.disableCookieManagement(); + clientBuilder.disableAuthCaching(); + clientBuilder.disableRedirectHandling(); + clientBuilder.setConnectionManager(factory.create(tlsStrategy)); + clientBuilder.setConnectionManagerShared(false); + return clientBuilder; + } + +} diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java b/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java new file mode 100644 index 00000000000..e01d278ff38 --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java @@ -0,0 +1,93 @@ +// 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.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.HostnameVerifier; +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 <em>for internal Vespa communications over http/https.</em> + * + * 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<ConnectionSocketFactory> socketFactories); + } + + public static HttpClientBuilder create() { + return create(PoolingHttpClientConnectionManager::new); + } + + public static HttpClientBuilder create(HttpClientConnectionManagerFactory connectionManagerFactory) { + return create(connectionManagerFactory, new NoopHostnameVerifier()); + } + + public static HttpClientBuilder create(HttpClientConnectionManagerFactory connectionManagerFactory, + HostnameVerifier hostnameVerifier) { + return create(connectionManagerFactory, hostnameVerifier, true); + } + + public static HttpClientBuilder create(HttpClientConnectionManagerFactory connectionManagerFactory, + HostnameVerifier hostnameVerifier, + boolean rewriteHttpToHttps) { + HttpClientBuilder builder = HttpClientBuilder.create(); + addSslSocketFactory(builder, connectionManagerFactory, hostnameVerifier); + if (rewriteHttpToHttps) + 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, + HostnameVerifier hostnameVerifier) { + getSystemTlsContext().ifPresent(tlsContext -> { + SSLParameters parameters = tlsContext.parameters(); + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(tlsContext.context(), + parameters.getProtocols(), + parameters.getCipherSuites(), + hostnameVerifier); + 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<ConnectionSocketFactory> createRegistry(SSLConnectionSocketFactory sslSocketFactory) { + return RegistryBuilder.<ConnectionSocketFactory>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()); + } + +} diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc5/package-info.java b/vespajlib/src/main/java/ai/vespa/util/http/hc5/package-info.java new file mode 100644 index 00000000000..1deb7cd6afb --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/util/http/hc5/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package ai.vespa.util.http.hc5; + +import com.yahoo.osgi.annotation.ExportPackage; |