001/**
002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.components.vault.client;
017
018import static java.util.Optional.ofNullable;
019import static java.util.concurrent.TimeUnit.MILLISECONDS;
020
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.security.KeyManagementException;
025import java.security.KeyStore;
026import java.security.KeyStoreException;
027import java.security.NoSuchAlgorithmException;
028import java.security.cert.CertificateException;
029import java.security.cert.X509Certificate;
030import java.util.List;
031import java.util.Objects;
032import java.util.Optional;
033import java.util.concurrent.ExecutorService;
034import java.util.concurrent.LinkedBlockingQueue;
035import java.util.concurrent.ThreadFactory;
036import java.util.concurrent.ThreadPoolExecutor;
037import java.util.concurrent.atomic.AtomicInteger;
038import java.util.stream.Stream;
039
040import javax.enterprise.context.ApplicationScoped;
041import javax.enterprise.inject.Disposes;
042import javax.enterprise.inject.Produces;
043import javax.inject.Inject;
044import javax.net.ssl.SSLContext;
045import javax.net.ssl.TrustManager;
046import javax.net.ssl.TrustManagerFactory;
047import javax.net.ssl.X509TrustManager;
048import javax.ws.rs.client.Client;
049import javax.ws.rs.client.ClientBuilder;
050import javax.ws.rs.client.WebTarget;
051
052import org.eclipse.microprofile.config.inject.ConfigProperty;
053import org.talend.sdk.components.vault.configuration.Documentation;
054
055import lombok.Data;
056import lombok.extern.slf4j.Slf4j;
057
058@Slf4j
059@Data
060@ApplicationScoped
061public class VaultClientSetup {
062
063    @Inject
064    @Documentation("HTTP connection timeout to vault server.")
065    @ConfigProperty(name = "talend.vault.cache.client.timeout.connect", defaultValue = "30000")
066    private Long connectTimeout;
067
068    @Inject
069    @Documentation("HTTP read timeout to vault server.")
070    @ConfigProperty(name = "talend.vault.cache.client.timeout.read", defaultValue = "30000")
071    private Long readTimeout;
072
073    @Inject
074    @Documentation("JAX-RS fully qualified name of the provides (message body readers/writers) for vault and component-server clients.")
075    @ConfigProperty(name = "talend.vault.cache.client.providers")
076    private Optional<String> providers;
077
078    @Inject
079    @Documentation("Should any certificate be accepted - only for dev purposes.")
080    @ConfigProperty(name = "talend.vault.cache.client.certificate.acceptAny", defaultValue = "false")
081    private Boolean acceptAnyCertificate;
082
083    @Inject
084    @Documentation("Where the keystore to use to connect to vault is located.")
085    @ConfigProperty(name = "talend.vault.cache.client.vault.certificate.keystore.location")
086    private Optional<String> vaultKeystoreLocation;
087
088    @Inject
089    @Documentation("The keystore type for `talend.vault.cache.client.vault.certificate.keystore.location`.")
090    @ConfigProperty(name = "talend.vault.cache.client.vault.certificate.keystore.type")
091    private Optional<String> vaultKeystoreType;
092
093    @Inject
094    @Documentation("The keystore password for `talend.vault.cache.client.vault.certificate.keystore.location`.")
095    @ConfigProperty(name = "talend.vault.cache.client.vault.certificate.keystore.password", defaultValue = "changeit")
096    private String vaultKeystorePassword;
097
098    @Inject
099    @Documentation("Valid hostnames for the Vault certificates (see `java.net.ssl.HostnameVerifier`).")
100    @ConfigProperty(name = "talend.vault.cache.client.vault.hostname.accepted",
101            defaultValue = "localhost,127.0.0.1,0:0:0:0:0:0:0:1")
102    private List<String> vaultHostnames;
103
104    @Inject
105    @Documentation("The truststore type for `talend.vault.cache.client.vault.certificate.keystore.location`.")
106    @ConfigProperty(name = "talend.vault.cache.client.vault.certificate.truststore.type")
107    private Optional<String> vaultTruststoreType;
108
109    @Inject
110    @Documentation("Thread pool max size for Vault client.")
111    @ConfigProperty(name = "talend.vault.cache.client.executor.vault.max", defaultValue = "256")
112    private Integer vaultExecutorMaxSize;
113
114    @Inject
115    @Documentation("Thread pool core size for Vault client.")
116    @ConfigProperty(name = "talend.vault.cache.client.executor.vault.core", defaultValue = "64")
117    private Integer vaultExecutorCoreSize;
118
119    @Inject
120    @Documentation("Thread keep alive (in ms) for Vault client thread pool.")
121    @ConfigProperty(name = "talend.vault.cache.client.executor.vault.keepAlive", defaultValue = "60000")
122    private Integer vaultExecutorKeepAlive;
123
124    @Inject
125    @Documentation("Base URL to connect to Vault.")
126    @ConfigProperty(name = "talend.vault.cache.vault.url", defaultValue = "no-vault")
127    private String vaultUrl;
128
129    @Produces
130    @ApplicationScoped
131    @VaultHttp
132    public WebTarget vaultTarget(@VaultHttp final Client client) {
133        return client.target(vaultUrl);
134    }
135
136    @Produces
137    @ApplicationScoped
138    @VaultHttp
139    public ExecutorService vaultExecutorService() {
140        return createExecutor(vaultExecutorCoreSize, vaultExecutorMaxSize, vaultExecutorKeepAlive, "vault");
141    }
142
143    @VaultHttp
144    public void releaseVaultExecutor(@Disposes @VaultHttp final ExecutorService executorService) {
145        executorService.shutdownNow();
146    }
147
148    @Produces
149    @ApplicationScoped
150    @VaultHttp
151    public Client vaultClient(@VaultHttp final ExecutorService executor) {
152        return createClient(executor, vaultKeystoreLocation, vaultKeystoreType, vaultKeystorePassword,
153                vaultTruststoreType, vaultHostnames).build();
154    }
155
156    @VaultHttp
157    public void releaseVaultClient(@Disposes @VaultHttp final Client client) {
158        client.close();
159    }
160
161    private ThreadPoolExecutor createExecutor(final int core, final int max, final long keepAlive,
162            final String nameMarker) {
163        return new ThreadPoolExecutor(core, max, keepAlive, MILLISECONDS, new LinkedBlockingQueue<>(),
164                new ThreadFactory() {
165
166                    private final ThreadGroup group = ofNullable(System.getSecurityManager())
167                            .map(SecurityManager::getThreadGroup)
168                            .orElseGet(() -> Thread.currentThread().getThreadGroup());
169
170                    private final AtomicInteger threadNumber = new AtomicInteger(1);
171
172                    @Override
173                    public Thread newThread(final Runnable r) {
174                        final Thread t = new Thread(group, r,
175                                "talend-vault-proxy-" + nameMarker + "-" + threadNumber.getAndIncrement(), 0);
176                        if (t.isDaemon()) {
177                            t.setDaemon(false);
178                        }
179                        if (t.getPriority() != Thread.NORM_PRIORITY) {
180                            t.setPriority(Thread.NORM_PRIORITY);
181                        }
182                        return t;
183                    }
184                });
185    }
186
187    private ClientBuilder createClient(final ExecutorService executor, final Optional<String> keystoreLocation,
188            final Optional<String> keystoreType, final String keystorePassword, final Optional<String> truststoreType,
189            final List<String> serverHostnames) {
190        final ClientBuilder builder = ClientBuilder.newBuilder();
191        builder.connectTimeout(connectTimeout, MILLISECONDS);
192        builder.readTimeout(readTimeout, MILLISECONDS);
193        builder.executorService(executor);
194        if (acceptAnyCertificate) {
195            builder.hostnameVerifier((host, session) -> true);
196            builder.sslContext(createUnsafeSSLContext());
197        } else if (keystoreLocation.isPresent()) {
198            builder.hostnameVerifier((host, session) -> serverHostnames.contains(host));
199            builder.sslContext(createSSLContext(keystoreLocation, keystoreType, keystorePassword, truststoreType));
200        }
201        providers.map(it -> Stream.of(it.split(",")).map(String::trim).filter(v -> !v.isEmpty()).map(fqn -> {
202            try {
203                return Thread.currentThread().getContextClassLoader().loadClass(fqn).getConstructor().newInstance();
204            } catch (final Exception e) {
205                log.warn("Can't add provider " + fqn + ": " + e.getMessage(), e);
206                return null;
207            }
208        }).filter(Objects::nonNull)).ifPresent(it -> it.forEach(builder::register));
209        return builder;
210    }
211
212    private SSLContext createUnsafeSSLContext() {
213        final TrustManager[] trustManagers = { new X509TrustManager() {
214
215            @Override
216            public void checkClientTrusted(final X509Certificate[] x509Certificates, final String s) {
217                // no-op
218            }
219
220            @Override
221            public void checkServerTrusted(final X509Certificate[] x509Certificates, final String s) {
222                // no-op
223            }
224
225            @Override
226            public X509Certificate[] getAcceptedIssuers() {
227                return null;
228            }
229        } };
230        try {
231            final SSLContext sslContext = SSLContext.getInstance("TLS");
232            sslContext.init(null, trustManagers, new java.security.SecureRandom());
233            return sslContext;
234        } catch (final NoSuchAlgorithmException | KeyManagementException e) {
235            throw new IllegalStateException(e);
236        }
237    }
238
239    private SSLContext createSSLContext(final Optional<String> keystoreLocation, final Optional<String> keystoreType,
240            final String keystorePassword, final Optional<String> truststoreType) {
241        final File source = new File(keystoreLocation.orElseThrow(IllegalArgumentException::new));
242        if (!source.exists()) {
243            throw new IllegalArgumentException(source + " does not exist");
244        }
245        final KeyStore keyStore;
246        try (final FileInputStream stream = new FileInputStream(source)) {
247            keyStore = KeyStore.getInstance(keystoreType.orElseGet(KeyStore::getDefaultType));
248            keyStore.load(stream, keystorePassword.toCharArray());
249        } catch (final KeyStoreException | NoSuchAlgorithmException e) {
250            throw new IllegalStateException(e);
251        } catch (final CertificateException | IOException e) {
252            throw new IllegalArgumentException(e);
253        }
254        try {
255            final TrustManagerFactory trustManagerFactory =
256                    TrustManagerFactory.getInstance(truststoreType.orElseGet(TrustManagerFactory::getDefaultAlgorithm));
257            trustManagerFactory.init(keyStore);
258            final SSLContext sslContext = SSLContext.getInstance("TLS");
259            sslContext.init(null, trustManagerFactory.getTrustManagers(), new java.security.SecureRandom());
260            return sslContext;
261        } catch (final KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
262            throw new IllegalStateException(e);
263        }
264    }
265}