001/*
002 * (C) Copyright 2017-2018 Nuxeo (http://nuxeo.com/) and others.
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 *
016 * Contributors:
017 *     bdelbosc
018 */
019package org.nuxeo.elasticsearch.client;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.nio.file.Files;
024import java.nio.file.Paths;
025import java.security.GeneralSecurityException;
026import java.security.KeyStore;
027
028import javax.net.ssl.SSLContext;
029
030import org.apache.commons.lang3.ArrayUtils;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.apache.http.HttpHost;
035import org.apache.http.auth.AuthScope;
036import org.apache.http.auth.UsernamePasswordCredentials;
037import org.apache.http.impl.client.BasicCredentialsProvider;
038import org.apache.http.ssl.SSLContextBuilder;
039import org.apache.http.ssl.SSLContexts;
040import org.elasticsearch.client.RequestOptions;
041import org.elasticsearch.client.RestClient;
042import org.elasticsearch.client.RestClientBuilder;
043import org.elasticsearch.client.RestHighLevelClient;
044import org.elasticsearch.common.transport.TransportAddress;
045import org.elasticsearch.http.HttpServerTransport;
046import org.nuxeo.ecm.core.api.NuxeoException;
047import org.nuxeo.elasticsearch.api.ESClient;
048import org.nuxeo.elasticsearch.api.ESClientFactory;
049import org.nuxeo.elasticsearch.config.ElasticSearchClientConfig;
050import org.nuxeo.elasticsearch.core.ElasticSearchEmbeddedNode;
051import org.nuxeo.runtime.api.Framework;
052
053/**
054 * @since 9.3
055 */
056public class ESRestClientFactory implements ESClientFactory {
057    private static final Log log = LogFactory.getLog(ESRestClientFactory.class);
058
059    public static final String DEFAULT_CONNECT_TIMEOUT_MS = "5000";
060
061    public static final String DEFAULT_SOCKET_TIMEOUT_MS = "20000";
062
063    public static final String CONNECTION_TIMEOUT_MS_OPT = "connection.timeout.ms";
064
065    public static final String SOCKET_TIMEOUT_MS_OPT = "socket.timeout.ms";
066
067    public static final String AUTH_USER_OPT = "username";
068
069    public static final String AUTH_PASSWORD_OPT = "password";
070
071    /** @since 10.3 */
072    public static final String TRUST_STORE_PATH_OPT = "trustStorePath";
073
074    /** @since 10.3 */
075    public static final String TRUST_STORE_PASSWORD_OPT = "trustStorePassword";
076
077    /** @since 10.3 */
078    public static final String TRUST_STORE_TYPE_OPT = "trustStoreType";
079
080    /** @since 10.3 */
081    public static final String KEY_STORE_PATH_OPT = "keyStorePath";
082
083    /** @since 10.3 */
084    public static final String KEY_STORE_PASSWORD_OPT = "keyStorePassword";
085
086    /** @since 10.3 */
087    public static final String KEY_STORE_TYPE_OPT = "keyStoreType";
088
089    /** @deprecated since 10.3, misnamed, use {@link #TRUST_STORE_PATH_OPT} instead */
090    @Deprecated
091    public static final String DEPRECATED_TRUST_STORE_PATH_OPT = "keystore.path";
092
093    /** @deprecated since 10.3, misnamed, use {@link #TRUST_STORE_PATH_OPT} instead */
094    @Deprecated
095    public static final String DEPRECATED_TRUST_STORE_PASSWORD_OPT = "keystore.password";
096
097    /**
098     * @since 9.10-HF01
099     * @deprecated since 10.3, misnamed, use {@link #TRUST_STORE_PATH_OPT} instead
100     */
101    @Deprecated
102    public static final String DEPRECATED_TRUST_STORE_TYPE_OPT = "keystore.type";
103
104    @Override
105    public ESClient create(ElasticSearchEmbeddedNode node, ElasticSearchClientConfig config) {
106        if (node != null) {
107            return createLocalRestClient(node);
108        }
109        return createRestClient(config);
110    }
111
112    @SuppressWarnings("resource") // factory for ESClient / RestHighLevelClient
113    protected ESClient createLocalRestClient(ElasticSearchEmbeddedNode node) {
114        if (!node.getConfig().httpEnabled()) {
115            throw new IllegalArgumentException(
116                    "Embedded configuration has no HTTP port enable, use TransportClient instead of Rest");
117        }
118        HttpServerTransport http = node.getNode().injector().getInstance(HttpServerTransport.class);
119        TransportAddress[] addresses = http.boundAddress().boundAddresses();
120        if (ArrayUtils.isEmpty(addresses)) {
121            throw new IllegalStateException("Embedded node did not bind any address");
122        }
123        int port = addresses[0].getPort();
124        RestClientBuilder lowLevelRestClientBuilder = RestClient.builder(new HttpHost("localhost", port));
125        RestHighLevelClient client = new RestHighLevelClient(lowLevelRestClientBuilder); // NOSONAR (factory)
126        // checkConnection(client);
127        return new ESRestClient(client.getLowLevelClient(), client);
128    }
129
130    @SuppressWarnings("resource") // factory for ESClient / RestHighLevelClient
131    protected ESClient createRestClient(ElasticSearchClientConfig config) {
132        String addressList = config.getOption("addressList", "");
133        if (addressList.isEmpty()) {
134            throw new IllegalArgumentException("No addressList option provided cannot connect RestClient");
135        }
136        String[] hosts = addressList.split(",");
137        HttpHost[] httpHosts = new HttpHost[hosts.length];
138        int i = 0;
139        for (String host : hosts) {
140            httpHosts[i++] = HttpHost.create(host);
141        }
142        RestClientBuilder builder = RestClient.builder(httpHosts)
143                                              .setRequestConfigCallback(
144                                                      requestConfigBuilder -> requestConfigBuilder.setConnectTimeout(
145                                                              getConnectTimeoutMs(config))
146                                                                                                  .setSocketTimeout(
147                                                                                                          getSocketTimeoutMs(
148                                                                                                                  config)));
149        addClientCallback(config, builder);
150        RestHighLevelClient client = new RestHighLevelClient(builder); // NOSONAR (factory)
151        // checkConnection(client);
152        return new ESRestClient(client.getLowLevelClient(), client);
153    }
154
155    private void addClientCallback(ElasticSearchClientConfig config, RestClientBuilder builder) {
156        BasicCredentialsProvider credentialProvider = getCredentialProvider(config);
157        SSLContext sslContext = getSslContext(config);
158        if (sslContext == null && credentialProvider == null) {
159            return;
160        }
161        builder.setHttpClientConfigCallback(httpClientBuilder -> {
162            httpClientBuilder.setSSLContext(sslContext);
163            httpClientBuilder.setDefaultCredentialsProvider(credentialProvider);
164            return httpClientBuilder;
165        });
166    }
167
168    protected BasicCredentialsProvider getCredentialProvider(ElasticSearchClientConfig config) {
169        if (StringUtils.isBlank(config.getOption(AUTH_USER_OPT))) {
170            return null;
171        }
172        String user = config.getOption(AUTH_USER_OPT);
173        String password = config.getOption(AUTH_PASSWORD_OPT);
174        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
175        credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(user, password));
176        return credentialsProvider;
177    }
178
179    protected SSLContext getSslContext(ElasticSearchClientConfig config) {
180        checkDeprecatedProperties();
181        String trustStorePath = StringUtils.defaultIfBlank(config.getOption(TRUST_STORE_PATH_OPT),
182                config.getOption(DEPRECATED_TRUST_STORE_PATH_OPT));
183        String trustStorePassword = StringUtils.defaultIfBlank(config.getOption(TRUST_STORE_PASSWORD_OPT),
184                config.getOption(DEPRECATED_TRUST_STORE_PASSWORD_OPT));
185        String trustStoreType = StringUtils.defaultIfBlank(config.getOption(TRUST_STORE_TYPE_OPT),
186                config.getOption(DEPRECATED_TRUST_STORE_TYPE_OPT));
187        String keyStorePath = config.getOption(KEY_STORE_PATH_OPT);
188        String keyStorePassword = config.getOption(KEY_STORE_PASSWORD_OPT);
189        String keyStoreType = config.getOption(KEY_STORE_TYPE_OPT);
190        try {
191            KeyStore trustStore = loadKeyStore(trustStorePath, trustStorePassword, trustStoreType);
192            KeyStore keyStore = loadKeyStore(keyStorePath, keyStorePassword, keyStoreType);
193            if (trustStore == null && keyStore == null) {
194                return null;
195            }
196            SSLContextBuilder sslContextBuilder = SSLContexts.custom();
197            if (trustStore != null) {
198                sslContextBuilder.loadTrustMaterial(trustStore, null);
199            }
200            if (keyStore != null) {
201                sslContextBuilder.loadKeyMaterial(keyStore,
202                        StringUtils.isBlank(keyStorePassword) ? null : keyStorePassword.toCharArray());
203            }
204            return sslContextBuilder.build();
205        } catch (GeneralSecurityException | IOException e) {
206            throw new NuxeoException("Cannot setup SSL for RestClient: " + config, e);
207        }
208    }
209
210    // deprecated and new system properties, used in warnings only
211    // actual values are used in templates and accessed through the options map of ElasticSearchClientConfig
212
213    protected static final String DEPRECATED_ES_TRUST_STORE_PATH_PROP = "elasticsearch.restClient.keystorePath";
214
215    protected static final String DEPRECATED_ES_TRUST_STORE_PASSWORD_PROP = "elasticsearch.restClient.keystorePassword";
216
217    protected static final String DEPRECATED_ES_TRUST_STORE_TYPE_PROP = "elasticsearch.restClient.keystoreType";
218
219    protected static final String ES_TRUST_STORE_PATH_PROP = "elasticsearch.restClient.truststore.path";
220
221    protected static final String ES_TRUST_STORE_PASSWORD_PROP = "elasticsearch.restClient.truststore.password";
222
223    protected static final String ES_TRUST_STORE_TYPE_PROP = "elasticsearch.restClient.truststore.type";
224
225    protected void checkDeprecatedProperties() {
226        checkDeprecatedProperty(DEPRECATED_ES_TRUST_STORE_PATH_PROP, ES_TRUST_STORE_PATH_PROP);
227        checkDeprecatedProperty(DEPRECATED_ES_TRUST_STORE_PASSWORD_PROP, ES_TRUST_STORE_PASSWORD_PROP);
228        checkDeprecatedProperty(DEPRECATED_ES_TRUST_STORE_TYPE_PROP, ES_TRUST_STORE_TYPE_PROP);
229    }
230
231    protected void checkDeprecatedProperty(String oldProp, String newProp) {
232        if (Framework.getRuntime() == null) {
233            // unit tests
234            return;
235        }
236        if (StringUtils.isNotBlank(Framework.getProperty(oldProp))) {
237            log.warn("Configuration property " + oldProp + " is deprecated, use " + newProp + " instead");
238        }
239    }
240
241    protected KeyStore loadKeyStore(String path, String password, String type)
242            throws GeneralSecurityException, IOException {
243        if (StringUtils.isBlank(path)) {
244            return null;
245        }
246        String keyStoreType = StringUtils.defaultIfBlank(type, KeyStore.getDefaultType());
247        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
248        char[] passwordChars = StringUtils.isBlank(password) ? null : password.toCharArray();
249        try (InputStream is = Files.newInputStream(Paths.get(path))) {
250            keyStore.load(is, passwordChars);
251        }
252        return keyStore;
253    }
254
255    protected int getConnectTimeoutMs(ElasticSearchClientConfig config) {
256        return Integer.parseInt(config.getOption(CONNECTION_TIMEOUT_MS_OPT, DEFAULT_CONNECT_TIMEOUT_MS));
257    }
258
259    protected int getSocketTimeoutMs(ElasticSearchClientConfig config) {
260        return Integer.parseInt(config.getOption(SOCKET_TIMEOUT_MS_OPT, DEFAULT_SOCKET_TIMEOUT_MS));
261    }
262
263    protected void checkConnection(RestHighLevelClient client) {
264        boolean ping = false;
265        try {
266            ping = client.ping(RequestOptions.DEFAULT);
267        } catch (IOException e) {
268            log.error(e.getMessage(), e);
269        }
270        if (!ping) {
271            throw new IllegalStateException("Fail to ping rest node");
272        }
273    }
274}