package org.mortbay.jetty.servlet;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;

import org.mortbay.jetty.LocalConnector;
import org.mortbay.jetty.Server;
import org.mortbay.servlet.NoJspServlet;
import org.mortbay.util.IO;
import org.mortbay.util.StringUtil;

public class DefaultServletTest extends TestCase
{
    private Server server;
    private LocalConnector connector;
    private Context context;
    private String os = System.getProperty("os.name").toLowerCase();

    protected void setUp() throws Exception
    {
        super.setUp();

        server = new Server();
        server.setSendServerVersion(false);

        connector = new LocalConnector();

        context = new Context();
        context.setContextPath("/context");
        context.setWelcomeFiles(new String[] {"index.html","index.jsp","index.htm"}); // no welcome files

        server.addHandler(context);
        server.addConnector(connector);

        server.start();
    }

    protected void tearDown() throws Exception
    {
        super.tearDown();

        if (server != null)
        {
            server.stop();
        }
    }
    
    public void testListingWithSession() throws Exception
    {
        ServletHolder defholder = context.addServlet(DefaultServlet.class,"/*");
        defholder.setInitParameter("dirAllowed","true");
        defholder.setInitParameter("redirectWelcome","false");
        defholder.setInitParameter("gzip","false");

        File testDir = new File("target/tests/" + getName());
        prepareEmptyTestDir(testDir);
        
        /* create some content in the docroot */
        File resBase = new File(testDir, "docroot");
        resBase.mkdirs();
        new File(resBase, "one").mkdir();
        new File(resBase, "two").mkdir();
        new File(resBase, "three").mkdir();

        String resBasePath = resBase.getAbsolutePath();
        defholder.setInitParameter("resourceBase",resBasePath);

        StringBuffer req1 = new StringBuffer();
        req1.append("GET /context/;JSESSIONID=1234567890 HTTP/1.1\n");
        req1.append("Host: localhost\n");
        req1.append("\n");

        String response = connector.getResponses(req1.toString());

        assertResponseContains("/one/;JSESSIONID=1234567890",response);
        assertResponseContains("/two/;JSESSIONID=1234567890",response);
        assertResponseContains("/three/;JSESSIONID=1234567890",response);

        assertResponseNotContains("<script>",response);
    }

    public void testListingXSS() throws Exception
    {
        ServletHolder defholder = context.addServlet(DefaultServlet.class,"/*");
        defholder.setInitParameter("dirAllowed","true");
        defholder.setInitParameter("redirectWelcome","false");
        defholder.setInitParameter("gzip","false");

        File testDir = new File("target/tests/" + getName());
        prepareEmptyTestDir(testDir);
        
        /* create some content in the docroot */
        File resBase = new File(testDir, "docroot");
        resBase.mkdirs();
        new File(resBase, "one").mkdir();
        new File(resBase, "two").mkdir();
        new File(resBase, "three").mkdir();
        if (os.indexOf("windows")<0)
            assertTrue("Creating dir 'f??r' (Might not work in Windows)", new File(resBase, "f??r").mkdir());

        String resBasePath = resBase.getAbsolutePath();
        defholder.setInitParameter("resourceBase",resBasePath);

        StringBuffer req1 = new StringBuffer();
        /* Intentionally bad request URI.
         * Sending a non-encoded URI with typically encoded characters '<', '>', and '"'.
         */
        req1.append("GET /context/;<script>window.alert(\"hi\");</script> HTTP/1.1\n");
        req1.append("Host: localhost\n");
        req1.append("\n");

        String response = connector.getResponses(req1.toString());

        assertResponseContains("/one/",response);
        assertResponseContains("/two/",response);
        assertResponseContains("/three/",response);

        if (os.indexOf("windows")<0)
            assertResponseContains("/f%3F%3Fr",response);

        assertResponseNotContains("<script>",response);
    }
    
