//
//  ========================================================================
//  Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.server.handler;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import static org.junit.Assume.assumeNoException;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.resource.PathResource;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class AllowSymLinkAliasCheckerTest
{
    @Parameterized.Parameters(name = "{0}")
    public static List<Object[]> params()
    {
        List<Object[]> data = new ArrayList<>();

        String dirs[] = {"/testdir/", "/testdirlnk/", "/testdirprefixlnk/", "/testdirsuffixlnk/",
                "/testdirwraplnk/"};

        for (String dirname : dirs)
        {
            data.add(new Object[]{dirname, 200, "text/html", "Directory: " + dirname});
            data.add(new Object[]{dirname + "testfile.txt", 200, "text/plain", "Hello TestFile"});
            data.add(new Object[]{dirname + "testfilelnk.txt", 200, "text/plain", "Hello TestFile"});
            data.add(new Object[]{dirname + "testfileprefixlnk.txt", 200, "text/plain", "Hello TestFile"});
        }

        return data;
    }

    private Server server;
    private LocalConnector localConnector;
    private Path rootPath;

    @Before
    public void setup() throws Exception
    {
        setupRoot();
        setupServer();
    }

    @After
    public void teardown() throws Exception
    {
        if( server != null )
        {
            server.stop();
        }
    }

    private void setupRoot() throws IOException
    {
        rootPath = MavenTestingUtils.getTargetTestingPath(AllowSymLinkAliasCheckerTest.class.getSimpleName());
        FS.ensureEmpty(rootPath);

        Path testdir = rootPath.resolve("testdir");
        FS.ensureDirExists(testdir);

        try
        {
            // If we used testdir (Path) from above, these symlinks
            // would point to an absolute path.

            // Create a relative symlink testdirlnk -> testdir
            Files.createSymbolicLink(rootPath.resolve("testdirlnk"), new File("testdir").toPath());
            // Create a relative symlink testdirprefixlnk -> ./testdir
            Files.createSymbolicLink(rootPath.resolve("testdirprefixlnk"), new File("./testdir").toPath());
            // Create a relative symlink testdirsuffixlnk -> testdir/
            Files.createSymbolicLink(rootPath.resolve("testdirsuffixlnk"), new File("testdir/").toPath());
            // Create a relative symlink testdirwraplnk -> ./testdir/
            Files.createSymbolicLink(rootPath.resolve("testdirwraplnk"), new File("./testdir/").toPath());
        }
        catch (UnsupportedOperationException | FileSystemException e)
        {
            // If unable to create symlink, no point testing the rest.
            // This is the path that Microsoft Windows takes.
            assumeNoException(e);
        }

        Path testfileTxt = testdir.resolve("testfile.txt");
        Files.createFile(testfileTxt);
        try (OutputStream out = Files.newOutputStream(testfileTxt))
        {
            out.write("Hello TestFile".getBytes(StandardCharsets.UTF_8));
        }

        try
        {
            Path testfileTxtLnk = testdir.resolve("testfilelnk.txt");
            // Create a relative symlink testfilelnk.txt -> testfile.txt
            // If we used testfileTxt (Path) from above, this symlink
            // would point to an absolute path.
            Files.createSymbolicLink(testfileTxtLnk, new File("testfile.txt").toPath());
        }
        catch (UnsupportedOperationException | FileSystemException e)
        {
            // If unable to create symlink, no point testing the rest.
            // This is the path that Microsoft Windows takes.
            assumeNoException(e);
        }

        try
        {
            Path testfilePrefixTxtLnk = testdir.resolve("testfileprefixlnk.txt");
            // Create a relative symlink testfileprefixlnk.txt -> ./testfile.txt
            // If we used testfileTxt (Path) from above, this symlink
            // would point to an absolute path.
            Files.createSymbolicLink(testfilePrefixTxtLnk, new File("./testfile.txt").toPath());
        }
        catch (UnsupportedOperationException | FileSystemException e)
        {
            // If unable to create symlink, no point testing the rest.
            // This is the path that Microsoft Windows takes.
            assumeNoException(e);
        }
    }

    private void setupServer() throws Exception
    {
        // Setup server
        server = new Server();
        localConnector = new LocalConnector(server);
        server.addConnector(localConnector);

        ResourceHandler fileResourceHandler = new ResourceHandler();
        fileResourceHandler.setDirectoriesListed(true);
        fileResourceHandler.setWelcomeFiles(new String[]{"index.html"});
        fileResourceHandler.setEtags(true);

        ContextHandler fileResourceContext = new ContextHandler();
        fileResourceContext.setContextPath("/");
        fileResourceContext.setAllowNullPathInfo(true);
        fileResourceContext.setHandler(fileResourceHandler);
        fileResourceContext.setBaseResource(new PathResource(rootPath));

        fileResourceContext.clearAliasChecks();
        fileResourceContext.addAliasCheck(new AllowSymLinkAliasChecker());

        server.setHandler(fileResourceContext);
        server.start();
    }

    @Parameterized.Parameter(0)
    public String requestURI;
    @Parameterized.Parameter(1)
    public int expectedResponseStatus;
    @Parameterized.Parameter(2)
    public String expectedResponseContentType;
    @Parameterized.Parameter(3)
    public String expectedResponseContentContains;

    public AllowSymLinkAliasCheckerTest()
    {
    }

    @Test(timeout = 5000)
    public void testAccess() throws Exception
    {
        HttpTester.Request request = HttpTester.newRequest();

        request.setMethod("GET");
        request.setHeader("Host", "tester");
        request.setURI(requestURI);

        String responseString = localConnector.getResponse(BufferUtil.toString(request.generate()));
        assertThat("Response status code", responseString, startsWith("HTTP/1.1 " + expectedResponseStatus + " "));
        assertThat("Response Content-Type", responseString, containsString("\nContent-Type: " + expectedResponseContentType));
        assertThat("Response", responseString, containsString(expectedResponseContentContains));
    }
}
