blob: ffe0f7b40e39864562642e42d737587ea5b3e23a [file] [log] [blame]
/*
* 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 javax.net.ssl.SSLException;
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.lucene.util.TestRuleRestoreSystemProperties;
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.Http2SolrClient;
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.UrlScheme;
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.junit.Rule;
import org.junit.rules.TestRule;
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 &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 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();
@Rule
public TestRule syspropRestore = new TestRuleRestoreSystemProperties
(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME);
@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 SocketFactoryRegistryProvider
Http2SolrClient.resetSslContextFactory();
System.clearProperty(ZkStateReader.URL_SCHEME);
}
@After
public void after() {
HttpClientUtil.resetHttpClientBuilder(); // also resets SocketFactoryRegistryProvider
Http2SolrClient.resetSslContextFactory();
System.clearProperty(ZkStateReader.URL_SCHEME);
SSLContext.setDefault(DEFAULT_SSL_CONTEXT);
}
public void testNoSsl() throws Exception {
final SSLTestConfig sslConfig = new SSLTestConfig(false, false);
HttpClientUtil.setSocketFactoryRegistryProvider(sslConfig.buildClientSocketFactoryRegistryProvider());
Http2SolrClient.setDefaultSSLConfig(sslConfig.buildClientSSLConfig());
System.setProperty(ZkStateReader.URL_SCHEME, "http");
UrlScheme.INSTANCE.setUrlScheme(UrlScheme.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.setSocketFactoryRegistryProvider(sslConfig.buildClientSocketFactoryRegistryProvider());
Http2SolrClient.setDefaultSSLConfig(sslConfig.buildClientSSLConfig());
System.setProperty(ZkStateReader.URL_SCHEME, "http");
UrlScheme.INSTANCE.setUrlScheme(UrlScheme.HTTP);
checkClusterWithNodeReplacement(sslConfig);
}
public void testSslAndNoClientAuth() throws Exception {
final SSLTestConfig sslConfig = new SSLTestConfig(true, false);
HttpClientUtil.setSocketFactoryRegistryProvider(sslConfig.buildClientSocketFactoryRegistryProvider());
Http2SolrClient.setDefaultSSLConfig(sslConfig.buildClientSSLConfig());
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.setSocketFactoryRegistryProvider(sslConfig.buildClientSocketFactoryRegistryProvider());
Http2SolrClient.setDefaultSSLConfig(sslConfig.buildClientSSLConfig());
System.setProperty(ZkStateReader.URL_SCHEME, "https");
checkClusterWithNodeReplacement(sslConfig);
}
// commented out on: 17-Feb-2019 @LuceneTestCase.BadApple(bugUrl="https://issues.apache.org/jira/browse/SOLR-12028") // 2-Aug-2018
public void testSslWithCheckPeerName() throws Exception {
final SSLTestConfig sslConfig = new SSLTestConfig(true, false, true);
HttpClientUtil.setSocketFactoryRegistryProvider(sslConfig.buildClientSocketFactoryRegistryProvider());
Http2SolrClient.setDefaultSSLConfig(sslConfig.buildClientSSLConfig());
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.buildServerSSLConfig()).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);
System.setProperty(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME,
Boolean.toString(sslConfig.getCheckPeerName()));
HttpClientUtil.resetHttpClientBuilder();
Http2SolrClient.resetSslContextFactory();
// recheck that we can communicate with all the jetty instances in our cluster
checkClusterJettys(cluster, sslConfig);
} finally {
cluster.shutdown();
}
}
/** Sanity check that our test scaffolding for validating SSL peer names fails when it should */
public void testSslWithInvalidPeerName() throws Exception {
// NOTE: first initialize the cluster w/o peer name checks, which means our server will use
// certs with a bogus hostname/ip and clients shouldn't care...
final SSLTestConfig sslConfig = new SSLTestConfig(true, false, false);
HttpClientUtil.setSocketFactoryRegistryProvider(sslConfig.buildClientSocketFactoryRegistryProvider());
Http2SolrClient.setDefaultSSLConfig(sslConfig.buildClientSSLConfig());
System.setProperty(ZkStateReader.URL_SCHEME, "https");
final JettyConfig config = JettyConfig.builder().withSSLConfig(sslConfig.buildServerSSLConfig()).build();
final MiniSolrCloudCluster cluster = new MiniSolrCloudCluster(NUM_SERVERS, createTempDir(), config);
try {
checkClusterWithCollectionCreations(cluster, sslConfig);
// now initialize a client that still uses the existing SSLContext/Provider, so it will accept
// our existing certificate, but *does* care about validating the peer name
System.setProperty(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME, "true");
HttpClientUtil.resetHttpClientBuilder();
Http2SolrClient.resetSslContextFactory();
// and validate we get failures when trying to talk to our cluster...
final List<JettySolrRunner> jettys = cluster.getJettySolrRunners();
for (JettySolrRunner jetty : jettys) {
final String baseURL = jetty.getBaseUrl().toString();
// verify new solr clients validate peer name and can't talk to this server
Exception ex = expectThrows(SolrServerException.class, () -> {
try (HttpSolrClient client = getRandomizedHttpSolrClient(baseURL)) {
CoreAdminRequest req = new CoreAdminRequest();
req.setAction( CoreAdminAction.STATUS );
client.request(req);
}
});
assertTrue("Expected an root cause SSL Exception, got: " + ex.toString(),
ex.getCause() instanceof SSLException);
}
} 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 &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.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);
cluster.waitForJettyToStop(stoppedServer);
assertTrue(stoppedServer.isStopped());
assertEquals(NUM_SERVERS - 1, cluster.getJettySolrRunners().size());
// create a new server
JettySolrRunner startedServer = cluster.startJettySolrRunner();
cluster.waitForAllNodes(30);
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);
cluster.waitForActiveCollection(collection, NUM_SERVERS, NUM_SERVERS);
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.Builder(url)).build();
} // else...
return getHttpSolrClient(url);
}
}