    public void testListingProperUrlEncoding() throws Exception
    {
        ServletHolder defholder = context.addServlet(DefaultServlet.class,"/*");
        defholder.setInitParameter("dirAllowed","true");
        defholder.setInitParameter("redirectWelcome","false");
        defholder.setInitParameter("gzip","false");

        File testDir = new File("target/tests/" + getName());
        prepareEmptyTestDir(testDir);
        
        /* create some content in the docroot */
        File resBase = new File(testDir, "docroot");
        resBase.mkdirs();
        File wackyDir = new File(resBase, "dir;"); // this should not be double-encoded.
        assertTrue(wackyDir.mkdirs());
        
        new File(wackyDir, "four").mkdir();
        new File(wackyDir, "five").mkdir();
        new File(wackyDir, "six").mkdir();

        /* At this point we have the following
         * testListingProperUrlEncoding/
         * `-- docroot
         *     `-- dir;
         *         |-- five
         *         |-- four
         *         `-- six
         */
        
        String resBasePath = resBase.getAbsolutePath();
        defholder.setInitParameter("resourceBase",resBasePath);

        // First send request in improper, unencoded way.
        String response = connector.getResponses("GET /context/dir;/ HTTP/1.0\r\n\r\n");
        
        assertResponseContains("HTTP/1.1 404 Not Found", response);
        
        
        // Now send request in proper, encoded format.
        connector.reopen();
        StringBuffer req2 = new StringBuffer();
        response = connector.getResponses("GET /context/dir%3B/ HTTP/1.0\r\n\r\n");
        
        // Should not see double-encoded ";"
        // First encoding: ";" -> "%3b"
        // Second encoding: "%3B" -> "%253B" (BAD!)
        assertResponseNotContains("%253B",response);
        
        assertResponseContains("/dir%3B/",response);
        assertResponseContains("/dir%3B/four/",response);
        assertResponseContains("/dir%3B/five/",response);
        assertResponseContains("/dir%3B/six/",response);
    }
    
    public void testListingContextBreakout() throws Exception
    {
        ServletHolder defholder = context.addServlet(DefaultServlet.class,"/*");
        defholder.setInitParameter("dirAllowed","true");
        defholder.setInitParameter("redirectWelcome","false");
        defholder.setInitParameter("gzip","false");
        defholder.setInitParameter("aliases","true");

        File testDir = new File("target/tests/" + getName());
        prepareEmptyTestDir(testDir);
        
        /* create some content in the docroot */
        File resBase = new File(testDir, "docroot");
        resBase.mkdirs();

        File index = new File(resBase, "index.html");
        createFile(index, "<h1>Hello Index</h1>");
        File wackyDir;
        if (os.indexOf("windows")<0)
        {
            wackyDir = new File(resBase, "dir?");
            assertTrue(wackyDir.mkdirs());
        }
        wackyDir = new File(resBase, "dir;");
        assertTrue(wackyDir.mkdirs());

        /* create some content outside of the docroot */
        File sekret = new File(testDir, "sekret");
        sekret.mkdirs();
        File pass = new File(sekret, "pass");
        createFile(pass, "Sssh, you shouldn't be seeing this");
        
        /* At this point we have the following
         * testListingContextBreakout/
         * |-- docroot
         * |   |-- index.html
         * |   |-- dir?
         * |   |-- dir;
         * `-- sekret
         *     `-- pass
         */
        
        String resBasePath = resBase.getAbsolutePath();
        defholder.setInitParameter("resourceBase",resBasePath);

        String response;

        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("<h1>Hello Index</h1>",response);

        connector.reopen();
        response= connector.getResponses("GET /context/dir?/ HTTP/1.0\r\n\r\n");
        assertResponseContains("404",response);

        connector.reopen();
        response= connector.getResponses("GET /context/dir%3F/ HTTP/1.0\r\n\r\n");
        if (os.indexOf("windows")<0)
            assertResponseContains("Directory: /context/dir?/<",response);
        else
            assertResponseContains("404",response);

        connector.reopen();
        response= connector.getResponses("GET /context/index.html HTTP/1.0\r\n\r\n");
        assertResponseContains("Hello Index",response);
        
        connector.reopen();
        response= connector.getResponses("GET /context/dir%3F/../index.html HTTP/1.0\r\n\r\n");
        assertResponseContains("Hello Index",response);

        connector.reopen();
        response= connector.getResponses("GET /context/dir%3F/../../ HTTP/1.0\r\n\r\n");
        assertResponseNotContains("Directory: ",response);
        
        connector.reopen();
        response= connector.getResponses("GET /context/dir%3F/../../sekret/pass HTTP/1.0\r\n\r\n");
        assertResponseNotContains("Sssh",response);

        connector.reopen();
        response= connector.getResponses("GET /context/dir?/../../ HTTP/1.0\r\n\r\n");
        assertResponseNotContains("Directory: ",response);
        
        connector.reopen();
        response= connector.getResponses("GET /context/dir?/../../sekret/pass HTTP/1.0\r\n\r\n");
        assertResponseNotContains("Sssh",response);
        
        

        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("<h1>Hello Index</h1>",response);

        connector.reopen();
        response= connector.getResponses("GET /context/dir;/ HTTP/1.0\r\n\r\n");
        assertResponseContains("404",response);
        
        connector.reopen();
        response= connector.getResponses("GET /context/dir%3B/ HTTP/1.0\r\n\r\n");
        assertResponseContains("Directory: /context/dir;/<",response);

        connector.reopen();
        response= connector.getResponses("GET /context/index.html HTTP/1.0\r\n\r\n");
        assertResponseContains("Hello Index",response);
        
        connector.reopen();
        response= connector.getResponses("GET /context/dir%3B/../index.html HTTP/1.0\r\n\r\n");
        assertResponseContains("Hello Index",response);

        connector.reopen();
        response= connector.getResponses("GET /context/dir%3B/../../ HTTP/1.0\r\n\r\n");
        assertResponseNotContains("Directory: ",response);
        
        connector.reopen();
        response= connector.getResponses("GET /context/dir%3B/../../sekret/pass HTTP/1.0\r\n\r\n");
        assertResponseNotContains("Sssh",response);

        connector.reopen();
        response= connector.getResponses("GET /context/dir;/../../ HTTP/1.0\r\n\r\n");
        assertResponseNotContains("Directory: ",response);
        
        connector.reopen();
        response= connector.getResponses("GET /context/dir;/../../sekret/pass HTTP/1.0\r\n\r\n");
        assertResponseNotContains("Sssh",response);
        
    }



