/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.solr.cloud; import javax.net.ssl.SSLContext; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.util.List; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpHead; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.lucene.util.Constants; import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.embedded.JettyConfig; import org.apache.solr.client.solrj.embedded.JettySolrRunner; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.impl.HttpClientUtil; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.client.solrj.request.CoreAdminRequest; import org.apache.solr.common.cloud.ZkStateReader; import org.apache.solr.common.params.CoreAdminParams.CoreAdminAction; import org.apache.solr.util.SSLTestConfig; import org.junit.After; import org.junit.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Tests various permutations of SSL options with {@link MiniSolrCloudCluster}. * <b>NOTE: This Test ignores the randomized SSL & clientAuth settings selected by base class</b>, * instead each method initializes a {@link SSLTestConfig} will specific combinations of settings to test. * * @see TestSSLRandomization */ public class TestMiniSolrCloudClusterSSL extends SolrTestCaseJ4 { private static final SSLContext DEFAULT_SSL_CONTEXT; static { try { DEFAULT_SSL_CONTEXT = SSLContext.getDefault(); assert null != DEFAULT_SSL_CONTEXT; } catch (Exception e) { throw new RuntimeException("Unable to initialize 'Default' SSLContext Algorithm, JVM is borked", e); } } private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); public static final int NUM_SERVERS = 3; public static final String CONF_NAME = MethodHandles.lookup().lookupClass().getName(); @Before public void before() { // undo the randomization of our super class log.info("NOTE: This Test ignores the randomized SSL & clientAuth settings selected by base class"); HttpClientUtil.resetHttpClientBuilder(); // also resets SchemaRegistryProvider System.clearProperty(ZkStateReader.URL_SCHEME); } @After public void after() { HttpClientUtil.resetHttpClientBuilder(); // also resets SchemaRegistryProvider System.clearProperty(ZkStateReader.URL_SCHEME); SSLContext.setDefault(DEFAULT_SSL_CONTEXT); } public void testNoSsl() throws Exception { final SSLTestConfig sslConfig = new SSLTestConfig(false, false); HttpClientUtil.setSchemaRegistryProvider(sslConfig.buildClientSchemaRegistryProvider()); System.setProperty(ZkStateReader.URL_SCHEME, "http"); checkClusterWithNodeReplacement(sslConfig); } public void testNoSslButSillyClientAuth() throws Exception { // this combination doesn't really make sense, since ssl==false the clientauth option will be ignored // but we test it anyway for completeness of sanity checking the behavior of code that looks at those // options. final SSLTestConfig sslConfig = new SSLTestConfig(false, true); HttpClientUtil.setSchemaRegistryProvider(sslConfig.buildClientSchemaRegistryProvider()); System.setProperty(ZkStateReader.URL_SCHEME, "http"); checkClusterWithNodeReplacement(sslConfig); } public void testSslAndNoClientAuth() throws Exception { final SSLTestConfig sslConfig = new SSLTestConfig(true, false); HttpClientUtil.setSchemaRegistryProvider(sslConfig.buildClientSchemaRegistryProvider()); System.setProperty(ZkStateReader.URL_SCHEME, "https"); checkClusterWithNodeReplacement(sslConfig); } public void testSslAndClientAuth() throws Exception { assumeFalse("SOLR-9039: SSL w/clientAuth does not work on MAC_OS_X", Constants.MAC_OS_X); final SSLTestConfig sslConfig = new SSLTestConfig(true, true); HttpClientUtil.setSchemaRegistryProvider(sslConfig.buildClientSchemaRegistryProvider()); System.setProperty(ZkStateReader.URL_SCHEME, "https"); checkClusterWithNodeReplacement(sslConfig); } /** * Constructs a cluster with the specified sslConfigs, runs {@link #checkClusterWithCollectionCreations}, * then verifies that if we modify the default SSLContext (mimicing <code>javax.net.ssl.*</code> * sysprops set on JVM startup) and reset to the default HttpClientBuilder, new HttpSolrClient instances * will still be able to talk to our servers. * * @see SSLContext#setDefault * @see HttpClientUtil#resetHttpClientBuilder * @see #checkClusterWithCollectionCreations */ private void checkClusterWithNodeReplacement(SSLTestConfig sslConfig) throws Exception { final JettyConfig config = JettyConfig.builder().withSSLConfig(sslConfig).build(); final MiniSolrCloudCluster cluster = new MiniSolrCloudCluster(NUM_SERVERS, createTempDir(), config); try { checkClusterWithCollectionCreations(cluster, sslConfig); // Change the defaul SSLContext to match our test config, or to match our original system default if // our test config doesn't use SSL, and reset HttpClientUtil to it's defaults so it picks up our // SSLContext that way. SSLContext.setDefault( sslConfig.isSSLMode() ? sslConfig.buildClientSSLContext() : DEFAULT_SSL_CONTEXT); HttpClientUtil.resetHttpClientBuilder(); // recheck that we can communicate with all the jetty instances in our cluster checkClusterJettys(cluster, sslConfig); } finally { cluster.shutdown(); } } /** * General purpose cluster sanity check... * <ol> * <li>Upload a config set</li> * <li>verifies a collection can be created</li> * <li>verifies many things that should succeed/fail when communicating with the cluster according to the specified sslConfig</li> * <li>shutdown a server & startup a new one in it's place</li> * <li>repeat the verifications of ssl / no-ssl communication</li> * <li>create a second collection</li> * </ol> * @see #CONF_NAME * @see #NUM_SERVERS */ public static void checkClusterWithCollectionCreations(final MiniSolrCloudCluster cluster, final SSLTestConfig sslConfig) throws Exception { cluster.uploadConfigSet(SolrTestCaseJ4.TEST_PATH().resolve("collection1").resolve("conf"), CONF_NAME); checkCreateCollection(cluster, "first_collection"); checkClusterJettys(cluster, sslConfig); // shut down a server JettySolrRunner stoppedServer = cluster.stopJettySolrRunner(0); assertTrue(stoppedServer.isStopped()); assertEquals(NUM_SERVERS - 1, cluster.getJettySolrRunners().size()); // create a new server JettySolrRunner startedServer = cluster.startJettySolrRunner(); assertTrue(startedServer.isRunning()); assertEquals(NUM_SERVERS, cluster.getJettySolrRunners().size()); checkClusterJettys(cluster, sslConfig); checkCreateCollection(cluster, "second_collection"); } /** * Verify that we can create a collection that involves one replica per node using the * CloudSolrClient available for the cluster */ private static void checkCreateCollection(final MiniSolrCloudCluster cluster, final String collection) throws Exception { final CloudSolrClient cloudClient = cluster.getSolrClient(); CollectionAdminRequest.createCollection(collection, CONF_NAME, NUM_SERVERS, 1) .withProperty("config", "solrconfig-tlog.xml") .process(cloudClient); ZkStateReader zkStateReader = cloudClient.getZkStateReader(); AbstractDistribZkTestBase.waitForRecoveriesToFinish(collection, zkStateReader, true, true, 330); assertEquals("sanity query", 0, cloudClient.query(collection, params("q","*:*")).getStatus()); } /** * verify that we can query all of the Jetty instances the specified cluster using the expected * options (based on the sslConfig), and that we can <b>NOT</b> query the Jetty instances in * specified cluster in the ways that should fail (based on the sslConfig) * * @see #getRandomizedHttpSolrClient */ private static void checkClusterJettys(final MiniSolrCloudCluster cluster, final SSLTestConfig sslConfig) throws Exception { final boolean ssl = sslConfig.isSSLMode(); List<JettySolrRunner> jettys = cluster.getJettySolrRunners(); for (JettySolrRunner jetty : jettys) { final String baseURL = jetty.getBaseUrl().toString(); // basic base URL sanity checks assertTrue("WTF baseURL: " + baseURL, null != baseURL && 10 < baseURL.length()); assertEquals("http vs https: " + baseURL, ssl ? "https" : "http:", baseURL.substring(0,5)); // verify solr client success with expected protocol try (HttpSolrClient client = getRandomizedHttpSolrClient(baseURL)) { assertEquals(0, CoreAdminRequest.getStatus(/* all */ null, client).getStatus()); } // sanity check the HttpClient used under the hood by our the cluster's CloudSolrClient // ensure it has the necessary protocols/credentials for each jetty server // // NOTE: we're not responsible for closing the cloud client final HttpClient cloudClient = cluster.getSolrClient().getLbClient().getHttpClient(); try (HttpSolrClient client = getRandomizedHttpSolrClient(baseURL)) { assertEquals(0, CoreAdminRequest.getStatus(/* all */ null, client).getStatus()); } final String wrongBaseURL = baseURL.replaceFirst((ssl ? "https://" : "http://"), (ssl ? "http://" : "https://")); // verify solr client using wrong protocol can't talk to server expectThrows(SolrServerException.class, () -> { try (HttpSolrClient client = getRandomizedHttpSolrClient(wrongBaseURL)) { CoreAdminRequest req = new CoreAdminRequest(); req.setAction( CoreAdminAction.STATUS ); client.request(req); } }); if (! sslConfig.isClientAuthMode()) { // verify simple HTTP(S) client can't do HEAD request for URL with wrong protocol try (CloseableHttpClient client = getSslAwareClientWithNoClientCerts()) { final String wrongUrl = wrongBaseURL + "/admin/cores"; // vastly diff exception details between plain http vs https, not worried about details here expectThrows(IOException.class, () -> { doHeadRequest(client, wrongUrl); }); } } if (ssl) { // verify expected results for a HEAD request to valid URL from HTTP(S) client w/o client certs try (CloseableHttpClient client = getSslAwareClientWithNoClientCerts()) { final String url = baseURL + "/admin/cores"; if (sslConfig.isClientAuthMode()) { // w/o a valid client cert, SSL connection should fail expectThrows(IOException.class, () -> { doHeadRequest(client, url); }); } else { assertEquals("Wrong status for head request ("+url+") when clientAuth=" + sslConfig.isClientAuthMode(), 200, doHeadRequest(client, url)); } } } } } /** * Trivial helper method for doing a HEAD request of the specified URL using the specified client * and getting the HTTP statusCode from the response */ private static int doHeadRequest(final CloseableHttpClient client, final String url) throws Exception { return client.execute(new HttpHead(url)).getStatusLine().getStatusCode(); } /** * Returns a new HttpClient that supports both HTTP and HTTPS (with the default test truststore), but * has no keystore -- so servers requiring client authentication should fail. */ private static CloseableHttpClient getSslAwareClientWithNoClientCerts() throws Exception { // NOTE: This method explicitly does *NOT* use HttpClientUtil code because that // will muck with the global static HttpClientBuilder / SchemeRegistryProvider // and we can't do that and still test the entire purpose of what we are trying to test here. final SSLTestConfig clientConfig = new SSLTestConfig(true, false); final SSLConnectionSocketFactory sslFactory = clientConfig.buildClientSSLConnectionSocketFactory(); assert null != sslFactory; final Registry<ConnectionSocketFactory> socketFactoryReg = RegistryBuilder.<ConnectionSocketFactory> create() .register("https", sslFactory) .register("http", PlainConnectionSocketFactory.INSTANCE ) .build(); final HttpClientBuilder builder = HttpClientBuilder.create(); builder.setConnectionManager(new PoolingHttpClientConnectionManager(socketFactoryReg)); return builder.build(); } /** * Generates an HttpSolrClient, either by using the test framework helper method or by direct * instantiation (determined randomly) * @see #getHttpSolrClient */ public static HttpSolrClient getRandomizedHttpSolrClient(String url) { // NOTE: at the moment, SolrTestCaseJ4 already returns "new HttpSolrClient" most of the time, // so this method may seem redundant -- but the point here is to sanity check 2 things: // 1) a direct test that "new HttpSolrClient" works given the current JVM/sysprop defaults // 2) a sanity check that whatever getHttpSolrClient(String) returns will work regardless of // current test configuration. // ... so we are hopefully future proofing against possible changes to SolrTestCaseJ4.getHttpSolrClient // that "optimize" the test client construction in a way that would prevent us from finding bugs with // regular HttpSolrClient instantiation. if (random().nextBoolean()) { return new HttpSolrClient(url); } // else... return getHttpSolrClient(url); } }