org.apache.solr.util.SSLTestConfig.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.solr.util.SSLTestConfig.java

Source

/*
 * 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.util;

import java.io.File;
import java.util.Random;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.security.UnrecoverableKeyException;

import javax.net.ssl.SSLContext;
import java.net.MalformedURLException;

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.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.solr.client.solrj.embedded.SSLConfig;
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.client.solrj.impl.HttpClientUtil.SchemaRegistryProvider;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.security.CertificateUtils;
import org.eclipse.jetty.util.ssl.SslContextFactory;

/**
 * An {@link SSLConfig} that supports reading key/trust store information directly from resource 
 * files provided with the Solr test-framework classes
 */
public class SSLTestConfig extends SSLConfig {

    /** @deprecated No longer used except by {@link #setSSLSystemProperties} */
    public static File TEST_KEYSTORE = ExternalPaths.SERVER_HOME == null ? null
            : new File(ExternalPaths.SERVER_HOME, "../etc/test/solrtest.keystore");

    /** @deprecated No longer used except by {@link #setSSLSystemProperties} */
    private static String TEST_KEYSTORE_PATH = TEST_KEYSTORE != null && TEST_KEYSTORE.exists()
            ? TEST_KEYSTORE.getAbsolutePath()
            : null;

    private static final String TEST_KEYSTORE_RESOURCE = "SSLTestConfig.testing.keystore";
    private static final String TEST_KEYSTORE_PASSWORD = "secret";

    private final Resource keyStore;
    private final Resource trustStore;

    /** Creates an SSLTestConfig that does not use SSL or client authentication */
    public SSLTestConfig() {
        this(false, false);
    }

    /** 
     * Create an SSLTestConfig based on a few caller specified options.  As needed, 
     * keystore/truststore information will be pulled from a hardocded resource file provided 
     * by the solr test-framework.
     *
     * @param useSSL - wether SSL should be required.
     * @param clientAuth - whether client authentication should be required.
     */
    public SSLTestConfig(boolean useSSL, boolean clientAuth) {
        super(useSSL, clientAuth, null, TEST_KEYSTORE_PASSWORD, null, TEST_KEYSTORE_PASSWORD);
        trustStore = keyStore = Resource.newClassPathResource(TEST_KEYSTORE_RESOURCE);
        if (null == keyStore || !keyStore.exists()) {
            throw new IllegalStateException(
                    "Unable to locate keystore resource file in classpath: " + TEST_KEYSTORE_RESOURCE);
        }
    }

    /** 
     * Create an SSLTestConfig using explicit paths for files 
     * @deprecated - use {@link SSLConfig} directly
     */
    @Deprecated
    public SSLTestConfig(boolean useSSL, boolean clientAuth, String keyStore, String keyStorePassword,
            String trustStore, String trustStorePassword) {
        super(useSSL, clientAuth, keyStore, keyStorePassword, trustStore, trustStorePassword);
        this.keyStore = tryNewResource(keyStore, "KeyStore");
        this.trustStore = tryNewResource(trustStore, "TrustStore");
    }

    /**
     * Helper utility for building resources from arbitrary user input paths/urls
     * if input is null, returns null; otherwise attempts to build Resource and verifies that Resource exists.
     */
    private static final Resource tryNewResource(String userInput, String type) {
        if (null == userInput) {
            return null;
        }
        Resource result;
        try {
            result = Resource.newResource(userInput);
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException("Can't build " + type + " Resource: " + e.getMessage(), e);
        }
        if (!result.exists()) {
            throw new IllegalArgumentException(type + " Resource does not exist " + result.getName());
        }
        return result;
    }

    /** NOTE: This method is meaningless unless you explicitly provide paths when constructing this instance 
     * @see #SSLTestConfig(boolean,boolean,String,String,String,String)
     */
    @Override
    public String getKeyStore() {
        return super.getKeyStore();
    }

    /** NOTE: This method is meaningless unless you explicitly provide paths when constructing this instance 
     * @see #SSLTestConfig(boolean,boolean,String,String,String,String)
     */
    @Override
    public String getTrustStore() {
        return super.getTrustStore();
    }

    /**
     * Creates a {@link SchemaRegistryProvider} 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>SchemaRegistryProvider</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 SchemaRegistryProvider buildClientSchemaRegistryProvider() {
        if (isSSLMode()) {
            SSLConnectionSocketFactory sslConnectionFactory = buildClientSSLConnectionSocketFactory();
            assert null != sslConnectionFactory;
            return new SSLSchemaRegistryProvider(sslConnectionFactory);
        } else {
            return HTTP_ONLY_SCHEMA_PROVIDER;
        }
    }

    /**
     * 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.  
     *
     * NOTE: Uses a completely insecure {@link SecureRandom} instance to prevent tests from blocking 
     * due to lack of entropy, also explicitly allows the use of self-signed 
     * certificates (since that's what is almost always used during testing).
     */
    public SSLContext buildClientSSLContext()
            throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {

        assert isSSLMode();

        SSLContextBuilder builder = SSLContexts.custom();
        builder.setSecureRandom(NotSecurePsuedoRandom.INSTANCE);

        // 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(keyStore, getKeyStorePassword()), new TrustSelfSignedStrategy())
                .build();

        if (isClientAuthMode()) {
            builder.loadKeyMaterial(buildKeyStore(trustStore, getTrustStorePassword()),
                    getTrustStorePassword().toCharArray());

        }

        return builder.build();
    }