    public void testWelcome() throws Exception
    {
        File testDir = new File("target/tests/" + getName());
        prepareEmptyTestDir(testDir);
        File resBase = new File(testDir, "docroot");
        resBase.mkdirs();
        File inde = new File(resBase, "index.htm");
        File index = new File(resBase, "index.html");
        

        String resBasePath = resBase.getAbsolutePath();

        ServletHolder defholder = context.addServlet(DefaultServlet.class,"/");
        defholder.setInitParameter("dirAllowed","false");
        defholder.setInitParameter("redirectWelcome","false");
        defholder.setInitParameter("welcomeServlets","false");
        defholder.setInitParameter("gzip","false");
        defholder.setInitParameter("resourceBase",resBasePath);
        
        ServletHolder jspholder = context.addServlet(NoJspServlet.class,"*.jsp");

        String response;

        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("403",response);

        createFile(index, "<h1>Hello Index</h1>");
        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("<h1>Hello Index</h1>",response);
        
        createFile(inde, "<h1>Hello Inde</h1>");
        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("<h1>Hello Index</h1>",response);

        index.delete();
        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("<h1>Hello Inde</h1>",response);
        
        inde.delete();
        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("403",response);

    }

    public void testWelcomeServlet() throws Exception
    {
        File testDir = new File("target/tests/" + getName());
        prepareEmptyTestDir(testDir);
        File resBase = new File(testDir, "docroot");
        resBase.mkdirs();
        File inde = new File(resBase, "index.htm");
        File index = new File(resBase, "index.html");
        

        String resBasePath = resBase.getAbsolutePath();

        ServletHolder defholder = context.addServlet(DefaultServlet.class,"/");
        defholder.setInitParameter("dirAllowed","false");
        defholder.setInitParameter("redirectWelcome","false");
        defholder.setInitParameter("welcomeServlets","true");
        defholder.setInitParameter("gzip","false");
        defholder.setInitParameter("resourceBase",resBasePath);
        
        ServletHolder jspholder = context.addServlet(NoJspServlet.class,"*.jsp");

        String response;

        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("JSP support not configured",response);

        createFile(index, "<h1>Hello Index</h1>");
        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("<h1>Hello Index</h1>",response);
        
        createFile(inde, "<h1>Hello Inde</h1>");
        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("<h1>Hello Index</h1>",response);

        index.delete();
        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("<h1>Hello Inde</h1>",response);
        
        inde.delete();
        connector.reopen();
        response= connector.getResponses("GET /context/ HTTP/1.0\r\n\r\n");
        assertResponseContains("JSP support not configured",response);
    }
    

