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

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;

import javax.servlet.AsyncContext;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.catalina.util.URLEncoder;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.UDecoder;

@RunWith(value = Parameterized.class)
public class TestApplicationContextGetRequestDispatcher extends TomcatBaseTest {

    private final boolean useAsync;

    public TestApplicationContextGetRequestDispatcher(boolean useAsync) {
        this.useAsync = useAsync;
    }

    @Parameters(name = "{index}: useAsync[{0}]")
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][]{
                {Boolean.TRUE},
                {Boolean.FALSE}
        });
    }

    @Test
    public void testGetRequestDispatcherNullPath01() throws Exception {
        doTestGetRequestDispatcher(true, "/start", null, null, "/target", DispatcherServlet.NULL);
    }


    @Test
    public void testGetRequestDispatcherNullPath02() throws Exception {
        doTestGetRequestDispatcher(false, "/start", null, null, "/target", DispatcherServlet.NULL);
    }


    @Test
    public void testGetRequestDispatcherOutsideContextRoot01() throws Exception {
        doTestGetRequestDispatcher(
                true, "/start", null, "../outside", "/target", DispatcherServlet.NULL);
    }


    @Test
    public void testGetRequestDispatcherOutsideContextRoot02() throws Exception {
        doTestGetRequestDispatcher(
                false, "/start", null, "../outside", "/target", DispatcherServlet.NULL);
    }


    @Test
    public void testGetRequestDispatcherEncodedTraversal() throws Exception {
        doTestGetRequestDispatcher(
                true, "/prefix/start", null, "%2E%2E/target", "/target", DispatcherServlet.NULL);
    }


    @Test
    public void testGetRequestDispatcherTraversal01() throws Exception {
        doTestGetRequestDispatcher(
                true, "/prefix/start", null, "../target", "/target", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcherTraversal02() throws Exception {
        doTestGetRequestDispatcher(
                false, "/prefix/start", null, "../target", "/target", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcherTraversal03() throws Exception {
        doTestGetRequestDispatcher(
                true, "/prefix/start", null, "../target?a=b", "/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcherTraversal04() throws Exception {
        doTestGetRequestDispatcher(
                false, "/prefix/start", null, "../target?a=b", "/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcherTraversal05() throws Exception {
        doTestGetRequestDispatcher(
                true, "/prefix/start", "a=b", "../target", "/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcherTraversal06() throws Exception {
        doTestGetRequestDispatcher(
                false, "/prefix/start", "a=b", "../target", "/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher01() throws Exception {
        doTestGetRequestDispatcher(
                true, "/prefix/start", null, "target", "/prefix/target", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcher02() throws Exception {
        doTestGetRequestDispatcher(
                false, "/prefix/start", null, "target", "/prefix/target", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcher03() throws Exception {
        doTestGetRequestDispatcher(true, "/prefix/start", null, "target?a=b", "/prefix/target",
                TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher04() throws Exception {
        doTestGetRequestDispatcher(false, "/prefix/start", null, "target?a=b", "/prefix/target",
                TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher05() throws Exception {
        doTestGetRequestDispatcher(true, "/prefix/start", "a=b", "target", "/prefix/target",
                TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher06() throws Exception {
        doTestGetRequestDispatcher(false, "/prefix/start", "a=b", "target", "/prefix/target",
                TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher11() throws Exception {
        doTestGetRequestDispatcher(true, "/aa%3Fbb%3Dcc/start", null, "target",
                "/aa%3Fbb%3Dcc/target", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcher12() throws Exception {
        // Expected to fail because when the RD processes this as unencoded it
        // sees /aa?bb=cc/target which it thinks is a query string. This is why
        // Tomcat encodes by default.
        doTestGetRequestDispatcher(false, "/aa%3Fbb%3Dcc/start", null, "target",
                "/aa%3Fbb%3Dcc/target", Default404Servlet.DEFAULT_404);
    }


    @Test
    public void testGetRequestDispatcher13() throws Exception {
        doTestGetRequestDispatcher(true, "/aa%3Fbb%3Dcc/start", null, "target?a=b",
                "/aa%3Fbb%3Dcc/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher14() throws Exception {
        // Expected to fail because when the RD processes this as unencoded it
        // sees /aa?bb=cc/target which it thinks is a query string. This is why
        // Tomcat encodes by default.
        doTestGetRequestDispatcher(false, "/aa%3Fbb%3Dcc/start", null, "target?a=b",
                "/aa%3Fbb%3Dcc/target", Default404Servlet.DEFAULT_404);
    }


    @Test
    public void testGetRequestDispatcher15() throws Exception {
        doTestGetRequestDispatcher(true, "/aa%3Fbb%3Dcc/start", "a=b", "target",
                "/aa%3Fbb%3Dcc/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher16() throws Exception {
        // Expected to fail because when the RD processes this as unencoded it
        // sees /aa?bb=cc/target which it thinks is a query string. This is why
        // Tomcat encodes by default.
        doTestGetRequestDispatcher(false, "/aa%3Fbb%3Dcc/start", "a=b", "target",
                "/aa%3Fbb%3Dcc/target", Default404Servlet.DEFAULT_404);
    }


    @Test
    public void testGetRequestDispatcher21() throws Exception {
        doTestGetRequestDispatcher(true, "/aa%3Dbb%3Dcc/start", null, "target",
                "/aa%3Dbb%3Dcc/target", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcher22() throws Exception {
        doTestGetRequestDispatcher(false, "/aa%3Dbb%3Dcc/start", null, "target",
                "/aa%3Dbb%3Dcc/target", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcher23() throws Exception {
        doTestGetRequestDispatcher(true, "/aa%3Dbb%3Dcc/start", null, "target?a=b",
                "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher24() throws Exception {
        doTestGetRequestDispatcher(false, "/aa%3Dbb%3Dcc/start", null, "target?a=b",
                "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher25() throws Exception {
        doTestGetRequestDispatcher(true, "/aa%3Dbb%3Dcc/start", "a=b", "target",
                "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher26() throws Exception {
        doTestGetRequestDispatcher(false, "/aa%3Dbb%3Dcc/start", "a=b", "target",
                "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher31() throws Exception {
        doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc",
                "/prefix/aa%3Fbb%3Dcc", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcher32() throws Exception {
        doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc",
                "/prefix/aa%3Fbb%3Dcc", Default404Servlet.DEFAULT_404);
    }


    @Test
    public void testGetRequestDispatcher33() throws Exception {
        doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b",
                "/prefix/aa%3Fbb%3Dcc", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher34() throws Exception {
        doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b",
                "/prefix/aa%3Fbb%3Dcc", Default404Servlet.DEFAULT_404);
    }


    @Test
    public void testGetRequestDispatcher35() throws Exception {
        doTestGetRequestDispatcher(true, "/prefix/start", "a=b", "aa%3Fbb%3Dcc",
                "/prefix/aa%3Fbb%3Dcc", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher36() throws Exception {
        doTestGetRequestDispatcher(false, "/prefix/start", "a=b", "aa%3Fbb%3Dcc",
                "/prefix/aa%3Fbb%3Dcc", Default404Servlet.DEFAULT_404);
    }


    @Test
    public void testGetRequestDispatcher41() throws Exception {
        doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc",
                "/prefix/aa%253Fbb%253Dcc", Default404Servlet.DEFAULT_404);
    }


    @Test
    public void testGetRequestDispatcher42() throws Exception {
        doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc",
                "/prefix/aa%253Fbb%253Dcc", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcher43() throws Exception {
        doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b",
                "/prefix/aa%253Fbb%253Dcc", Default404Servlet.DEFAULT_404);
    }


    @Test
    public void testGetRequestDispatcher44() throws Exception {
        doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b",
                "/prefix/aa%253Fbb%253Dcc", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher45() throws Exception {
        doTestGetRequestDispatcher(true, "/prefix/start", "a=b", "aa%3Fbb%3Dcc",
                "/prefix/aa%253Fbb%253Dcc", Default404Servlet.DEFAULT_404);
    }


    @Test
    public void testGetRequestDispatcher46() throws Exception {
        doTestGetRequestDispatcher(false, "/prefix/start", "a=b", "aa%3Fbb%3Dcc",
                "/prefix/aa%253Fbb%253Dcc", TargetServlet.OK + "a=b");
    }


    @Test
    public void testGetRequestDispatcher47() throws Exception {
        doTestGetRequestDispatcher(true, "/prefix/start", null, "aa+bb",
                "/prefix/aa+bb", TargetServlet.OK);
    }


    @Test
    public void testGetRequestDispatcher48() throws Exception {
        doTestGetRequestDispatcher(false, "/prefix/start", null, "aa+bb",
                "/prefix/aa+bb", TargetServlet.OK);
    }


    private void doTestGetRequestDispatcher(boolean useEncodedDispatchPaths, String startPath,
            String startQueryString, String dispatchPath, String targetPath, String expectedBody)
            throws Exception {

        // Setup Tomcat instance
        Tomcat tomcat = getTomcatInstance();

        // No file system docBase required
        Context ctx = tomcat.addContext("/test", null);
        ctx.setDispatchersUseEncodedPaths(useEncodedDispatchPaths);

        // Add a default servlet to return 404 for not found resources
        Tomcat.addServlet(ctx, "Default", new Default404Servlet());
        ctx.addServletMappingDecoded("/*", "Default");

        // Add a target servlet to dispatch to
        Tomcat.addServlet(ctx, "target", new TargetServlet());
        ctx.addServletMappingDecoded(
                UDecoder.URLDecode(targetPath, StandardCharsets.UTF_8), "target");

        if (useAsync) {
            Wrapper w = Tomcat.addServlet(
                    ctx, "rd", new AsyncDispatcherServlet(dispatchPath, useEncodedDispatchPaths));
            w.setAsyncSupported(true);
        } else {
            Tomcat.addServlet(ctx, "rd", new DispatcherServlet(dispatchPath));
        }
        ctx.addServletMappingDecoded(UDecoder.URLDecode(startPath, StandardCharsets.UTF_8), "rd");

        tomcat.start();

        StringBuilder url = new StringBuilder("http://localhost:");
        url.append(getPort());
        url.append("/test");
        url.append(startPath);
        if (startQueryString != null) {
            url.append('?');
            url.append(startQueryString);
        }

        ByteChunk bc = getUrl(url.toString());
        String body = bc.toString();

        Assert.assertEquals(expectedBody, body);
    }


    private static class Default404Servlet extends HttpServlet {

        private static final long serialVersionUID = 1L;
        private static final String DEFAULT_404 = "DEFAULT-404";

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            resp.setContentType("text/plain");
            resp.setCharacterEncoding("UTF-8");
            resp.getWriter().print(DEFAULT_404);
            resp.setStatus(404);
        }
    }


    private static class DispatcherServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;
        private static final String NULL = "RD-NULL";

        private final String dispatchPath;

        public DispatcherServlet(String dispatchPath) {
            this.dispatchPath = dispatchPath;
        }

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {

            RequestDispatcher rd = req.getRequestDispatcher(dispatchPath);
            if (rd == null) {
                resp.setContentType("text/plain");
                resp.setCharacterEncoding("UTF-8");
                resp.getWriter().print(NULL);
            } else {
                rd.forward(req, resp);
            }
        }
    }


    private static class TargetServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;
        private static final String OK = "OK";

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            resp.setContentType("text/plain");
            resp.setCharacterEncoding("UTF-8");
            resp.getWriter().print(OK);
            String qs = req.getQueryString();
            if (qs != null) {
                resp.getWriter().print(qs);
            }
        }
    }


    private static class AsyncDispatcherServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;
        private static final String NULL = "RD-NULL";

        private final String dispatchPath;
        private final boolean encodePath;

        public AsyncDispatcherServlet(String dispatchPath, boolean encodePath) {
            this.dispatchPath = dispatchPath;
            this.encodePath = encodePath;
        }

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {

            AsyncContext ac = req.startAsync();
            // Quick and dirty. Sufficient for this test but ignores lots of
            // edge cases.
            String target = null;
            if (dispatchPath != null) {
                target = req.getServletPath();
                int lastSlash = target.lastIndexOf('/');
                target = target.substring(0, lastSlash + 1);
                if (encodePath) {
                    target = URLEncoder.DEFAULT.encode(target, StandardCharsets.UTF_8);
                }
                target += dispatchPath;
            }
            try {
                ac.dispatch(target);
            } catch (UnsupportedOperationException uoe) {
                ac.complete();
                resp.setContentType("text/plain");
                resp.setCharacterEncoding("UTF-8");
                resp.getWriter().print(NULL);
            }
        }
    }
}
