You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ho...@apache.org on 2016/04/29 19:24:49 UTC

lucene-solr:branch_6x: SOLR-9028: Fixed some test related bugs preventing SSL + ClientAuth from ever being tested (cherry picked from commit 791d1e7)

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_6x 14b42fe10 -> 7aecf344b


SOLR-9028: Fixed some test related bugs preventing SSL + ClientAuth from ever being tested
(cherry picked from commit 791d1e7)

Conflicts:
	solr/core/src/test/org/apache/solr/cloud/SSLMigrationTest.java
	solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpClientUtil.java
	solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
	solr/test-framework/src/java/org/apache/solr/util/SSLTestConfig.java


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/7aecf344
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/7aecf344
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/7aecf344

Branch: refs/heads/branch_6x
Commit: 7aecf344b15fb7f1a3136198ca590efd9eec7164
Parents: 14b42fe
Author: Chris Hostetter <ho...@apache.org>
Authored: Thu Apr 28 13:18:01 2016 -0700
Committer: Chris Hostetter <ho...@apache.org>
Committed: Fri Apr 29 10:08:41 2016 -0700

----------------------------------------------------------------------
 lucene/tools/junit4/solr-tests.policy           |   1 +
 solr/CHANGES.txt                                |   2 +
 .../solr/client/solrj/embedded/SSLConfig.java   |  38 ++-
 .../solr/cloud/TestMiniSolrCloudClusterSSL.java | 338 +++++++++++++++++--
 .../apache/solr/cloud/TestSSLRandomization.java |  54 +++
 .../java/org/apache/solr/SolrTestCaseJ4.java    |  13 +-
 .../org/apache/solr/util/SSLTestConfig.java     | 127 +++++--
 7 files changed, 504 insertions(+), 69 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/7aecf344/lucene/tools/junit4/solr-tests.policy
