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

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;

import jakarta.servlet.http.HttpServletResponse;

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

import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.authenticator.TestBasicAuthParser.BasicAuthHeader;
import org.apache.catalina.realm.MemoryRealm;
import org.apache.catalina.realm.MessageDigestCredentialHandler;
import org.apache.catalina.startup.Catalina;
import org.apache.catalina.startup.HostConfig;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.catalina.storeconfig.StoreConfigLifecycleListener;
import org.apache.catalina.util.IOTools;
import org.apache.catalina.util.ServerInfo;
import org.apache.catalina.util.URLEncoder;


public class TestManagerWebapp extends TomcatBaseTest {

    public static final String CONFIG = "<?xml version=\"1.0\" ?>" +
            "<tomcat-users xmlns=\"http://tomcat.apache.org/xml\"" +
            " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" +
            " xsi:schemaLocation=\"http://tomcat.apache.org/xml/tomcat-users.xsd\"" + " version=\"1.0\">" +
            "<role rolename=\"admin\" />" +
            "<user username=\"admin\" password=\"sekr3t\" roles=\"manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script\" />" +
            "</tomcat-users>";

    /**
     * Integration test for the manager webapp (verify all main Servlets are working).
     *
     * @throws Exception if an error occurs
     */
    @Test
    public void testServlets() throws Exception {
        // Avoid possible locking failure due to deployment with the manager in testDeploy
        ignoreTearDown = true;
        Tomcat tomcat = getTomcatInstance();
        tomcat.setAddDefaultWebXmlToWebapp(false);
        tomcat.addUser("admin", "sekr3t");
        tomcat.addRole("admin", "manager-gui");
        tomcat.addRole("admin", "manager-script");
        tomcat.addRole("admin", "manager-jmx");
        tomcat.addRole("admin", "manager-status");

        File webappDir = new File(getBuildDirectory(), "webapps");

        // Add manager webapp
        File appDir = new File(webappDir, "manager");
        tomcat.addWebapp(null, "/manager", appDir.getAbsolutePath());

        tomcat.start();

        SimpleHttpClient client = new SimpleHttpClient() {
            @Override
            public boolean isResponseBodyOK() {
                return true;
            }
        };
        client.setPort(getPort());
        String basicHeader = (new BasicAuthHeader("Basic", "admin", "sekr3t")).getHeader().toString();

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/html HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_UNAUTHORIZED, client.getStatusCode());

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/html HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("/manager/css/manager.css"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/status HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("MiB"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/jmxproxy HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("Tomcat:type=ThreadPool,name="));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains(" - "));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/sessions?path=/manager HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("[1]"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/resources HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains(" - "));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/serverinfo HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("[" + ServerInfo.getServerInfo() + "]"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/vminfo HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("vmName: "));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/threaddump HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("http-"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/list HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("/manager:running"));