    /**
     * Builds a new SSLContext for jetty servers which have been configured based on the settings of 
     * this object.
     *
     * NOTE: Uses a completely insecure {@link SecureRandom} instance to prevent tests from blocking 
     * due to lack of entropy, also explicitly allows the use of self-signed 
     * certificates (since that's what is almost always used during testing).
     * almost always used during testing). 
     */
    public SSLContext buildServerSSLContext()
            throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {

        assert isSSLMode();

        SSLContextBuilder builder = SSLContexts.custom();
        builder.setSecureRandom(NotSecurePsuedoRandom.INSTANCE);

        builder.loadKeyMaterial(buildKeyStore(keyStore, getKeyStorePassword()),
                getKeyStorePassword().toCharArray());

        if (isClientAuthMode()) {
            builder.loadTrustMaterial(buildKeyStore(trustStore, getTrustStorePassword()),
                    new TrustSelfSignedStrategy()).build();

        }

        return builder.build();
    }

    /**
     * Returns an SslContextFactory using {@link #buildServerSSLContext} if SSL should be used, else returns null.
     */
    @Override
    public SslContextFactory createContextFactory() {
        if (!isSSLMode()) {
            return null;
        }
        // else...

        SslContextFactory factory = new SslContextFactory(false);
        try {
            factory.setSslContext(buildServerSSLContext());
        } catch (Exception e) {
            throw new RuntimeException("ssl context init failure: " + e.getMessage(), e);
        }
        factory.setNeedClientAuth(isClientAuthMode());
        return factory;
    }

    /**
     * Constructs a KeyStore using the specified filename and password
     */
    protected static KeyStore buildKeyStore(Resource resource, String password) {
        try {
            return CertificateUtils.getKeyStore(resource, "JKS", null, password);
        } catch (Exception ex) {
            throw new IllegalStateException("Unable to build KeyStore from resource: " + resource.getName(), ex);
        }
    }

    /** 
     * 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;
    }

    /** A SchemaRegistryProvider that only knows about SSL using a specified SSLConnectionSocketFactory */
    private static class SSLSchemaRegistryProvider extends SchemaRegistryProvider {
        private final SSLConnectionSocketFactory sslConnectionFactory;

        public SSLSchemaRegistryProvider(SSLConnectionSocketFactory sslConnectionFactory) {
            this.sslConnectionFactory = sslConnectionFactory;
        }

        @Override
        public Registry<ConnectionSocketFactory> getSchemaRegistry() {
            return RegistryBuilder.<ConnectionSocketFactory>create().register("https", sslConnectionFactory)
                    .build();
        }
    }

    /** A SchemaRegistryProvider that only knows about HTTP */
    private static final SchemaRegistryProvider HTTP_ONLY_SCHEMA_PROVIDER = new SchemaRegistryProvider() {
        @Override
        public Registry<ConnectionSocketFactory> getSchemaRegistry() {
            return RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("http", PlainConnectionSocketFactory.getSocketFactory()).build();
        }
    };

    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);
        System.setProperty("javax.net.ssl.trustStore", TEST_KEYSTORE_PATH);
        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");
        System.clearProperty("javax.net.ssl.trustStore");
        System.clearProperty("javax.net.ssl.trustStorePassword");
    }

    /**
     * A mocked up instance of SecureRandom that just uses {@link Random} under the covers.
     * This is to prevent blocking issues that arise in platform default 
     * SecureRandom instances due to too many instances / not enough random entropy.  
     * Tests do not need secure SSL.
     */
    private static class NotSecurePsuedoRandom extends SecureRandom {
        public static final SecureRandom INSTANCE = new NotSecurePsuedoRandom();
        private static final Random RAND = new Random(42);

        /** 
         * Helper method that can be used to fill an array with non-zero data.
         * (Attempted workarround of Solaris SSL Padding bug: SOLR-9068)
         */
        private static final byte[] fillData(byte[] data) {
            RAND.nextBytes(data);
            return data;
        }

        /** SPI Used to init all instances */
        private static final SecureRandomSpi NOT_SECURE_SPI = new SecureRandomSpi() {
            /** returns a new byte[] filled with static data */
            public byte[] engineGenerateSeed(int numBytes) {
                return fillData(new byte[numBytes]);
            }

            /** fills the byte[] with static data */
            public void engineNextBytes(byte[] bytes) {
                fillData(bytes);
            }

            /** NOOP */
            public void engineSetSeed(byte[] seed) {
                /* NOOP */ }
        };

        private NotSecurePsuedoRandom() {
            super(NOT_SECURE_SPI, null);
        }

        /** returns a new byte[] filled with static data */
        public byte[] generateSeed(int numBytes) {
            return fillData(new byte[numBytes]);
        }

        /** fills the byte[] with static data */
        synchronized public void nextBytes(byte[] bytes) {
            fillData(bytes);
        }

        /** NOOP */
        synchronized public void setSeed(byte[] seed) {
            /* NOOP */ }

        /** NOOP */
        synchronized public void setSeed(long seed) {
            /* NOOP */ }

    }
}