----------------------------------------------------------------------
diff --git a/lucene/tools/junit4/solr-tests.policy b/lucene/tools/junit4/solr-tests.policy
index 0d745bf..6f03b0f 100644
--- a/lucene/tools/junit4/solr-tests.policy
+++ b/lucene/tools/junit4/solr-tests.policy
@@ -73,6 +73,7 @@ grant {
 
   // SSL related properties for Solr tests
   permission java.security.SecurityPermission "getProperty.ssl.*";
+  permission javax.net.ssl.SSLPermission "setDefaultSSLContext";
 
   // SASL/Kerberos related properties for Solr tests
   permission javax.security.auth.PrivateCredentialPermission "javax.security.auth.kerberos.KerberosTicket * \"*\"", "read";

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/7aecf344/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 76cdb16..ff19483 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -126,6 +126,8 @@ Bug Fixes
 * SOLR-9034: Atomic updates failed to work when there were copyField targets that had docValues
   enabled. (Karthik Ramachandran, Ishan Chattopadhyaya, yonik)
 
+* SOLR-9028: Fixed some test related bugs preventing SSL + ClientAuth from ever being tested (hossman)
+
 Optimizations
 ----------------------
 * SOLR-8722: Don't force a full ZkStateReader refresh on every Overseer operation.

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/7aecf344/solr/core/src/java/org/apache/solr/client/solrj/embedded/SSLConfig.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/client/solrj/embedded/SSLConfig.java b/solr/core/src/java/org/apache/solr/client/solrj/embedded/SSLConfig.java
index 2881279..f777951 100644
--- a/solr/core/src/java/org/apache/solr/client/solrj/embedded/SSLConfig.java
+++ b/solr/core/src/java/org/apache/solr/client/solrj/embedded/SSLConfig.java
@@ -18,6 +18,11 @@ package org.apache.solr.client.solrj.embedded;
 
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 
+/** 
+ * Encapsulates settings related to SSL Configuration for an embedded Jetty Server.
+ * NOTE: all other settings are ignogred if {@link #isSSLMode} is false.
+ * @see #setUseSSL
+ */
 public class SSLConfig {
   
   private boolean useSsl;
@@ -26,7 +31,8 @@ public class SSLConfig {
   private String keyStorePassword;
   private String trustStore;
   private String trustStorePassword;
-  
+
+  /** NOTE: all other settings are ignored if useSSL is false; trustStore settings are ignored if clientAuth is false */
   public SSLConfig(boolean useSSL, boolean clientAuth, String keyStore, String keyStorePassword, String trustStore, String trustStorePassword) {
     this.useSsl = useSSL;
     this.clientAuth = clientAuth;
@@ -44,6 +50,7 @@ public class SSLConfig {
     this.clientAuth = clientAuth;
   }
   
+  /** All other settings on this object are ignored unless this is true */
   public boolean isSSLMode() {
     return useSsl;
   }
@@ -68,28 +75,41 @@ public class SSLConfig {
     return trustStorePassword;
   }
 
+  /**
+   * Returns an SslContextFactory that should be used by a jetty server based on the specified 
+   * configuration, or null if no SSL should be used.
+   *
+   * The specified sslConfig will be completely ignored if the "tests.jettySsl" system property is 
+   * true - in which case standard "javax.net.ssl.*" system properties will be used instead, along 
+   * with "tests.jettySsl.clientAuth"
+   * 
+   * @see #isSSLMode
+   */
   public static SslContextFactory createContextFactory(SSLConfig sslConfig) {
 
     if (sslConfig == null) {
-      if (Boolean.getBoolean(System.getProperty("tests.jettySsl"))) {
+      if (Boolean.getBoolean("tests.jettySsl")) {
         return configureSslFromSysProps();
       }
       return null;
     }
 
-    if (!sslConfig.useSsl)
-      return null;
+    if (!sslConfig.isSSLMode()) 
+       return null;
 
     SslContextFactory factory = new SslContextFactory(false);
     if (sslConfig.getKeyStore() != null)
       factory.setKeyStorePath(sslConfig.getKeyStore());
     if (sslConfig.getKeyStorePassword() != null)
       factory.setKeyStorePassword(sslConfig.getKeyStorePassword());
-    if (sslConfig.getTrustStore() != null)
-      factory.setTrustStorePath(sslConfig.getTrustStore());
-    if (sslConfig.getTrustStorePassword() != null)
-      factory.setTrustStorePassword(sslConfig.getTrustStorePassword());
-
+    factory.setNeedClientAuth(sslConfig.isClientAuthMode());
+    
+    if (sslConfig.isClientAuthMode()) {
+      if (sslConfig.getTrustStore() != null)
+        factory.setTrustStorePath(sslConfig.getTrustStore());
+      if (sslConfig.getTrustStorePassword() != null)
+        factory.setTrustStorePassword(sslConfig.getTrustStorePassword());
+    }
     return factory;
 
   }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/7aecf344/solr/core/src/test/org/apache/solr/cloud/TestMiniSolrCloudClusterSSL.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestMiniSolrCloudClusterSSL.java b/solr/core/src/test/org/apache/solr/cloud/TestMiniSolrCloudClusterSSL.java
index b82ba74..c075503 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestMiniSolrCloudClusterSSL.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestMiniSolrCloudClusterSSL.java
@@ -16,69 +16,337 @@
  */
 package org.apache.solr.cloud;
 
+import java.lang.invoke.MethodHandles;
+
+import java.util.Collections;
 import java.util.List;
+import java.io.File;
+import java.io.IOException;
+import java.net.SocketException;
+
+import javax.net.ssl.SSLContext;
 
 import org.apache.solr.SolrTestCaseJ4;
 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.HttpSolrClient;
+import org.apache.solr.client.solrj.impl.HttpClientUtil;
+import org.apache.solr.client.solrj.impl.HttpClientConfigurer;
 import org.apache.solr.client.solrj.request.CoreAdminRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.CoreAdminParams.CoreAdminAction;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.apache.solr.util.SSLTestConfig;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.config.Registry;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+
+import org.apache.lucene.util.Constants;
+
+import org.junit.After;
+import org.junit.Before;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
- * Tests SSL (if test framework selects it) with MiniSolrCloudCluster.
- * {@link TestMiniSolrCloudCluster} does not inherit from {@link SolrTestCaseJ4}
- * so does not support SSL.
+ * Tests various permutations of SSL options with {@link MiniSolrCloudCluster}.
+ * <b>NOTE: This Test ignores the randomized SSL &amp; 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 MiniSolrCloudCluster miniCluster;
-  private static final int NUM_SERVERS = 5;
+  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.setConfigurer(new HttpClientConfigurer());
+    System.clearProperty(ZkStateReader.URL_SCHEME);
+  }
+  @After
+  public void after() {
+    HttpClientUtil.setConfigurer(new HttpClientConfigurer());
+    System.clearProperty(ZkStateReader.URL_SCHEME);
+    SSLContext.setDefault(DEFAULT_SSL_CONTEXT);
+  }
+  
+  public void testNoSsl() throws Exception {
+    final SSLTestConfig sslConfig = new SSLTestConfig(false, false);
+    HttpClientUtil.setConfigurer(sslConfig.getHttpClientConfigurer());
+    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.setConfigurer(sslConfig.getHttpClientConfigurer());
+    System.setProperty(ZkStateReader.URL_SCHEME, "http");
+    checkClusterWithNodeReplacement(sslConfig);
+  }
+  
+  public void testSslAndNoClientAuth() throws Exception {
+    final SSLTestConfig sslConfig = new SSLTestConfig(true, false);
+    HttpClientUtil.setConfigurer(sslConfig.getHttpClientConfigurer());
+    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);
 
-  @BeforeClass
-  public static void startup() throws Exception {
-    JettyConfig config = JettyConfig.builder().withSSLConfig(sslConfig).build();
-    miniCluster = new MiniSolrCloudCluster(NUM_SERVERS, createTempDir(), config);
+    HttpClientUtil.setConfigurer(sslConfig.getHttpClientConfigurer());
+    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 HttpClientConfigurer, new HttpSolrClient instances 
+   * will still be able to talk to our servers.
+   *
+   * @see SSLContext#setDefault
+   * @see HttpClientUtil#setConfigurer
+   * @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);
 
-  @AfterClass
-  public static void shutdown() throws Exception {
-    if (miniCluster != null) {
-      miniCluster.shutdown();
+      
+      // 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.setConfigurer(new HttpClientConfigurer());
+      
+      // recheck that we can communicate with all the jetty instances in our cluster
+      checkClusterJettys(cluster, sslConfig);
+    } finally {
+      cluster.shutdown();
     }
-    miniCluster = null;
   }
 
-  @Test
-  public void testMiniSolrCloudClusterSSL() throws Exception {
-    // test send request to each server
-    sendRequestToEachServer();
+  /**
+   * 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 &amp; 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.uploadConfigDir(new File(SolrTestCaseJ4.TEST_HOME() + File.separator +
+                                     "collection1" + File.separator + "conf"),
+                            CONF_NAME);
+    
+    checkCreateCollection(cluster, "first_collection");
+    
+    checkClusterJettys(cluster, sslConfig);
+    
     // shut down a server
-    JettySolrRunner stoppedServer = miniCluster.stopJettySolrRunner(0);
+    JettySolrRunner stoppedServer = cluster.stopJettySolrRunner(0);
     assertTrue(stoppedServer.isStopped());
-    assertEquals(NUM_SERVERS - 1, miniCluster.getJettySolrRunners().size());
-
+    assertEquals(NUM_SERVERS - 1, cluster.getJettySolrRunners().size());
+    
     // create a new server
-    JettySolrRunner startedServer = miniCluster.startJettySolrRunner();
+    JettySolrRunner startedServer = cluster.startJettySolrRunner();
     assertTrue(startedServer.isRunning());
-    assertEquals(NUM_SERVERS, miniCluster.getJettySolrRunners().size());
-
-    // test send request to each server
-    sendRequestToEachServer();
+    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 {
+    assertNotNull(cluster.createCollection(collection,
+                                           /* 1 shard/replica per server */ NUM_SERVERS, 1,
+                                           CONF_NAME, null, null,
+                                           Collections.singletonMap("config","solrconfig-tlog.xml")));
+    final CloudSolrClient cloudClient = cluster.getSolrClient();
+    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();
 
-  private void sendRequestToEachServer() throws Exception {
-    List<JettySolrRunner> jettys = miniCluster.getJettySolrRunners();
     for (JettySolrRunner jetty : jettys) {
-      try (HttpSolrClient client = getHttpSolrClient(jetty.getBaseUrl().toString())) {
-        CoreAdminRequest req = new CoreAdminRequest();
-        req.setAction( CoreAdminAction.STATUS );
-        client.request(req);
+      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 neccessary 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(Exception.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 betwen 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 HttpClientConfigurer
+    // 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 redundent -- 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);
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/7aecf344/solr/core/src/test/org/apache/solr/cloud/TestSSLRandomization.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestSSLRandomization.java b/solr/core/src/test/org/apache/solr/cloud/TestSSLRandomization.java
new file mode 100644
index 0000000..e6dd90e
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cloud/TestSSLRandomization.java
@@ -0,0 +1,54 @@
+/*
+ * 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 java.lang.invoke.MethodHandles;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.util.SSLTestConfig;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A "test the test" method that verifies the SSL options randomized by {@link SolrTestCaseJ4} are 
+ * correctly used in the various helper methods available from the test framework and
+ * {@link MiniSolrCloudCluster}.
+ *
+ * @see TestMiniSolrCloudClusterSSL
+ */
+public class TestSSLRandomization extends SolrCloudTestCase {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  @BeforeClass
+  public static void createMiniSolrCloudCluster() throws Exception {
+    configureCluster(TestMiniSolrCloudClusterSSL.NUM_SERVERS).configure();
+  }
+  
+  public void testRandomizedSslAndClientAuth() throws Exception {
+    TestMiniSolrCloudClusterSSL.checkClusterWithCollectionCreations(cluster,sslConfig);
+  }
+  
+  public void testBaseUrl() throws Exception {
+    String url = buildUrl(6666, "/foo");
+    assertEquals(sslConfig.isSSLMode() ? "https://127.0.0.1:6666/foo" : "http://127.0.0.1:6666/foo", url);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/7aecf344/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
----------------------------------------------------------------------
diff --git a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
index a98c62d..645d654 100644
--- a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
+++ b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
@@ -233,7 +233,7 @@ public abstract class SolrTestCaseJ4 extends LuceneTestCase {
     newRandomConfig();
     
     sslConfig = buildSSLConfig();
-    //will use ssl specific or default depending on sslConfig
+    // based on randomized SSL config, set HttpClientConfigurer appropriately
     HttpClientUtil.setConfigurer(sslConfig.getHttpClientConfigurer());
     if(isSSLMode()) {
       // SolrCloud tests should usually clear this
@@ -327,13 +327,18 @@ public abstract class SolrTestCaseJ4 extends LuceneTestCase {
     
     // we don't choose ssl that often because of SOLR-5776
     final boolean trySsl = random().nextInt(10) < 2;
+    // NOTE: clientAuth is useless unless trySsl==true, but we randomize it independently
+    // just in case it might find bugs in our test/ssl client code (ie: attempting to use
+    // SSL w/client cert to non-ssl servers)
     boolean trySslClientAuth = random().nextInt(10) < 2;
     if (Constants.MAC_OS_X) {
-      trySslClientAuth = false;
+      // see SOLR-9039
+      // If a solution is found to remove this, please make sure to also update
+      // TestMiniSolrCloudClusterSSL.testSslAndClientAuth as well.
+      trySslClientAuth = false; 
     }
     
-    log.info("Randomized ssl ({}) and clientAuth ({})", trySsl,
-        trySslClientAuth);
+    log.info("Randomized ssl ({}) and clientAuth ({})", trySsl, trySslClientAuth);
     
     return new SSLTestConfig(trySsl, trySslClientAuth);
   }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/7aecf344/solr/test-framework/src/java/org/apache/solr/util/SSLTestConfig.java
----------------------------------------------------------------------
diff --git a/solr/test-framework/src/java/org/apache/solr/util/SSLTestConfig.java b/solr/test-framework/src/java/org/apache/solr/util/SSLTestConfig.java
index 8d626dc..35a5eac 100644
--- a/solr/test-framework/src/java/org/apache/solr/util/SSLTestConfig.java
+++ b/solr/test-framework/src/java/org/apache/solr/util/SSLTestConfig.java
@@ -27,11 +27,16 @@ import javax.net.ssl.SSLContext;
 
 import org.apache.http.conn.scheme.Scheme;
 import org.apache.http.conn.scheme.SchemeRegistry;
+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.conn.ssl.SSLContexts;
+import org.apache.http.conn.ssl.SSLContextBuilder;
 import org.apache.http.conn.ssl.SSLSocketFactory;
 import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.solr.client.solrj.embedded.SSLConfig;
+import org.apache.solr.client.solrj.impl.HttpClientUtil;
 import org.apache.solr.client.solrj.impl.HttpClientConfigurer;
 import org.apache.solr.common.params.SolrParams;
 import org.eclipse.jetty.util.resource.Resource;
@@ -44,14 +49,13 @@ public class SSLTestConfig extends SSLConfig {
   private static String TEST_KEYSTORE_PATH = TEST_KEYSTORE != null
       && TEST_KEYSTORE.exists() ? TEST_KEYSTORE.getAbsolutePath() : null;
   private static String TEST_KEYSTORE_PASSWORD = "secret";
-  private static HttpClientConfigurer DEFAULT_CONFIGURER = new HttpClientConfigurer();
   
   public SSLTestConfig() {
     this(false, false);
   }
   
   public SSLTestConfig(boolean useSSL, boolean clientAuth) {
-    super(useSSL, clientAuth, TEST_KEYSTORE_PATH, TEST_KEYSTORE_PASSWORD, TEST_KEYSTORE_PATH, TEST_KEYSTORE_PASSWORD);
+    this(useSSL, clientAuth, TEST_KEYSTORE_PATH, TEST_KEYSTORE_PASSWORD, TEST_KEYSTORE_PATH, TEST_KEYSTORE_PASSWORD);
   }
  
   public SSLTestConfig(boolean useSSL, boolean clientAuth, String keyStore, String keyStorePassword, String trustStore, String trustStorePassword) {
@@ -59,27 +63,47 @@ public class SSLTestConfig extends SSLConfig {
   }
   
   /**
-   * Will provide an HttpClientConfigurer for SSL support (adds https and
-   * removes http schemes) is SSL is enabled, otherwise return the default
-   * configurer
+   * Creates a {@link HttpClientConfigurer} for HTTP <b>clients</b> to use when communicating with servers 
+   * which have been configured based on the settings of this object.  When {@link #isSSLMode} is true, this 
+   * <code>HttpClientConfigurer</code> will <i>only</i> support HTTPS (no HTTP scheme) using the 
+   * appropriate certs.  When {@link #isSSLMode} is false, <i>only</i> HTTP (no HTTPS scheme) will be 
+   * supported.
    */
   public HttpClientConfigurer getHttpClientConfigurer() {
-    return isSSLMode() ? new SSLHttpClientConfigurer() : DEFAULT_CONFIGURER;
+    try {
+      return isSSLMode() ? new SSLHttpClientConfigurer(buildClientSSLContext()) : HTTP_ONLY_NO_SSL_CONFIGURER;
+    } catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) {
+      throw new IllegalStateException("Unable to setup HttpClientConfigurer test SSL", e);
+    }
   }
-
+  
   /**
-   * Builds a new SSLContext with the given configuration and allows the uses of
-   * self-signed certificates during testing.
+   * Builds a new SSLContext for HTTP <b>clients</b> to use when communicating with servers which have 
+   * been configured based on the settings of this object.  Also explicitly allows the use of self-signed 
+   * certificates (since that's what is almost always used during testing).
    */
-  protected SSLContext buildSSLContext() throws KeyManagementException, 
+  public SSLContext buildClientSSLContext() throws KeyManagementException, 
     UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
+
+    assert isSSLMode();
     
-    return SSLContexts.custom()
-        .loadKeyMaterial(buildKeyStore(getKeyStore(), getKeyStorePassword()), getKeyStorePassword().toCharArray())
-        .loadTrustMaterial(buildKeyStore(getTrustStore(), getTrustStorePassword()), new TrustSelfSignedStrategy()).build();
+    SSLContextBuilder builder = SSLContexts.custom();
+
+    // NOTE: KeyStore & TrustStore are swapped because they are from configured from server perspective...
+    // we are a client - our keystore contains the keys the server trusts, and vice versa
+    builder.loadTrustMaterial(buildKeyStore(getKeyStore(), getKeyStorePassword()), new TrustSelfSignedStrategy()).build();
+
+    if (isClientAuthMode()) {
+      builder.loadKeyMaterial(buildKeyStore(getTrustStore(), getTrustStorePassword()), getTrustStorePassword().toCharArray());
+      
+    }
+
+    return builder.build();
   }
   
-  
+  /**
+   * Constructs a KeyStore using the specified filename and password
+   */
   protected static KeyStore buildKeyStore(String keyStoreLocation, String password) {
     try {
       return CertificateUtils.getKeyStore(Resource.newResource(keyStoreLocation), "JKS", null, password);
@@ -88,22 +112,78 @@ public class SSLTestConfig extends SSLConfig {
     }
   }
   
-  private class SSLHttpClientConfigurer extends HttpClientConfigurer {
+  private static class SSLHttpClientConfigurer extends HttpClientConfigurer {
+    private final SSLContext sslContext;
+    public SSLHttpClientConfigurer(SSLContext sslContext) {
+       this.sslContext = sslContext;
+     }
     @SuppressWarnings("deprecation")
     public void configure(DefaultHttpClient httpClient, SolrParams config) {
       super.configure(httpClient, config);
       SchemeRegistry registry = httpClient.getConnectionManager().getSchemeRegistry();
       // Make sure no tests cheat by using HTTP
       registry.unregister("http");
-      try {
-        registry.register(new Scheme("https", 443, new SSLSocketFactory(buildSSLContext())));
-      } catch (KeyManagementException | UnrecoverableKeyException
-          | NoSuchAlgorithmException | KeyStoreException ex) {
-        throw new IllegalStateException("Unable to setup https scheme for HTTPClient to test SSL.", ex);
+      registry.register(new Scheme("https", 443, new SSLSocketFactory(sslContext)));
+    }
+  }
+
+  private static final HttpClientConfigurer HTTP_ONLY_NO_SSL_CONFIGURER =
+    new HttpClientConfigurer() {
+      @SuppressWarnings("deprecation")
+      public void configure(DefaultHttpClient httpClient, SolrParams config) {
+        super.configure(httpClient, config);
+        SchemeRegistry registry = httpClient.getConnectionManager().getSchemeRegistry();
+        registry.unregister("https");
       }
+    };
+  
+  /** 
+   * Constructs a new SSLConnectionSocketFactory for HTTP <b>clients</b> to use when communicating 
+   * with servers which have been configured based on the settings of this object. Will return null
+   * unless {@link #isSSLMode} is true.
+   */
+  public SSLConnectionSocketFactory buildClientSSLConnectionSocketFactory() {
+    if (!isSSLMode()) {
+      return null;
+    }
+    SSLConnectionSocketFactory sslConnectionFactory;
+    try {
+      boolean sslCheckPeerName = toBooleanDefaultIfNull(toBooleanObject(System.getProperty(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME)), true);
+      SSLContext sslContext = buildClientSSLContext();
+      if (sslCheckPeerName == false) {
+        sslConnectionFactory = new SSLConnectionSocketFactory
+          (sslContext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+      } else {
+        sslConnectionFactory = new SSLConnectionSocketFactory(sslContext);
+      }
+    } catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) {
+      throw new IllegalStateException("Unable to setup https scheme for HTTPClient to test SSL.", e);
+    }
+    return sslConnectionFactory;
+  }
+  
+  public static boolean toBooleanDefaultIfNull(Boolean bool, boolean valueIfNull) {
+    if (bool == null) {
+      return valueIfNull;
     }
+    return bool.booleanValue() ? true : false;
   }
   
+  public static Boolean toBooleanObject(String str) {
+    if ("true".equalsIgnoreCase(str)) {
+      return Boolean.TRUE;
+    } else if ("false".equalsIgnoreCase(str)) {
+      return Boolean.FALSE;
+    }
+    // no match
+    return null;
+  }
+  
+  /**
+   * @deprecated this method has very little practical use, in most cases you'll want to use 
+   * {@link SSLContext#setDefault} with {@link #buildClientSSLContext} instead.
+   */
+  @Deprecated
   public static void setSSLSystemProperties() {
     System.setProperty("javax.net.ssl.keyStore", TEST_KEYSTORE_PATH);
     System.setProperty("javax.net.ssl.keyStorePassword", TEST_KEYSTORE_PASSWORD);
@@ -111,6 +191,11 @@ public class SSLTestConfig extends SSLConfig {
     System.setProperty("javax.net.ssl.trustStorePassword", TEST_KEYSTORE_PASSWORD);
   }
   
+  /**
+   * @deprecated this method has very little practical use, in most cases you'll want to use 
+   * {@link SSLContext#setDefault} with {@link #buildClientSSLContext} instead.
+   */
+  @Deprecated
   public static void clearSSLSystemProperties() {
     System.clearProperty("javax.net.ssl.keyStore");
     System.clearProperty("javax.net.ssl.keyStorePassword");
@@ -118,4 +203,4 @@ public class SSLTestConfig extends SSLConfig {
     System.clearProperty("javax.net.ssl.trustStorePassword");
   }
   
-}
\ No newline at end of file
+}