        tomcat.stop();
    }

    @Test
    public void testDeploy() throws Exception {
        // Avoid possible locking failure due to deployment with the manager
        ignoreTearDown = true;
        Tomcat tomcat = getTomcatInstance();
        tomcat.setAddDefaultWebXmlToWebapp(false);
        tomcat.getServer().addLifecycleListener(new StoreConfigLifecycleListener());

        File configFile = new File(getTemporaryDirectory(), "tomcat-users-manager-deploy.xml");
        try (PrintWriter writer = new PrintWriter(configFile)) {
            writer.write(CONFIG);
        }
        addDeleteOnTearDown(configFile);

        MemoryRealm memoryRealm = new MemoryRealm();
        memoryRealm.setCredentialHandler(new MessageDigestCredentialHandler());
        memoryRealm.setPathname(configFile.getAbsolutePath());
        tomcat.getEngine().setRealm(memoryRealm);

        File webappDir = new File(getBuildDirectory(), "webapps");

        // Add manager webapp
        File appDir = new File(webappDir, "manager");
        Context ctx = tomcat.addWebapp(null, "/manager", appDir.getAbsolutePath());

        // Add host config otherwise there's no JMX deployer bean
        HostConfig hostConfig = new HostConfig();
        ctx.getParent().addLifecycleListener(hostConfig);

        tomcat.start();

        SimpleHttpClient client = new SimpleHttpClient() {
            // 10s default too low for some CI systems
            @Override
            public void connect() throws IOException {
                connect(30000, 30000);
            }

            @Override
            public boolean isResponseBodyOK() {
                return true;
            }
        };
        client.setPort(getPort());
        String basicHeader = (new BasicAuthHeader("Basic", "admin", "sekr3t")).getHeader().toString();

        appDir = new File(webappDir, "examples");

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/deploy?war=" + URLEncoder.QUERY.encode(appDir.getAbsolutePath(), StandardCharsets.UTF_8) + " HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("OK - "));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /examples/servlets/servlet/RequestInfoExample HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("/examples/servlets/servlet/RequestInfoExample"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/stop?path=/examples HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

        // @formatter:off
        client.setRequest(new String[] {
                "GET /examples/servlets/servlet/RequestInfoExample HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, client.getStatusCode());

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/start?path=/examples HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

        // @formatter:off
        client.setRequest(new String[] {
                "GET /examples/servlets/servlet/RequestInfoExample HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("/examples/servlets/servlet/RequestInfoExample"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/save HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/save?path=/examples HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

        File serverXml = new File(tomcat.getServer().getCatalinaBase(), Catalina.SERVER_XML);
        Assert.assertTrue(serverXml.canRead());
        addDeleteOnTearDown(serverXml);
        String serverXmlDump;
        try (FileReader reader = new FileReader(serverXml); StringWriter writer = new StringWriter()) {
            IOTools.flow(reader, writer);
            serverXmlDump = writer.toString();
        }
        Assert.assertTrue(serverXmlDump.contains("StoreConfigLifecycleListener"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/reload?path=/examples HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/list HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("/examples:running"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /examples/servlets/servlet/RequestInfoExample HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("/examples/servlets/servlet/RequestInfoExample"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/list HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertTrue(client.getResponseBody().contains("/examples:running"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/undeploy?path=/examples HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/list HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
        Assert.assertFalse(client.getResponseBody().contains("/examples:running"));

        // @formatter:off
        client.setRequest(new String[] {
                "GET /manager/text/findleaks?statusLine=true HTTP/1.1" + CRLF +
                "Host: localhost" + CRLF +
                "Authorization: " + basicHeader + CRLF +
                "Connection: Close" + CRLF +
                CRLF
                });
        // @formatter:on
        client.connect();
        client.processRequest(true);
        Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

        tomcat.stop();
    }


    /*
     * Test case for <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=57700">Bug 57700</a>.
     */
    @Test
    public void testBug57700() throws Exception {
        ignoreTearDown = true;
        Tomcat tomcat = getTomcatInstance();
        tomcat.setAddDefaultWebXmlToWebapp(false);
        File configFile = new File(getTemporaryDirectory(), "tomcat-users-manager-lifecycle.xml");
        try (PrintWriter writer = new PrintWriter(configFile)) {
            writer.write(CONFIG);
        }
        addDeleteOnTearDown(configFile);

        MemoryRealm memoryRealm = new MemoryRealm();
        memoryRealm.setCredentialHandler(new MessageDigestCredentialHandler());
        memoryRealm.setPathname(configFile.getAbsolutePath());
        tomcat.getEngine().setRealm(memoryRealm);

        File webappDir = new File(getBuildDirectory(), "webapps");

        File appDir = new File(webappDir, "manager");
        Context ctx = tomcat.addWebapp(null, "/manager", appDir.getAbsolutePath());

        HostConfig hostConfig = new HostConfig();
        ctx.getParent().addLifecycleListener(hostConfig);

        File appRoot = new File(webappDir, "bug57700");
        Assert.assertTrue(appRoot.mkdirs() && appRoot.isDirectory());
        addDeleteOnTearDown(appRoot);

        try (@SuppressWarnings("unused") TomcatBaseTest.ContainerInjector ignored =
                 TomcatBaseTest.ContainerInjector.inject(ctx.getParent(),
                     c -> c.getPath().equals("/bug57700"),
                     c -> {
                         c.addLifecycleListener(new FailOnceListener());
                         Tomcat.initWebappDefaults(c);
                     })) {

            tomcat.start();
            SimpleHttpClient client = new SimpleHttpClient() {
                @Override
                public void connect() throws IOException {
                    connect(30000, 30000);
                }

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

            client.setPort(getPort());
            String basicHeader = (new BasicAuthHeader("Basic", "admin", "sekr3t")).getHeader().toString();

            client.setRequest(new String[]{
                    "GET /manager/text/deploy?war=" + URLEncoder.QUERY.encode(appRoot.getAbsolutePath(), StandardCharsets.UTF_8) + " HTTP/1.1" + CRLF +
                        "Host: localhost" + CRLF +
                        "Authorization: " + basicHeader + CRLF +
                        "Connection: Close" + CRLF + CRLF
            });
            client.connect();
            client.processRequest(true);
            Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

            client.setRequest(new String[]{
                    "GET /bug57700 HTTP/1.1" + CRLF +
                        "Host: localhost" + CRLF +
                        "Connection: Close" + CRLF + CRLF
            });
            client.connect();
            client.processRequest(true);
            Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, client.getStatusCode());

            client.setRequest(new String[]{
                    "GET /manager/text/start?path=/bug57700 HTTP/1.1" + CRLF +
                        "Host: localhost" + CRLF +
                        "Authorization: " + basicHeader + CRLF +
                        "Connection: Close" + CRLF + CRLF
            });
            client.connect();
            client.processRequest(true);
            Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

            client.setRequest(new String[]{
                    "GET /bug57700 HTTP/1.1" + CRLF +
                        "Host: localhost" + CRLF +
                        "Connection: Close" + CRLF + CRLF
            });
            client.connect();
            client.processRequest(true);
            Assert.assertEquals(HttpServletResponse.SC_FOUND, client.getStatusCode());

            client.setRequest(new String[] {
                    "GET /manager/text/undeploy?path=/bug57700 HTTP/1.1" + CRLF +
                        "Host: localhost" + CRLF +
                        "Authorization: " + basicHeader + CRLF +
                        "Connection: Close" + CRLF +
                        CRLF
            });
            client.connect();
            client.processRequest(true);
            Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());

            tomcat.stop();
        }
    }

    private static class FailOnceListener implements LifecycleListener {
        private volatile boolean firstRun = true;
        @Override
        public void lifecycleEvent(LifecycleEvent event) {
            if (event.getLifecycle() instanceof Context) {
                if (Lifecycle.CONFIGURE_START_EVENT.equals(event.getType()) && firstRun) {
                    firstRun = false;
                    throw new RuntimeException("Configuration failure in first run only");
                }
            }
        }
    }
}
