spring-cloud / spring-cloud-vault

Configuration Integration with HashiCorp Vault
http://cloud.spring.io/spring-cloud-vault/
Apache License 2.0
270 stars 151 forks source link

httpClientBuilder.setConnectionTimeToLive for org.springframework.vault.client.ClientHttpRequestFactoryFactory #660

Closed spectateur closed 1 year ago

spectateur commented 1 year ago

Hello,

This is related to https://github.com/spring-cloud/spring-cloud-vault/issues/659 We also created an issue with apache httpclient : https://issues.apache.org/jira/projects/HTTPCLIENT/issues/HTTPCLIENT-2235

They answered that "You should either define a max lifetime for the connections or enable the stale check. Closed/dropped connections on one side a pretty common."

But the max lifetime is a method called setConnectionTimeToLive in org.apache.http.impl.client.HttpClientBuilder

public final HttpClientBuilder setConnectionTimeToLive(long connTimeToLive, TimeUnit connTimeToLiveTimeUnit) {
    this.connTimeToLive = connTimeToLive;
    this.connTimeToLiveTimeUnit = connTimeToLiveTimeUnit;
    return this;
  }

which is never used in the framework here org.springframework.vault.client.ClientHttpRequestFactoryFactory.class:279 for Class HttpComponents

@mp911de could you please share what are your thoughts about how to set the timeout with ClientOptions as you shared previously ? From clientopptions.class I do not see any connTimeToLive value only connectionTimeout and readTimeout

Here as a suggestion does it make sens to include method setConnectionTimeToLive from org.apache.http.impl.client.HttpClientBuilder in org.springframework.vault.client.ClientHttpRequestFactoryFactory which is called by org.springframework.cloud.vault.config.VaultBootstrapConfiguration

Something like this : httpClientBuilder.setConnectionTimeToLive(CON_TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);

static class HttpComponents {
    static ClientHttpRequestFactory usingHttpComponents(ClientOptions options, SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException {
      HttpClientBuilder httpClientBuilder = HttpClients.custom();
      httpClientBuilder.setConnectionTimeToLive(CON_TIME_OUT_MILLIS, TimeUnit.MILLISECONDS);
      httpClientBuilder.setRoutePlanner((HttpRoutePlanner)new SystemDefaultRoutePlanner((SchemePortResolver)DefaultSchemePortResolver.INSTANCE, 
            ProxySelector.getDefault()));
      if (ClientHttpRequestFactoryFactory.hasSslConfiguration(sslConfiguration)) {
        SSLContext sslContext = ClientHttpRequestFactoryFactory.getSSLContext(sslConfiguration, ClientHttpRequestFactoryFactory
            .getTrustManagers(sslConfiguration));
        SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
        httpClientBuilder.setSSLSocketFactory((LayeredConnectionSocketFactory)sslSocketFactory);
        httpClientBuilder.setSSLContext(sslContext);
      } 
      RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(Math.toIntExact(options.getConnectionTimeout().toMillis())).setSocketTimeout(Math.toIntExact(options.getReadTimeout().toMillis())).setAuthenticationEnabled(true).build();
      httpClientBuilder.setDefaultRequestConfig(requestConfig);
      httpClientBuilder.setRedirectStrategy((RedirectStrategy)new LaxRedirectStrategy());
      return (ClientHttpRequestFactory)new HttpComponentsClientHttpRequestFactory((HttpClient)httpClientBuilder.build());
    }
  }
mp911de commented 1 year ago

We support with ClientOptions a common subset of options that are available across all clients. We do not have the possibility to reference client-specific types from ClientOptions as each client is only optional from a class path availability perspective.

If you require a more elaborate configuration then creating a configuration utility in your code would make sense.

juriohacc commented 1 year ago

Hello,

@mp911de , I would like to have your advice about how implement the apache client the other parameters (max ttl connections) in Spring Cloud Vault. We need to integrate this configuration in the way we are not breaking the orchestration of Spring Vault to handle the renew lease token (from the class SecretLeaseContainer which is used to request Vault).

Here the ClientHttpRequestFactory custom class to use custom client http request factory :

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.vault.config.AbstractVaultConfiguration;

@Component
@Primary
public class ClientHttpRequestFactoryCustom extends AbstractVaultConfiguration.ClientFactoryWrapper {

