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

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.TrustManagerFactory;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.Builder;
import org.apache.cassandra.distributed.shared.WithProperties;
import org.apache.cassandra.transport.TlsTestUtils;

import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.OpenSslContext;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslProvider;

import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_TCACTIVE_OPENSSL;
import static org.apache.cassandra.config.EncryptionOptions.ClientEncryptionOptions.ClientAuth.NOT_REQUIRED;
import static org.apache.cassandra.config.EncryptionOptions.ClientEncryptionOptions.ClientAuth.REQUIRED;

public class DefaultSslContextFactoryTest
{
    private Map<String, Object> commonConfig = new HashMap<>();

    @Before
    public void setup()
    {
        commonConfig.put("truststore", TlsTestUtils.SERVER_TRUSTSTORE_PATH);
        commonConfig.put("truststore_password", TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD);
        commonConfig.put("require_client_auth", "false");
        commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA"));
    }

    private void addKeystoreOptions(Map<String, Object> config)
    {
        config.put("keystore", TlsTestUtils.SERVER_KEYSTORE_PATH);
        config.put("keystore_password", TlsTestUtils.SERVER_KEYSTORE_PASSWORD);
    }

    private void addOutboundKeystoreOptions(Map<String, Object> config)
    {
        config.put("outbound_keystore", TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH);
        config.put("outbound_keystore_password", TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD);
    }

    @Test
    public void getSslContextOpenSSL() throws IOException
    {
        EncryptionOptions.ServerEncryptionOptions.Builder builder = new Builder();
        EncryptionOptions.ServerEncryptionOptions options = builder
                                                            .withOutboundKeystore(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PATH)
                                                            .withOutboundKeystorePassword(TlsTestUtils.SERVER_OUTBOUND_KEYSTORE_PASSWORD)
                                                            .withTrustStore(TlsTestUtils.SERVER_TRUSTSTORE_PATH)
                                                            .withTrustStorePassword(TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD)
                                                            .withKeyStore(TlsTestUtils.SERVER_KEYSTORE_PATH)
                                                            .withKeyStorePassword(TlsTestUtils.SERVER_KEYSTORE_PASSWORD)
                                                            .withRequireClientAuth(NOT_REQUIRED)
                                                            .withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA")
                                                            .build();

        SslContext sslContext = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test");
        Assert.assertNotNull(sslContext);
        if (OpenSsl.isAvailable())
            Assert.assertTrue(sslContext instanceof OpenSslContext);
        else
            Assert.assertTrue(sslContext instanceof SslContext);
    }

    @Test(expected = IOException.class)
    public void buildTrustManagerFactoryWithInvalidTruststoreFile() throws IOException
    {
        Map<String, Object> config = new HashMap<>();
        config.putAll(commonConfig);
        config.put("truststore", "/this/is/probably/not/a/file/on/your/test/machine");

        DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
        defaultSslContextFactoryImpl.keystoreContext.checkedExpiry = false;
        defaultSslContextFactoryImpl.buildTrustManagerFactory();
    }

    @Test(expected = IOException.class)
    public void buildTrustManagerFactoryWithBadPassword() throws IOException
    {
        Map<String, Object> config = new HashMap<>();
        config.putAll(commonConfig);
        config.put("truststore_password", "HomeOfBadPasswords");

        DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
        defaultSslContextFactoryImpl.keystoreContext.checkedExpiry = false;
        defaultSslContextFactoryImpl.buildTrustManagerFactory();
    }

    @Test
    public void buildTrustManagerFactoryHappyPath() throws IOException
    {
        Map<String, Object> config = new HashMap<>();
        config.putAll(commonConfig);

        DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
        defaultSslContextFactoryImpl.keystoreContext.checkedExpiry = false;
        TrustManagerFactory trustManagerFactory = defaultSslContextFactoryImpl.buildTrustManagerFactory();
        Assert.assertNotNull(trustManagerFactory);
    }

    @Test(expected = IOException.class)
    public void buildKeyManagerFactoryWithInvalidKeystoreFile() throws IOException
    {
        Map<String, Object> config = new HashMap<>();
        config.putAll(commonConfig);
        config.put("keystore", "/this/is/probably/not/a/file/on/your/test/machine");
        config.put("keystore_password", "ThisWontMatter");

        DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
        defaultSslContextFactoryImpl.keystoreContext.checkedExpiry = false;
        defaultSslContextFactoryImpl.buildKeyManagerFactory();
    }

