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

import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.logging.Level;

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

import org.apache.catalina.Globals;
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.connector.Request;
import org.apache.tomcat.unittest.TesterLogValidationFilter;
import org.apache.tomcat.util.buf.UEncoder;
import org.easymock.EasyMock;

public class TestSSLValve {

    public static class MockRequest extends Request {

        private static MockRequest single_instance = null;

        public MockRequest(Connector connector) {
            super(connector);
            setCoyoteRequest(new org.apache.coyote.Request());
        }

        public static MockRequest getInstance() {
            if (single_instance == null) {
                Connector connector = EasyMock.createNiceMock(Connector.class);
                EasyMock.replay(connector);
                single_instance = new MockRequest(connector);
            }

            return single_instance;
        }

        @Override
        public void setAttribute(String name, Object value) {
            getCoyoteRequest().getAttributes().put(name, value);
        }

        @Override
        public Object getAttribute(String name) {
            return getCoyoteRequest().getAttribute(name);
        }

        public void setHeader(String header, String value) {
            getCoyoteRequest().getMimeHeaders().setValue(header).setString(value);
        }

        public void addHeader(String header, String value) {
            getCoyoteRequest().getMimeHeaders().addValue(header).setString(value);
        }
    }

    private static final String[] CERTIFICATE_LINES = new String[] { "-----BEGIN CERTIFICATE-----",
            "MIIFXTCCA0WgAwIBAgIJANFf3YTJgYifMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV",
            "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX",
            "aWRnaXRzIFB0eSBMdGQwHhcNMTcwNTI2MjEzNjM3WhcNMTgwNTI2MjEzNjM3WjBF",
            "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50",
            "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC",
            "CgKCAgEA2ykNBanZz4cVITNpZcWNVmErUzqgSNrK361mj9vEdB1UkHatwal9jVrR",
            "QvFgfiZ8Gl+/85t0ebJhJ+rIr1ww6JE7v2s2MThENj95K5EwZOmgvw+CBlBYsFIz",
            "8BtjlVYy+v7RaGPXfjrFkexQP9UIaiIIog2ClDZirRvb+QxS930/YW5Qo+X6EX6W",
            "/m/HvlorD25U4ni2FQ0y+EMO2e1jD88cAAMoP5f+Mf6NBK8I6yUeaSuMq7WqtHGV",
            "e4F1WOg5z9J5c/M69rB0iQr5NUQwZ1mPYf5Kr0P6+TLh8DJphbVvmHJyT3bgofeV",
            "JYl/kdjiXS5P/jwY9tfmhu04tsyzopWRUFCcj5zCiqZYaMn0wtDn08KaAh9oOlg8",
            "Z6mJ9i5EybkLm63W7z7LxuM+qnYzq4wKkKdx8hbpASwPqzJkJeXFL/LzhKdZuHiR",
            "clgPVYnm98URwhObh073dKguG/gkhcnpXcVBBVdVTJZYGBvTpQh0afXd9bcBwOzY",
            "t4MDpGiQB2fLzBOEZhQ37kUcWPmZw5bNPxhx4yE96Md0rx/Gu4ipAHuqLemb1SL5",
            "uWNesVmgY3OXaIamQIm9BCwkf8mMvoYdAT+lukTUZLtJ6s2w+Oxnl10tmb+6sTXy",
            "UB3WcBTp/o3YjAyJPnM1Wq6nVNQ4W2+NbV5purGAP09sumxeJj8CAwEAAaNQME4w",
            "HQYDVR0OBBYEFCGOYMvymUG2ZZT+lK4LvwEvx731MB8GA1UdIwQYMBaAFCGOYMvy",
            "mUG2ZZT+lK4LvwEvx731MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIB",
            "AG6m4nDYCompUtRVude1qulwwAaYEMHyIIsfymI8uAE7d2o4bGjVpAUOdH/VWSOp",
            "Rzx0oK6K9cHyiBlKHw5zSZdqcRi++tDX3P9Iy5tXO//zkhMEnSpk6RF2+9JXtyhx",
            "Gma4yAET1yES+ybiFT21uZrGCC9r69rWG8JRZshc4RVWGwZsd0zrATVqfY0mZurm",
            "xLgU4UOvkTczjlrLiklwwU68M1DLcILJ5FZGTWeTJ/q1wpIn9isK2siAW/VOcbuG",
            "xdbGladnIFv+iQfuZG0yjcuMsBFsQiXi6ONM8GM+dr+61V63/1s73jYcOToEsTMM",
            "3bHeVffoSkhZvOGTRCI6QhK9wqnIKhAYqu+NbV4OphfE3gOaK+T1cASXUtSQPXoa",
            "sEoIVmbQsWRBhWvYShVqvINsH/hAT3Cf/+SslprtQUqiyt2ljdgrRFZdoyB3S7ky",
            "KWoZRvHRj2cKU65LVYwx6U1A8SGmViz4aHMSai0wwKzOVv9MGHeRaVhlMmMsbdfu",
            "wKoKJv0xYoVwEh1rB8TH8PjbL+6eFLeZXYVZnH71d5JHCghZ8W0a11ZuYkscSQWk",
            "yoTBqEpJloWksrypqp3iL4PAL5+KkB2zp66+MVAg8LcEDFJggBBJCtv4SCWV7ZOB",
            "WLu8gep+XCwSn0Wb6D3eFs4DoIiMvQ6g2rS/pk7o5eWj", "-----END CERTIFICATE-----" };