   public ClientHttpRequestFactoryCustom(VaultConfiguration configuration) {
      super(configuration.createClientHttpRequestFactory());
}

The VaultConfiguration class is final and cannot be extended or reference from my project. So i have created my implementation.

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.vault.config.VaultProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.StringUtils;
import org.springframework.vault.authentication.*;
import org.springframework.vault.client.*;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.core.lease.SecretLeaseContainer;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.SslConfiguration;
import org.springframework.web.client.RestTemplate;

import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.function.Supplier;

@Configuration
public class VaultConfigurationCustom {
    private final VaultProperties vaultProperties;

    VaultConfigurationCustom(VaultProperties vaultProperties) {
        this.vaultProperties = vaultProperties;
    }
..
..
..

    ClientHttpRequestFactory createClientHttpRequestFactory() {
        ClientOptions clientOptions = new ClientOptions(Duration.ofMillis((long)this.vaultProperties.getConnectionTimeout()), Duration.ofMillis((long)this.vaultProperties.getReadTimeout()));
        SslConfiguration sslConfiguration = createSslConfiguration(this.vaultProperties.getSsl());
        return ClientHttpRequestFactoryFactoryCustom.create(clientOptions, sslConfiguration);
    }

..
..
..
}

Here the class which is used to change the http client builder (the class ClientHttpRequestFactoryFactory have many static and private methods which cannot be used)


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.conn.DefaultSchemePortResolver;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.PemObject;
import org.springframework.vault.support.SslConfiguration;

import javax.net.ssl.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.ProxySelector;
import java.net.Socket;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class ClientHttpRequestFactoryFactoryCustom {

    private static Log logger = LogFactory.getLog(ClientHttpRequestFactoryFactoryCustom.class);
    private static final boolean HTTP_COMPONENTS_PRESENT = isPresent("org.apache.http.client.HttpClient");
    private static final boolean OKHTTP3_PRESENT = isPresent("okhttp3.OkHttpClient");
    private static final boolean NETTY_PRESENT = isPresent("io.netty.channel.nio.NioEventLoopGroup", "io.netty.handler.ssl.SslContext", "io.netty.handler.codec.http.HttpClientCodec");

..
..
   static class HttpComponents {
        HttpComponents() {
        }

        static ClientHttpRequestFactory usingHttpComponents(ClientOptions options, SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException {
            HttpClientBuilder httpClientBuilder = HttpClients.custom();

            // Set the connection max TTL (100 seconds for example)
            httpClientBuilder = httpClientBuilder.setConnectionTimeToLive(100L, TimeUnit.SECONDS);

           // Enable expired connection mode
            httpClientBuilder = httpClientBuilder.evictExpiredConnections();

            httpClientBuilder.setRoutePlanner(new SystemDefaultRoutePlanner(DefaultSchemePortResolver.INSTANCE, ProxySelector.getDefault()));
            if (ClientHttpRequestFactoryFactoryCustom.hasSslConfiguration(sslConfiguration)) {
                SSLContext sslContext = ClientHttpRequestFactoryFactoryCustom.getSSLContext(sslConfiguration, ClientHttpRequestFactoryFactoryCustom.getTrustManagers(sslConfiguration));
                String[] enabledProtocols = null;
                if (!sslConfiguration.getEnabledProtocols().isEmpty()) {
                    enabledProtocols = (String[])sslConfiguration.getEnabledProtocols().toArray(new String[0]);
                }

                String[] enabledCipherSuites = null;
                if (!sslConfiguration.getEnabledCipherSuites().isEmpty()) {
                    enabledCipherSuites = (String[])sslConfiguration.getEnabledCipherSuites().toArray(new String[0]);
                }

                SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, enabledProtocols, enabledCipherSuites, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
                httpClientBuilder.setSSLSocketFactory(sslSocketFactory);
                httpClientBuilder.setSSLContext(sslContext);
            }

            RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(Math.toIntExact(options.getConnectionTimeout().toMillis())).setSocketTimeout(Math.toIntExact(options.getReadTimeout().toMillis())).setAuthenticationEnabled(true).build();
            httpClientBuilder.setDefaultRequestConfig(requestConfig);
            httpClientBuilder.setRedirectStrategy(new LaxRedirectStrategy());

            return new HttpComponentsClientHttpRequestFactory(httpClientBuilder.build());
        }
    }
..
..

}

Do you have any recommandations to avoid too many duplicated code Spring Vault in our codes ?

mp911de commented 1 year ago

Copying code into your project is the only approach for now.