    @Test(expected = IOException.class)
    public void buildKeyManagerFactoryWithBadPassword() throws IOException
    {
        Map<String, Object> config = new HashMap<>();
        config.putAll(commonConfig);
        addKeystoreOptions(config);
        config.put("keystore_password", "HomeOfBadPasswords");

        DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
        defaultSslContextFactoryImpl.buildKeyManagerFactory();
    }

    @Test
    public void buildKeyManagerFactoryHappyPath() throws IOException
    {
        Map<String, Object> config = new HashMap<>();
        config.putAll(commonConfig);

        DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
        // Make sure the exiry check didn't happen so far for the private key
        Assert.assertFalse(defaultSslContextFactoryImpl.keystoreContext.checkedExpiry);

        addKeystoreOptions(config);
        DefaultSslContextFactory defaultSslContextFactoryImpl2 = new DefaultSslContextFactory(config);
        // Trigger the private key loading. That will also check for expired private key
        defaultSslContextFactoryImpl2.buildKeyManagerFactory();
        // Now we should have checked the private key's expiry
        Assert.assertTrue(defaultSslContextFactoryImpl2.keystoreContext.checkedExpiry);

        // Make sure that new factory object preforms the fresh private key expiry check
        DefaultSslContextFactory defaultSslContextFactoryImpl3 = new DefaultSslContextFactory(config);
        Assert.assertFalse(defaultSslContextFactoryImpl3.keystoreContext.checkedExpiry);
        defaultSslContextFactoryImpl3.buildKeyManagerFactory();
        Assert.assertTrue(defaultSslContextFactoryImpl3.keystoreContext.checkedExpiry);
    }

    @Test(expected = IOException.class)
    public void buildOutboundKeyManagerFactoryWithInvalidKeystoreFile() throws IOException
    {
        Map<String, Object> config = new HashMap<>();
        config.putAll(commonConfig);
        config.put("outbound_keystore", "/this/is/probably/not/a/file/on/your/test/machine");
        config.put("outbound_keystore_password", "ThisWontMatter");

        DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
        defaultSslContextFactoryImpl.outboundKeystoreContext.checkedExpiry = false;
        defaultSslContextFactoryImpl.buildOutboundKeyManagerFactory();
    }

    @Test(expected = IOException.class)
    public void buildOutboundKeyManagerFactoryWithBadPassword() throws IOException
    {
        Map<String, Object> config = new HashMap<>();
        config.putAll(commonConfig);
        addOutboundKeystoreOptions(config);
        config.put("outbound_keystore_password", "HomeOfBadPasswords");

        DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
        defaultSslContextFactoryImpl.buildOutboundKeyManagerFactory();
    }

    @Test
    public void buildOutboundKeyManagerFactoryHappyPath() throws IOException
    {
        Map<String, Object> config = new HashMap<>();
        config.putAll(commonConfig);

        DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
        // Make sure the exiry check didn't happen so far for the private key
        Assert.assertFalse(defaultSslContextFactoryImpl.outboundKeystoreContext.checkedExpiry);

        addOutboundKeystoreOptions(config);
        DefaultSslContextFactory defaultSslContextFactoryImpl2 = new DefaultSslContextFactory(config);
        // Trigger the private key loading. That will also check for expired private key
        defaultSslContextFactoryImpl2.buildOutboundKeyManagerFactory();
        // Now we should have checked the private key's expiry
        Assert.assertTrue(defaultSslContextFactoryImpl2.outboundKeystoreContext.checkedExpiry);
        Assert.assertFalse(defaultSslContextFactoryImpl2.keystoreContext.checkedExpiry);

        // Make sure that new factory object preforms the fresh private key expiry check
        DefaultSslContextFactory defaultSslContextFactoryImpl3 = new DefaultSslContextFactory(config);
        Assert.assertFalse(defaultSslContextFactoryImpl3.outboundKeystoreContext.checkedExpiry);
        defaultSslContextFactoryImpl3.buildOutboundKeyManagerFactory();
        Assert.assertTrue(defaultSslContextFactoryImpl3.outboundKeystoreContext.checkedExpiry);
        Assert.assertFalse(defaultSslContextFactoryImpl2.keystoreContext.checkedExpiry);
    }

    @Test
    public void testDisableOpenSslForInJvmDtests()
    {
        // The configuration name below is hard-coded intentionally to make sure we don't break the contract without
        // changing the documentation appropriately
        try (WithProperties properties = new WithProperties().set(DISABLE_TCACTIVE_OPENSSL, true))
        {
            Map<String, Object> config = new HashMap<>();
            config.putAll(commonConfig);

            DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
            Assert.assertEquals(SslProvider.JDK, defaultSslContextFactoryImpl.getSslProvider());
        }
    }
}