    private final SSLValve valve = new SSLValve();

    private MockRequest mockRequest;
    private final Valve mockNext = EasyMock.createMock(Valve.class);

    public void setUp() throws Exception {
        setUp(null);
    }

    public void setUp(Connector connector) throws Exception {
        valve.setNext(mockNext);
        if (connector == null) {
            mockRequest = MockRequest.getInstance();
        } else {
            EasyMock.replay(connector);
            mockRequest = new MockRequest(connector);
        }
        mockNext.invoke(mockRequest, null);
        EasyMock.replay(mockNext);
    }

    @Test
    public void testSslHeader() throws Exception {
        setUp();
        final String headerName = "myheader";
        final String headerValue = "BASE64_HEADER_VALUE";
        mockRequest.setHeader(headerName, headerValue);

        Assert.assertEquals(headerValue, valve.mygetHeader(mockRequest, headerName));
    }


    @Test
    public void testSslHeaderNull() throws Exception {
        setUp();
        final String headerName = "myheader";
        mockRequest.setHeader(headerName, null);

        Assert.assertNull(valve.mygetHeader(mockRequest, headerName));
    }


    @Test
    public void testSslHeaderNullModHeader() throws Exception {
        setUp();
        final String headerName = "myheader";
        final String nullModHeaderValue = "(null)";
        mockRequest.setHeader(headerName, nullModHeaderValue);

        Assert.assertNull(valve.mygetHeader(mockRequest, nullModHeaderValue));
    }


    @Test
    public void testSslHeaderNullName() throws Exception {
        setUp();
        Assert.assertNull(valve.mygetHeader(mockRequest, null));
    }


    @Test
    public void testSslHeaderMultiples() throws Exception {
        setUp();
        final String headerName = "myheader";
        final String headerValue = "BASE64_HEADER_VALUE";
        mockRequest.addHeader(headerName, headerValue);
        mockRequest.addHeader(headerName, "anyway won't be found");

        Assert.assertEquals(headerValue, valve.mygetHeader(mockRequest, headerName));
    }


    @Test
    public void testSslClientCertHeaderSingleSpace() throws Exception {
        setUp();
        String singleSpaced = certificateSingleLine(" ");
        mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced);

        valve.invoke(mockRequest, null);

