//
//  ========================================================================
//  Copyright (c) 1995-2019 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;

import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.IO;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class ForwardedRequestCustomizerTest
{
    private Server _server;
    private LocalConnector _connector;
    private RequestHandler _handler;
    final Deque<String> _results = new ArrayDeque<>();
    final AtomicBoolean _wasSecure = new AtomicBoolean(false);
    final AtomicReference<String> _sslSession = new AtomicReference<>();
    final AtomicReference<String> _sslCertificate = new AtomicReference<>();
    
    ForwardedRequestCustomizer _customizer;

    @BeforeEach
    public void init() throws Exception
    {
        _server = new Server();
        HttpConnectionFactory http = new HttpConnectionFactory();
        http.setInputBufferSize(1024);
        http.getHttpConfiguration().setRequestHeaderSize(512);
        http.getHttpConfiguration().setResponseHeaderSize(512);
        http.getHttpConfiguration().setOutputBufferSize(2048);
        http.getHttpConfiguration().addCustomizer(_customizer=new ForwardedRequestCustomizer());
        _connector = new LocalConnector(_server,http);
        _server.addConnector(_connector);
        _handler = new RequestHandler();
        _server.setHandler(_handler);

        _handler._checker = new RequestTester()
        {
            @Override
            public boolean check(HttpServletRequest request,HttpServletResponse response)
            {
                _wasSecure.set(request.isSecure());
                _sslSession.set(String.valueOf(request.getAttribute("javax.servlet.request.ssl_session_id")));
                _sslCertificate.set(String.valueOf(request.getAttribute("javax.servlet.request.cipher_suite")));
                _results.add(request.getScheme());
                _results.add(request.getServerName());
                _results.add(Integer.toString(request.getServerPort()));
                _results.add(request.getRemoteAddr());
                _results.add(Integer.toString(request.getRemotePort()));
                return true;
            }
        };
        
        _server.start();
    }

    @AfterEach
    public void destroy() throws Exception
    {
        _server.stop();
        _server.join();
    }

    @Test
    public void testHostIpv4() throws Exception
    {
        String response=_connector.getResponse(
            "GET / HTTP/1.1\n"+
                "Host: 1.2.3.4:2222\n"+
                "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("1.2.3.4",_results.poll());
        assertEquals("2222",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
    }

    @Test
    public void testHostIpv6() throws Exception
    {
        String response=_connector.getResponse(
            "GET / HTTP/1.1\n"+
                "Host: [::1]:2222\n"+
                "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("[::1]",_results.poll());
        assertEquals("2222",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
    }



    @Test
    public void testURIIpv4() throws Exception
    {
        String response=_connector.getResponse(
            "GET http://1.2.3.4:2222/ HTTP/1.1\n"+
                "Host: wrong\n"+
                "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("1.2.3.4",_results.poll());
        assertEquals("2222",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
    }

    @Test
    public void testURIIpv6() throws Exception
    {
        String response=_connector.getResponse(
            "GET http://[::1]:2222/ HTTP/1.1\n"+
                "Host: wrong\n"+
                "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("[::1]",_results.poll());
        assertEquals("2222",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
    }


    @Test
    public void testRFC7239_Examples_4() throws Exception
    {
        String response=_connector.getResponse(
            "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Forwarded: for=\"_gazonk\"\n"+
             "Forwarded: For=\"[2001:db8:cafe::17]:4711\"\n"+
             "Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43\n"+
             "Forwarded: for=192.0.2.43, for=198.51.100.17\n"+
            "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("[2001:db8:cafe::17]",_results.poll());
        assertEquals("4711",_results.poll());
    }
    
    @Test
    public void testRFC7239_Examples_7_1() throws Exception
    {
        _connector.getResponse(
            "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Forwarded: for=192.0.2.43,for=\"[2001:db8:cafe::17]\",for=unknown\n"+
            "\n");
        _connector.getResponse(
            "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Forwarded: for=192.0.2.43, for=\"[2001:db8:cafe::17]\", for=unknown\n"+
            "\n");
        _connector.getResponse(
            "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Forwarded: for=192.0.2.43\n"+
             "Forwarded: for=\"[2001:db8:cafe::17]\", for=unknown\n"+
            "\n");

        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("192.0.2.43",_results.poll());
        assertEquals("0",_results.poll());
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("192.0.2.43",_results.poll());
        assertEquals("0",_results.poll());
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("192.0.2.43",_results.poll());
        assertEquals("0",_results.poll());
    }

    @Test
    public void testRFC7239_Examples_7_4() throws Exception
    {
        _connector.getResponse(
            "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Forwarded: for=192.0.2.43, for=\"[2001:db8:cafe::17]\"\n"+
            "\n");

        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("192.0.2.43",_results.poll());
        assertEquals("0",_results.poll());
    }

    @Test
    public void testRFC7239_Examples_7_5() throws Exception
    {
        _connector.getResponse(
            "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Forwarded: for=192.0.2.43,for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com\n"+
            "\n");

        assertEquals("http",_results.poll());
        assertEquals("example.com",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("192.0.2.43",_results.poll());
        assertEquals("0",_results.poll());
    }

    @Test
    public void testProto() throws Exception
    {
        String response=_connector.getResponse(
            "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "X-Forwarded-Proto: foobar\n"+
             "Forwarded: proto=https\n"+
            "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("https",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("443",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
    }

    @Test
    public void testFor() throws Exception
    {
        String response=_connector.getResponse(
            "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "X-Forwarded-For: 10.9.8.7,6.5.4.3\n"+
             "X-Forwarded-For: 8.9.8.7,7.5.4.3\n"+
            "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("10.9.8.7",_results.poll());
        assertEquals("0",_results.poll());
    }

    @Test
    public void testForIpv4WithPort() throws Exception
    {
        String response=_connector.getResponse(
            "GET / HTTP/1.1\n"+
                "Host: myhost\n"+
                "X-Forwarded-For: 10.9.8.7:1111,6.5.4.3:2222\n"+
                "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("10.9.8.7",_results.poll());
        assertEquals("1111",_results.poll());
    }

    @Test
    public void testForIpv6WithPort() throws Exception
    {
        String response=_connector.getResponse(
            "GET / HTTP/1.1\n"+
                "Host: myhost\n"+
                "X-Forwarded-For: [2001:db8:cafe::17]:1111,6.5.4.3:2222\n"+
                "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("[2001:db8:cafe::17]",_results.poll());
        assertEquals("1111",_results.poll());
    }

    @Test
    public void testForIpv6AndPort() throws Exception
    {
        String response=_connector.getResponse(
                "GET / HTTP/1.1\n"+
                        "Host: myhost\n"+
                        "X-Forwarded-For: 1:2:3:4:5:6:7:8\n"+
                        "X-Forwarded-Port: 2222\n"+
                        "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("[1:2:3:4:5:6:7:8]",_results.poll());
        assertEquals("2222",_results.poll());

        response=_connector.getResponse(
            "GET / HTTP/1.1\n"+
                "Host: myhost\n"+
                "X-Forwarded-Port: 2222\n"+
                "X-Forwarded-For: 1:2:3:4:5:6:7:8\n"+
                "X-Forwarded-For: 7:7:7:7:7:7:7:7\n"+
                "X-Forwarded-Port: 3333\n"+
                "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("[1:2:3:4:5:6:7:8]",_results.poll());
        assertEquals("2222",_results.poll());
    }

    @Test
    public void testLegacyProto() throws Exception
    {
        String response=_connector.getResponse(
            "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "X-Proxied-Https: on\n"+
            "\n");
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("https",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("443",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
        assertTrue(_wasSecure.get());
    }

    @Test
    public void testSslSession() throws Exception
    {
        _customizer.setSslIsSecure(false);
        String response=_connector.getResponse(
             "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Proxy-Ssl-Id: Wibble\n"+
             "\n");
        
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
        assertFalse(_wasSecure.get());
        assertEquals("Wibble",_sslSession.get());
      
        _customizer.setSslIsSecure(true);  
        response=_connector.getResponse(
             "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Proxy-Ssl-Id: 0123456789abcdef\n"+
             "\n");
        
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("https",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("443",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
        assertTrue(_wasSecure.get());
        assertEquals("0123456789abcdef",_sslSession.get());
    }
    
    @Test
    public void testSslCertificate() throws Exception
    {
        _customizer.setSslIsSecure(false);
        String response=_connector.getResponse(
             "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Proxy-auth-cert: Wibble\n"+
             "\n");
        
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("http",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
        assertFalse(_wasSecure.get());
        assertEquals("Wibble",_sslCertificate.get());
        
      
        _customizer.setSslIsSecure(true);  
        response=_connector.getResponse(
             "GET / HTTP/1.1\n"+
             "Host: myhost\n"+
             "Proxy-auth-cert: 0123456789abcdef\n"+
             "\n");
        
        assertThat(response, Matchers.containsString("200 OK"));
        assertEquals("https",_results.poll());
        assertEquals("myhost",_results.poll());
        assertEquals("443",_results.poll());
        assertEquals("0.0.0.0",_results.poll());
        assertEquals("0",_results.poll());
        assertTrue(_wasSecure.get());
        assertEquals("0123456789abcdef",_sslCertificate.get());
    }


    @Test
    public void testMixed() throws Exception
    {
        String response = _connector.getResponse(
            "GET / HTTP/1.1\n" +
                "Host: myhost\n" +
                "X-Forwarded-For: 11.9.8.7:1111,8.5.4.3:2222\n" +
                "X-Forwarded-Port: 3333\n" +
                "Forwarded: for=192.0.2.43,for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com\n"+
                "X-Forwarded-For: 11.9.8.7:1111,8.5.4.3:2222\n" +
                "\n");

        assertEquals("http",_results.poll());
        assertEquals("example.com",_results.poll());
        assertEquals("80",_results.poll());
        assertEquals("192.0.2.43",_results.poll());
        assertEquals("0",_results.poll());
    }
    
    
    interface RequestTester
    {
        boolean check(HttpServletRequest request,HttpServletResponse response) throws IOException;
    }

    private class RequestHandler extends AbstractHandler
    {
        private RequestTester _checker;
        @SuppressWarnings("unused")
        private String _content;

        @Override
        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
        {
            ((Request)request).setHandled(true);

            if (request.getContentLength()>0
                    && !MimeTypes.Type.FORM_ENCODED.asString().equals(request.getContentType())
                    && !request.getContentType().startsWith("multipart/form-data"))
                _content=IO.toString(request.getInputStream());

            if (_checker!=null && _checker.check(request,response))
                response.setStatus(200);
            else
                response.sendError(500);
        }
    }
}
