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

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.SocketException;

import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServlet;

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

import org.apache.catalina.Context;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.catalina.valves.TesterAccessLogValve;

public class TestMimeHeadersIntegration extends TomcatBaseTest {

    private HeaderCountLogValve alv;

    private void setupHeadersTest(Tomcat tomcat) {
        Context ctx = tomcat.addContext("", getTemporaryDirectory()
                .getAbsolutePath());
        Tomcat.addServlet(ctx, "servlet", new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            public void service(ServletRequest req, ServletResponse res)
                    throws ServletException, IOException {
                res.setContentType("text/plain; charset=ISO-8859-1");
                res.getWriter().write("OK");
            }
        });
        ctx.addServletMappingDecoded("/", "servlet");

        alv = new HeaderCountLogValve();
        tomcat.getHost().getPipeline().addValve(alv);
    }

    private void runHeadersTest(final boolean successExpected,
            final Tomcat tomcat, final int count,
            final int expectedMaxHeaderCount) throws Exception {
        tomcat.start();

        String header = "A:B" + SimpleHttpClient.CRLF;
        StringBuilder request = new StringBuilder();
        request.append("GET / HTTP/1.0" + SimpleHttpClient.CRLF);
        for (int i = 0; i < count; i++) {
            request.append(header);
        }
        request.append(SimpleHttpClient.CRLF);

        Client client = new Client(tomcat);
        client.setRequest(new String[] { request.toString() });
        try {
            client.connect();
            client.processRequest();
            client.disconnect();
        } catch (SocketException ex) {
            // Connection was closed by Tomcat
            if (successExpected) {
                // unexpected
                log.error(ex.getMessage(), ex);
            } else {
                log.warn(ex.getMessage(), ex);
            }
        }
        if (successExpected) {
            alv.validateAccessLog(1, 200, 0, 3000);
            // Response 200
            Assert.assertTrue("Response line is: " + client.getResponseLine(),
                    client.getResponseLine() != null && client.isResponse200());
            Assert.assertEquals("OK", client.getResponseBody());
        } else {
            alv.validateAccessLog(1, 400, 0, 3000);
            // Connection cancelled or response 400
            Assert.assertTrue("Response line is: " + client.getResponseLine(),
                    client.getResponseLine() == null || client.isResponse400());
        }
        int maxHeaderCount =
                ((Integer) tomcat.getConnector().getProperty("maxHeaderCount")).intValue();
        Assert.assertEquals(expectedMaxHeaderCount, maxHeaderCount);
        if (maxHeaderCount > 0) {
            Assert.assertEquals(maxHeaderCount, alv.arraySize);
        } else if (maxHeaderCount < 0) {
            int maxHttpHeaderSize = ((Integer) tomcat.getConnector().getProperty("maxHttpHeaderSize")).intValue();
            int headerCount = Math.min(count,
                    maxHttpHeaderSize / header.length() + 1);
            int arraySize = 1;
            while (arraySize < headerCount) {
                arraySize <<= 1;
            }
            Assert.assertEquals(arraySize, alv.arraySize);
        }
    }

    @Test
    public void testHeaderLimits1() throws Exception {
        // Bumping into maxHttpHeaderSize
        Tomcat tomcat = getTomcatInstance();
        setupHeadersTest(tomcat);
        Assert.assertTrue(tomcat.getConnector().setProperty("maxHeaderCount", "-1"));
        runHeadersTest(false, tomcat, 8 * 1024, -1);
    }

    @Test
    public void testHeaderLimits2() throws Exception {
        // Can process 100 headers
        Tomcat tomcat = getTomcatInstance();
        setupHeadersTest(tomcat);
        runHeadersTest(true, tomcat, 100, 100);
    }

    @Test
    public void testHeaderLimits3() throws Exception {
        // Cannot process 101 header
        Tomcat tomcat = getTomcatInstance();
        setupHeadersTest(tomcat);
        runHeadersTest(false, tomcat, 101, 100);
    }

    @Test
    public void testHeaderLimits4() throws Exception {
        // Can change maxHeaderCount
        Tomcat tomcat = getTomcatInstance();
        setupHeadersTest(tomcat);
        Assert.assertTrue(tomcat.getConnector().setProperty("maxHeaderCount", "-1"));
        runHeadersTest(true, tomcat, 300, -1);
    }

    private static final class HeaderCountLogValve extends TesterAccessLogValve {
        public volatile int arraySize = -1;

        @Override
        public void log(Request request, Response response, long time) {
            super.log(request, response, time);
            try {
                MimeHeaders mh = request.getCoyoteRequest().getMimeHeaders();
                Field headersArrayField = MimeHeaders.class
                        .getDeclaredField("headers");
                headersArrayField.setAccessible(true);
                arraySize = ((Object[]) headersArrayField.get(mh)).length;
            } catch (Exception ex) {
                Assert.assertNull(ex.getMessage(), ex);
            }
        }
    }

    private static final class Client extends SimpleHttpClient {
        Client(Tomcat tomcat) {
            setPort(tomcat.getConnector().getLocalPort());
        }

        @Override
        public boolean isResponseBodyOK() {
            return true;
        }
    }
}
