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}