        assertCertificateParsed();
    }


    @Test
    public void testSslClientCertHeaderMultiSpace() throws Exception {
        setUp();
        String singleSpaced = certificateSingleLine("    ");
        mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced);

        valve.invoke(mockRequest, null);

        assertCertificateParsed();
    }


    @Test
    public void testSslClientCertHeaderTab() throws Exception {
        setUp();
        String singleSpaced = certificateSingleLine("\t");
        mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced);

        valve.invoke(mockRequest, null);

        assertCertificateParsed();
    }


    @Test
    public void testSslClientCertHeaderEscaped() throws Exception {
        setUp();
        String cert = certificateEscaped();
        mockRequest.setHeader(valve.getSslClientEscapedCertHeader(), cert);

        valve.invoke(mockRequest, null);

        assertCertificateParsed();
    }


    @Test
    public void testSslClientCertNull() throws Exception {
        setUp();
        TesterLogValidationFilter f = TesterLogValidationFilter.add(null, "", null,
                "org.apache.catalina.valves.SSLValve");

        valve.invoke(mockRequest, null);

        EasyMock.verify(mockNext);
        Assert.assertNull(mockRequest.getAttribute(Globals.CERTIFICATES_ATTR));
        Assert.assertEquals(0, f.getMessageCount());
    }


    @Test
    public void testSslClientCertShorter() throws Exception {
        setUp();
        mockRequest.setHeader(valve.getSslClientCertHeader(), "shorter than hell");

        TesterLogValidationFilter f = TesterLogValidationFilter.add(null, "", null,
                "org.apache.catalina.valves.SSLValve");

        valve.invoke(mockRequest, null);

        EasyMock.verify(mockNext);
        Assert.assertNull(mockRequest.getAttribute(Globals.CERTIFICATES_ATTR));
        Assert.assertEquals(0, f.getMessageCount());
    }


    @Test
    public void testSslClientCertIgnoredBegin() throws Exception {
        setUp();
        String[] linesBegin = Arrays.copyOf(CERTIFICATE_LINES, CERTIFICATE_LINES.length);
        linesBegin[0] = "3fisjcme3kdsakasdfsadkafsd3";
        String begin = certificateSingleLine(linesBegin, " ");
        mockRequest.setHeader(valve.getSslClientCertHeader(), begin);

        valve.invoke(mockRequest, null);

        assertCertificateParsed();
    }


    @Test
    public void testSslClientCertBadFormat() throws Exception {
        setUp();
        String[] linesDeleted = Arrays.copyOf(CERTIFICATE_LINES, CERTIFICATE_LINES.length / 2);
        String deleted = certificateSingleLine(linesDeleted, " ");
        mockRequest.setHeader(valve.getSslClientCertHeader(), deleted);

        TesterLogValidationFilter f = TesterLogValidationFilter.add(Level.WARNING, null,
                "java.security.cert.CertificateException", "org.apache.catalina.valves.SSLValve");

        valve.invoke(mockRequest, null);

        EasyMock.verify(mockNext);
        Assert.assertNull(mockRequest.getAttribute(Globals.CERTIFICATES_ATTR));
        Assert.assertEquals(1, f.getMessageCount());
    }


    @Test
    public void testClientCertProviderNotFound() throws Exception {
        Connector connector = EasyMock.createNiceMock(Connector.class);
        EasyMock.expect(connector.getProperty("clientCertProvider")).andStubReturn("wontBeFound");
        setUp(connector);

        mockRequest.setHeader(valve.getSslClientCertHeader(), certificateSingleLine(" "));

        TesterLogValidationFilter f = TesterLogValidationFilter.add(Level.SEVERE, null,
                "java.security.NoSuchProviderException", "org.apache.catalina.valves.SSLValve");

        valve.invoke(mockRequest, null);

        Assert.assertNull(mockRequest.getAttribute(Globals.CERTIFICATES_ATTR));
        Assert.assertEquals(1, f.getMessageCount());
    }


    @Test
    public void testSslCipherHeaderPresent() throws Exception {
        setUp();
        String cipher = "ciphered-with";
        mockRequest.setHeader(valve.getSslCipherHeader(), cipher);

        valve.invoke(mockRequest, null);

        Assert.assertEquals(cipher, mockRequest.getAttribute(Globals.CIPHER_SUITE_ATTR));
    }


    @Test
    public void testSslSessionIdHeaderPresent() throws Exception {
        setUp();
        String session = "ssl-session";
        mockRequest.setHeader(valve.getSslSessionIdHeader(), session);

        valve.invoke(mockRequest, null);

        Assert.assertEquals(session, mockRequest.getAttribute(Globals.SSL_SESSION_ID_ATTR));
    }


    @Test
    public void testSslCipherUserKeySizeHeaderPresent() throws Exception {
        setUp();
        Integer keySize = Integer.valueOf(452);
        mockRequest.setHeader(valve.getSslCipherUserKeySizeHeader(), String.valueOf(keySize));

        valve.invoke(mockRequest, null);

        Assert.assertEquals(keySize, mockRequest.getAttribute(Globals.KEY_SIZE_ATTR));
    }


    @Test(expected = NumberFormatException.class)
    public void testSslCipherUserKeySizeHeaderBadFormat() throws Exception {
        setUp();
        mockRequest.setHeader(valve.getSslCipherUserKeySizeHeader(), "not-an-integer");

        try {
            valve.invoke(mockRequest, null);
        } catch (NumberFormatException e) {
            Assert.assertNull(mockRequest.getAttribute(Globals.KEY_SIZE_ATTR));
            mockRequest.setHeader(valve.getSslCipherUserKeySizeHeader(), null);
            throw e;
        }
    }


    private static String certificateSingleLine(String[] lines, String separator) {
        StringBuilder singleSpaced = new StringBuilder();
        for (String current : lines) {
            singleSpaced.append(current).append(separator);
        }
        singleSpaced.deleteCharAt(singleSpaced.length() - 1);
        return singleSpaced.toString();
    }


    private static String certificateSingleLine(String separator) {
        return certificateSingleLine(CERTIFICATE_LINES, separator);
    }


    private static String certificateEscaped() throws Exception {
        String cert = certificateSingleLine(CERTIFICATE_LINES, "\n");
        String escaped = new UEncoder(UEncoder.SafeCharsSet.DEFAULT).encodeURL(cert, 0, cert.length()).toString();
        Assert.assertTrue(escaped, escaped.contains("%0a")); // newline is escaped
        Assert.assertTrue(escaped, escaped.contains("%20")); // space is escaped
        Assert.assertTrue(escaped, escaped.contains("%2b")); // + is escaped
        Assert.assertTrue(escaped, escaped.contains("%2f")); // / is escaped
        return escaped;
    }


    private void assertCertificateParsed() throws Exception {
        TesterLogValidationFilter f = TesterLogValidationFilter.add(null, "", null,
                "org.apache.catalina.valves.SSLValve");

        EasyMock.verify(mockNext);

        X509Certificate[] certificates = (X509Certificate[]) mockRequest.getAttribute(Globals.CERTIFICATES_ATTR);
        Assert.assertNotNull(certificates);
        Assert.assertEquals(1, certificates.length);
        Assert.assertNotNull(certificates[0]);
        Assert.assertEquals(0, f.getMessageCount());
    }
}
