/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.solr.factory.impl; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Paths; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import javax.net.ssl.SSLContext; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.methods.HttpHead; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLContexts; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.client.solrj.request.CoreAdminRequest; import org.apache.solr.client.solrj.response.CoreAdminResponse; import org.codice.ddf.configuration.SystemBaseUrl; import org.codice.solr.factory.SolrClientFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.jodah.failsafe.Failsafe; import net.jodah.failsafe.RetryPolicy; /** * Factory class used to create new {@link HttpSolrClient} instances. * <br/> * Uses the following system properties when creating an instance: * <ul> * <li>solr.data.dir: Absolute path to the directory where the Solr data will be stored</li> * <li>solr.http.url: Solr server URL</li> * <li>org.codice.ddf.system.threadPoolSize: Solr query thread pool size</li> * <li>https.protocols: Secure protocols supported by the Solr server</li> * <li>https.cipherSuites: Cipher suites supported by the Solr server</li> * </ul> */ public class HttpSolrClientFactory implements SolrClientFactory { public static final List<String> DEFAULT_PROTOCOLS = Collections.unmodifiableList(Arrays.asList( StringUtils.split(System.getProperty("https.protocols", ""), ","))); public static final List<String> DEFAULT_CIPHER_SUITES = Collections.unmodifiableList(Arrays.asList(StringUtils.split(System.getProperty( "https.cipherSuites", ""), ","))); public static final String DEFAULT_SCHEMA_XML = "schema.xml"; public static final String DEFAULT_SOLRCONFIG_XML = "solrconfig.xml"; private static final Logger LOGGER = LoggerFactory.getLogger(HttpSolrClientFactory.class); private static ScheduledExecutorService executor = getThreadPool(); private static final String DEFAULT_CORE_NAME = "core1"; private static final String THREAD_POOL_DEFAULT_SIZE = "128"; @Override public Future<SolrClient> newClient(String core) { String solrUrl = System.getProperty("solr.http.url", getDefaultHttpsAddress()); return getHttpSolrClient(solrUrl, core); } /** * Gets the default Solr server secure HTTP address. * * @return Solr server secure HTTP address */ public static String getDefaultHttpsAddress() { return SystemBaseUrl.constructUrl("https", "/solr"); } /** * Gets the default Solr server HTTP address. * * @return Solr server HTTP address */ public static String getDefaultHttpAddress() { return SystemBaseUrl.constructUrl("http", "/solr"); } /** * Creates a new {@link HttpSolrClient} using the URL provided. Uses the default Solr core name * ({@value #DEFAULT_CORE_NAME}) and configuration file ({@value #DEFAULT_SOLRCONFIG_XML}). * * @param url Solr server URL. If {@code null}, defaults to the system's base URL followed by * {@code /solr}. * @return {@code Future} used to retrieve the new {@link HttpSolrClient} instance */ public static Future<SolrClient> getHttpSolrClient(@Nullable String url) { return getHttpSolrClient(url, DEFAULT_CORE_NAME, null); } /** * Creates a new {@link HttpSolrClient} using the URL and core name provided. Uses the * default configuration file ({@value #DEFAULT_SOLRCONFIG_XML}). * * @param url Solr server URL. If {@code null}, defaults to the system's base URL followed * by {@code /solr}. * @param coreName name of the Solr core to create * @return {@code Future} used to retrieve the new {@link HttpSolrClient} instance */ public static Future<SolrClient> getHttpSolrClient(@Nullable String url, String coreName) { return getHttpSolrClient(url, coreName, null); } /** * Creates a new {@link HttpSolrClient} using the URL, core name and configuration file name * provided. * * @param url Solr server URL. If {@code null}, defaults to the system's base URL * followed by {@code /solr}. * @param coreName name of the Solr core to create * @param configFile configuration file name. If {@code null}, defaults to * {@value #DEFAULT_SOLRCONFIG_XML}. * @return {@code Future} used to retrieve the new {@link HttpSolrClient} instance */ public static Future<SolrClient> getHttpSolrClient(@Nullable String url, String coreName, @Nullable String configFile) { String solrUrl = StringUtils.defaultIfBlank(url, SystemBaseUrl.constructUrl("/solr")); String coreUrl = url + "/" + coreName; if (System.getProperty("solr.data.dir") != null) { ConfigurationStore.getInstance().setDataDirectoryPath(System.getProperty("solr.data.dir")); } RetryPolicy retryPolicy = new RetryPolicy().withBackoff(10, TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS); return Failsafe.with(retryPolicy) .with(executor) .onRetry((c, failure, ctx) -> LOGGER.debug( "Attempt {} failed to create HTTP Solr client ({}). Retrying again.", ctx.getExecutions(), coreName)) .onFailedAttempt(failure -> LOGGER.debug( "Attempt failed to create HTTP Solr client (" + coreName + ")", failure)) .onSuccess(client -> LOGGER.debug("Successfully created HTTP Solr client ({})", coreName)) .onFailure(failure -> LOGGER.warn( "All attempts failed to create HTTP Solr client (" + coreName + ")", failure)) .get(() -> createSolrHttpClient(solrUrl, coreName, configFile, coreUrl)); } static SolrClient getHttpSolrClient() { return new HttpSolrClient(getDefaultHttpAddress()); } private static ScheduledExecutorService getThreadPool() throws NumberFormatException { Integer threadPoolSize = Integer.parseInt(System.getProperty( "org.codice.ddf.system.threadPoolSize", THREAD_POOL_DEFAULT_SIZE)); return Executors.newScheduledThreadPool(threadPoolSize); } private static SolrClient createSolrHttpClient(String url, String coreName, String configFile, String coreUrl) throws IOException, SolrServerException { SolrClient client; if (StringUtils.startsWith(url, "https")) { createSolrCore(url, coreName, configFile, getSecureHttpClient(false)); client = new HttpSolrClient(coreUrl, getSecureHttpClient(true)); } else { createSolrCore(url, coreName, configFile, null); client = new HttpSolrClient(coreUrl); } return client; } private static CloseableHttpClient getSecureHttpClient(boolean retryRequestsOnError) { SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory( getSslContext(), getProtocols(), getCipherSuites(), SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); HttpRequestRetryHandler solrRetryHandler = new SolrHttpRequestRetryHandler(); HttpClientBuilder builder = HttpClients.custom() .setSSLSocketFactory(sslConnectionSocketFactory) .setDefaultCookieStore(new BasicCookieStore()) .setMaxConnTotal(128) .setMaxConnPerRoute(32); if (retryRequestsOnError) { builder.setRetryHandler(solrRetryHandler); } return builder.build(); } private static String[] getProtocols() { if (System.getProperty("https.protocols") != null) { return StringUtils.split(System.getProperty("https.protocols"), ","); } else { return DEFAULT_PROTOCOLS.toArray(new String[DEFAULT_PROTOCOLS.size()]); } } private static String[] getCipherSuites() { if (System.getProperty("https.cipherSuites") != null) { return StringUtils.split(System.getProperty("https.cipherSuites"), ","); } else { return DEFAULT_CIPHER_SUITES.toArray(new String[DEFAULT_CIPHER_SUITES.size()]); } } private static SSLContext getSslContext() { if (System.getProperty("javax.net.ssl.keyStore") == null || // System.getProperty("javax.net.ssl.keyStorePassword") == null || // System.getProperty("javax.net.ssl.trustStore") == null || // System.getProperty("javax.net.ssl.trustStorePassword") == null) { throw new IllegalArgumentException( "KeyStore and TrustStore system properties must be set."); } KeyStore trustStore = getKeyStore(System.getProperty("javax.net.ssl.trustStore"), System.getProperty("javax.net.ssl.trustStorePassword")); KeyStore keyStore = getKeyStore(System.getProperty("javax.net.ssl.keyStore"), System.getProperty("javax.net.ssl.keyStorePassword")); SSLContext sslContext = null; try { sslContext = SSLContexts.custom() .loadKeyMaterial(keyStore, System.getProperty("javax.net.ssl.keyStorePassword") .toCharArray()) .loadTrustMaterial(trustStore) .useTLS() .build(); } catch (UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { throw new IllegalArgumentException( "Unable to use javax.net.ssl.keyStorePassword to load key material to create SSL context for Solr client."); } sslContext.getDefaultSSLParameters() .setNeedClientAuth(true); sslContext.getDefaultSSLParameters() .setWantClientAuth(true); return sslContext; } private static KeyStore getKeyStore(String location, String password) { LOGGER.debug("Loading keystore from {}", location); KeyStore keyStore = null; try (FileInputStream storeStream = new FileInputStream(location)) { keyStore = KeyStore.getInstance(System.getProperty("javax.net.ssl.keyStoreType")); keyStore.load(storeStream, password.toCharArray()); } catch (CertificateException | IOException | NoSuchAlgorithmException | KeyStoreException e) { LOGGER.warn("Unable to load keystore at {}", location, e); } return keyStore; } private static void createSolrCore(String url, String coreName, String configFileName, HttpClient httpClient) throws IOException, SolrServerException { HttpSolrClient client; if (httpClient != null) { client = new HttpSolrClient(url, httpClient); } else { client = new HttpSolrClient(url); } HttpResponse ping = client.getHttpClient() .execute(new HttpHead(url)); if (ping != null && ping.getStatusLine() .getStatusCode() == 200) { ConfigurationFileProxy configProxy = new ConfigurationFileProxy(ConfigurationStore.getInstance()); configProxy.writeSolrConfiguration(coreName); if (!solrCoreExists(client, coreName)) { LOGGER.debug("Creating Solr core {}", coreName); String configFile = StringUtils.defaultIfBlank(configFileName, DEFAULT_SOLRCONFIG_XML); String solrDir; if (System.getProperty("solr.data.dir") != null) { solrDir = System.getProperty("solr.data.dir"); } else { solrDir = Paths.get(System.getProperty("karaf.home"), "data", "solr") .toString(); } String instanceDir = Paths.get(solrDir, coreName) .toString(); String dataDir = Paths.get(instanceDir, "data") .toString(); CoreAdminRequest.createCore(coreName, instanceDir, client, configFile, DEFAULT_SCHEMA_XML, dataDir, dataDir); } else { LOGGER.debug("Solr core ({}) already exists - reloading it", coreName); CoreAdminRequest.reloadCore(coreName, client); } } else { LOGGER.debug("Unable to ping Solr core {}", coreName); throw new SolrServerException("Unable to ping Solr core"); } } private static boolean solrCoreExists(SolrClient client, String coreName) throws IOException, SolrServerException { CoreAdminResponse response = CoreAdminRequest.getStatus(coreName, client); return response.getCoreStatus(coreName) .get("instanceDir") != null; } }