    public void testRangeRequests() throws Exception
    {
        File testDir = new File("target/tests/" + getName());
        prepareEmptyTestDir(testDir);
        File resBase = new File(testDir, "docroot");
        resBase.mkdirs();
        File data = new File(resBase, "data.txt");
        createFile(data,"01234567890123456789012345678901234567890123456789012345678901234567890123456789");
        String resBasePath = resBase.getAbsolutePath();
        
     

        ServletHolder defholder = context.addServlet(DefaultServlet.class,"/");
        defholder.setInitParameter("acceptRanges","true");
        defholder.setInitParameter("resourceBase",resBasePath);
        
        String response;

        connector.reopen();
        response= connector.getResponses(
                "GET /context/data.txt HTTP/1.1\r\n"+
                "Host: localhost\r\n"+
                "\r\n");
        assertResponseContains("200 OK",response);
        assertResponseContains("Accept-Ranges: bytes",response);

        connector.reopen();
        response= connector.getResponses(
                "GET /context/data.txt HTTP/1.1\r\n"+
                "Host: localhost\r\n"+
                "Range: bytes=0-9\r\n"+
                "\r\n");
        assertResponseContains("206 Partial",response);
        assertResponseContains("Content-Type: text/plain",response);
        assertResponseContains("Content-Length: 10",response);
        assertResponseContains("Content-Range: bytes 0-9/80",response);

        connector.reopen();
        response= connector.getResponses(
                "GET /context/data.txt HTTP/1.1\r\n"+
                "Host: localhost\r\n"+
                "Range: bytes=0-9,20-29,40-49\r\n"+
                "\r\n");
        int start = response.indexOf("--jetty");
        String body=response.substring(start);
        String boundary = body.substring(0,body.indexOf("\r\n"));
        assertResponseContains("206 Partial",response);
        assertResponseContains("Content-Type: multipart/byteranges; boundary=",response);
        assertResponseContains("Content-Range: bytes 0-9/80",response);
        assertResponseContains("Content-Range: bytes 20-29/80",response);
        assertResponseContains("Content-Length: "+body.length(),response);
        assertTrue(body.endsWith(boundary+"--\r\n"));

        connector.reopen();
        response= connector.getResponses(
                "GET /context/data.txt HTTP/1.1\r\n"+
                "Host: localhost\r\n"+
                "Range: bytes=0-9,20-29,40-49,70-79\r\n"+
                "\r\n");
        start = response.indexOf("--jetty");
        body=response.substring(start);
        boundary = body.substring(0,body.indexOf("\r\n"));
        assertResponseContains("206 Partial",response);
        assertResponseContains("Content-Type: multipart/byteranges; boundary=",response);
        assertResponseContains("Content-Range: bytes 0-9/80",response);
        assertResponseContains("Content-Range: bytes 20-29/80",response);
        assertResponseContains("Content-Range: bytes 70-79/80",response);
        assertResponseContains("Content-Length: "+body.length(),response);
        assertTrue(body.endsWith(boundary+"--\r\n"));

        connector.reopen();
        response= connector.getResponses(
                "GET /context/data.txt HTTP/1.1\r\n"+
                "Host: localhost\r\n"+
                "Range: bytes=0-9,20-29,40-49,60-60,70-79\r\n"+
                "\r\n");
        start = response.indexOf("--jetty");
        body=response.substring(start);
        boundary = body.substring(0,body.indexOf("\r\n"));
        assertResponseContains("206 Partial",response);
        assertResponseContains("Content-Type: multipart/byteranges; boundary=",response);
        assertResponseContains("Content-Range: bytes 0-9/80",response);
        assertResponseContains("Content-Range: bytes 20-29/80",response);
        assertResponseContains("Content-Range: bytes 60-60/80",response);
        assertResponseContains("Content-Range: bytes 70-79/80",response);
        assertResponseContains("Content-Length: "+body.length(),response);
        assertTrue(body.endsWith(boundary+"--\r\n"));
    }


    private void createFile(File file, String str) throws IOException
    {
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(file);
            out.write(str.getBytes(StringUtil.__UTF8));
            out.flush();
        } finally {
            IO.close(out);
        }
    }

    private void prepareEmptyTestDir(File testdir)
    {
        if (testdir.exists())
        {
            emptyDir(testdir);
        }
        else
        {
            testdir.mkdirs();
        }

        assertTrue("test dir should exists",testdir.exists());
        assertTrue("test dir should be a dir",testdir.isDirectory());
        assertTrue("test dir should be empty",isEmpty(testdir));
    }

    private boolean isEmpty(File dir)
    {
        if (!dir.isDirectory())
        {
            return true;
        }

        return dir.list().length == 0;
    }

    private void emptyDir(File dir)
    {
        File entries[] = dir.listFiles();
        for (int i = 0; i < entries.length; i++)
        {
            deletePath(entries[i]);
        }
    }

    private void deletePath(File path)
    {
        if (path.isDirectory())
        {
            File entries[] = path.listFiles();
            for (int i = 0; i < entries.length; i++)
            {
                deletePath(entries[i]);
            }
        }

        assertTrue("Deleting: " + path.getAbsolutePath(),path.delete());
    }

    private void assertResponseNotContains(String forbidden, String response)
    {
        int idx = response.indexOf(forbidden);
        if (idx != (-1))
        {
            // Found (when should not have)
            StringBuffer err = new StringBuffer();
            err.append("Response contain forbidden string \"").append(forbidden).append("\"");
            err.append("\n").append(response);

            System.err.println(err);
            throw new AssertionFailedError(err.toString());
        }
    }

    private int assertResponseContains(String expected, String response)
    {
        int idx = response.indexOf(expected);
        if (idx == (-1))
        {
            // Not found
            StringBuffer err = new StringBuffer();
            err.append("Response does not contain expected string \"").append(expected).append("\"");
            err.append("\n").append(response);

            System.err.println(err);
            throw new AssertionFailedError(err.toString());
        }
        return idx;
    }
}
