//
//  ========================================================================
//  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.net.InetSocketAddress;

import javax.servlet.ServletRequest;

import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.QuotedCSV;
import org.eclipse.jetty.server.HttpConfiguration.Customizer;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;


/* ------------------------------------------------------------ */
/** Customize Requests for Proxy Forwarding.
 * <p>
 * This customizer looks at at HTTP request for headers that indicate
 * it has been forwarded by one or more proxies.  Specifically handled are
 * <ul>
 * <li>{@code Forwarded}, as defined by <a href="https://tools.ietf.org/html/rfc7239">rfc7239</a>
 * <li>{@code X-Forwarded-Host}</li>
 * <li>{@code X-Forwarded-Server}</li>
 * <li>{@code X-Forwarded-For}</li>
 * <li>{@code X-Forwarded-Proto}</li>
 * <li>{@code X-Proxied-Https}</li>
 * </ul>
 * <p>If these headers are present, then the {@link Request} object is updated
 * so that the proxy is not seen as the other end point of the connection on which
 * the request came</p>
 * <p>Headers can also be defined so that forwarded SSL Session IDs and Cipher
 * suites may be customised</p> 
 * @see <a href="http://en.wikipedia.org/wiki/X-Forwarded-For">Wikipedia: X-Forwarded-For</a>
 */
public class ForwardedRequestCustomizer implements Customizer
{
    private static final Logger LOG = Log.getLogger(ForwardedRequestCustomizer.class);

    private HostPortHttpField _forcedHost;
    private String _forwardedHeader = HttpHeader.FORWARDED.toString();
    private String _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString();
    private String _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString();
    private String _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString();
    private String _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString();
    private String _forwardedHttpsHeader = "X-Proxied-Https";
    private String _forwardedCipherSuiteHeader = "Proxy-auth-cert";
    private String _forwardedSslSessionIdHeader = "Proxy-ssl-id";
    private boolean _proxyAsAuthority=false;
    private boolean _sslIsSecure=true;
    
    /**
     * @return true if the proxy address obtained via
     * {@code X-Forwarded-Server} or RFC7239 "by" is used as
     * the request authority. Default false
     */
    public boolean getProxyAsAuthority()
    {
        return _proxyAsAuthority;
    }

    /**
     * @param proxyAsAuthority if true, use the proxy address obtained via
     * {@code X-Forwarded-Server} or RFC7239 "by" as the request authority.
     */
    public void setProxyAsAuthority(boolean proxyAsAuthority)
    {
        _proxyAsAuthority = proxyAsAuthority;
    }

    /**
     * @param rfc7239only Configure to only support the RFC7239 Forwarded header and to
     *                    not support any {@code X-Forwarded-} headers.   This convenience method
     *                    clears all the non RFC headers if passed true and sets them to
     *                    the default values (if not already set) if passed false.
     */
    public void setForwardedOnly(boolean rfc7239only)
    {
        if (rfc7239only)
        {
            if (_forwardedHeader==null)
                _forwardedHeader=HttpHeader.FORWARDED.toString();
            _forwardedHostHeader=null;
            _forwardedHostHeader=null;
            _forwardedServerHeader=null;
            _forwardedForHeader=null;
            _forwardedProtoHeader=null;
            _forwardedHttpsHeader=null;
        }
        else
        {
            if (_forwardedHostHeader==null)
                _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString();
            if (_forwardedServerHeader==null)
                _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString();
            if (_forwardedForHeader==null)
                _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString();
            if (_forwardedProtoHeader==null)
                _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString();
            if (_forwardedHttpsHeader==null)
                _forwardedHttpsHeader = "X-Proxied-Https";
        }
    }
    
    public String getForcedHost()
    {
        return _forcedHost.getValue();
    }
    
    /**
     * Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}.
     *
     * @param hostAndPort
     *            The value of the host header to force.
     */
    public void setForcedHost(String hostAndPort)
    {
        _forcedHost = new HostPortHttpField(hostAndPort);
    }

    /**
     * @return The header name for RFC forwarded (default Forwarded)
     */
    public String getForwardedHeader()
    {
        return _forwardedHeader;
    }

    /**
     * @param forwardedHeader 
     *            The header name for RFC forwarded (default Forwarded)
     */
    public void setForwardedHeader(String forwardedHeader)
    {
        _forwardedHeader = forwardedHeader;
    }

    public String getForwardedHostHeader()
    {
        return _forwardedHostHeader;
    }

    /**
     * @param forwardedHostHeader
     *            The header name for forwarded hosts (default {@code X-Forwarded-Host})
     */
    public void setForwardedHostHeader(String forwardedHostHeader)
    {
        _forwardedHostHeader = forwardedHostHeader;
    }

    /**
     * @return the header name for forwarded server.
     */
    public String getForwardedServerHeader()
    {
        return _forwardedServerHeader;
    }

    /**
     * @param forwardedServerHeader
     *            The header name for forwarded server (default {@code X-Forwarded-Server})
     */
    public void setForwardedServerHeader(String forwardedServerHeader)
    {
        _forwardedServerHeader = forwardedServerHeader;
    }

    /**
     * @return the forwarded for header
     */
    public String getForwardedForHeader()
    {
        return _forwardedForHeader;
    }

    /**
     * @param forwardedRemoteAddressHeader
     *            The header name for forwarded for (default {@code X-Forwarded-For})
     */
    public void setForwardedForHeader(String forwardedRemoteAddressHeader)
    {
        _forwardedForHeader = forwardedRemoteAddressHeader;
    }

    /**
     * Get the forwardedProtoHeader.
     *
     * @return the forwardedProtoHeader (default {@code X-Forwarded-Proto})
     */
    public String getForwardedProtoHeader()
    {
        return _forwardedProtoHeader;
    }

    /**
     * Set the forwardedProtoHeader.
     *
     * @param forwardedProtoHeader
     *            the forwardedProtoHeader to set (default {@code X-Forwarded-Proto})
     */
    public void setForwardedProtoHeader(String forwardedProtoHeader)
    {
        _forwardedProtoHeader = forwardedProtoHeader;
    }

    /**
     * @return The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert})
     */
    public String getForwardedCipherSuiteHeader()
    {
        return _forwardedCipherSuiteHeader;
    }

    /**
     * @param forwardedCipherSuite
     *            The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert})
     */
    public void setForwardedCipherSuiteHeader(String forwardedCipherSuite)
    {
        _forwardedCipherSuiteHeader = forwardedCipherSuite;
    }

    /**
     * @return The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id})
     */
    public String getForwardedSslSessionIdHeader()
    {
        return _forwardedSslSessionIdHeader;
    }

    /**
     * @param forwardedSslSessionId
     *            The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id})
     */
    public void setForwardedSslSessionIdHeader(String forwardedSslSessionId)
    {
        _forwardedSslSessionIdHeader = forwardedSslSessionId;
    }

    /**
     * @return The header name holding a forwarded Https status indicator (on|off true|false) (default {@code X-Proxied-Https})
     */
    public String getForwardedHttpsHeader()
    {
        return _forwardedHttpsHeader;
    }

    /**
     * @param forwardedHttpsHeader the header name holding a forwarded Https status indicator(default {@code X-Proxied-Https})
     */
    public void setForwardedHttpsHeader(String forwardedHttpsHeader)
    {
        _forwardedHttpsHeader = forwardedHttpsHeader;
    }
    
    /**
     * @return true if the presence of a SSL session or certificate header is sufficient
     * to indicate a secure request (default is true)
     */
    public boolean isSslIsSecure()
    {
        return _sslIsSecure;
    }

    /**
     * @param sslIsSecure true if the presence of a SSL session or certificate header is sufficient
     * to indicate a secure request (default is true)
     */
    public void setSslIsSecure(boolean sslIsSecure)
    {
        _sslIsSecure = sslIsSecure;
    }

    @Override
    public void customize(Connector connector, HttpConfiguration config, Request request)
    {
        HttpFields httpFields = request.getHttpFields();

        RFC7239 rfc7239 = null;
        String forwardedHost = null;
        String forwardedServer = null;
        HostPort forwardedFor = null;
        String forwardedProto = null;
        String forwardedHttps = null;
        
        // Do a single pass through the header fields as it is a more efficient single iteration.
        for (HttpField field : httpFields)
        {
            String name = field.getName();
            
            if (getForwardedCipherSuiteHeader()!=null && getForwardedCipherSuiteHeader().equalsIgnoreCase(name))
            {
                request.setAttribute("javax.servlet.request.cipher_suite",field.getValue());
                if (isSslIsSecure())
                {
                    request.setSecure(true);
                    request.setScheme(config.getSecureScheme());
                }
            }
            
            if (getForwardedSslSessionIdHeader()!=null && getForwardedSslSessionIdHeader().equalsIgnoreCase(name))
            {
                request.setAttribute("javax.servlet.request.ssl_session_id", field.getValue());
                if (isSslIsSecure())
                {
                    request.setSecure(true);
                    request.setScheme(config.getSecureScheme());
                }
            }
            
            if (forwardedHost==null && _forwardedHostHeader!=null && _forwardedHostHeader.equalsIgnoreCase(name))
                forwardedHost = getLeftMost(field.getValue());
            
            if (forwardedServer==null && _forwardedServerHeader!=null && _forwardedServerHeader.equalsIgnoreCase(name))
                forwardedServer = getLeftMost(field.getValue());
            
            if (forwardedFor==null && _forwardedForHeader!=null && _forwardedForHeader.equalsIgnoreCase(name))
                forwardedFor = getRemoteAddr(field.getValue());
            
            if (forwardedProto==null && _forwardedProtoHeader!=null && _forwardedProtoHeader.equalsIgnoreCase(name))
                forwardedProto = getLeftMost(field.getValue());
            
            if (forwardedHttps==null && _forwardedHttpsHeader!=null && _forwardedHttpsHeader.equalsIgnoreCase(name))
                forwardedHttps = getLeftMost(field.getValue());
            
            if (_forwardedHeader!=null && _forwardedHeader.equalsIgnoreCase(name))
            {
                if (rfc7239==null)
                    rfc7239= new RFC7239();
                rfc7239.addValue(field.getValue());
            }
        }
        
        // Handle host header if if not available any RFC7230.by or X-ForwardedServer header      
        if (_forcedHost != null)
        {
            // Update host header
            httpFields.put(_forcedHost);
            request.setAuthority(_forcedHost.getHost(),_forcedHost.getPort());
        }
        else if (rfc7239!=null && rfc7239._host!=null)
        {
            HostPortHttpField auth = rfc7239._host;
            httpFields.put(auth);
            request.setAuthority(auth.getHost(),auth.getPort());
        }
        else if (forwardedHost != null)
        {
            HostPortHttpField auth = new HostPortHttpField(forwardedHost);
            httpFields.put(auth);
            request.setAuthority(auth.getHost(),auth.getPort());
        }
        else if (_proxyAsAuthority)
        {
            if (rfc7239!=null && rfc7239._by!=null)
            {
                HostPortHttpField auth = rfc7239._by;
                httpFields.put(auth);
                request.setAuthority(auth.getHost(),auth.getPort());
            }
            else if (forwardedServer != null)
            {
                request.setAuthority(forwardedServer,request.getServerPort());
            }
        }

        // handle remote end identifier
        if (rfc7239!=null && rfc7239._for!=null)
        {
            request.setRemoteAddr(InetSocketAddress.createUnresolved(rfc7239._for.getHost(),rfc7239._for.getPort()));
        }
        else if (forwardedFor != null)
        {
            request.setRemoteAddr(InetSocketAddress.createUnresolved(forwardedFor.getHost(), (forwardedFor.getPort() > 0) ? forwardedFor.getPort() : request.getRemotePort()));
        }

        // handle protocol identifier
        if (rfc7239!=null && rfc7239._proto!=null)
        {
            request.setScheme(rfc7239._proto);
            if (rfc7239._proto.equals(config.getSecureScheme()))
                request.setSecure(true);
        }
        else if (forwardedProto != null)
        {
            request.setScheme(forwardedProto);
            if (forwardedProto.equals(config.getSecureScheme()))
                request.setSecure(true);
        }
        else if (forwardedHttps !=null && ("on".equalsIgnoreCase(forwardedHttps)||"true".equalsIgnoreCase(forwardedHttps)))
        {
            request.setScheme(HttpScheme.HTTPS.asString());
            if (HttpScheme.HTTPS.asString().equals(config.getSecureScheme()))
                request.setSecure(true);
        }
    }

    /* ------------------------------------------------------------ */
    protected String getLeftMost(String headerValue)
    {
        if (headerValue == null)
            return null;

        int commaIndex = headerValue.indexOf(',');

        if (commaIndex == -1)
        {
            // Single value
            return headerValue;
        }

        // The left-most value is the farthest downstream client
        return headerValue.substring(0,commaIndex).trim();
    }

    protected HostPort getRemoteAddr(String headerValue)
    {
        String leftMost = getLeftMost(headerValue);
        if (leftMost == null)
        {
            return null;
        }

        try
        {
            return new HostPort(leftMost);
        }
        catch (Exception e)
        {
            // failed to parse in host[:port] format
            LOG.ignore(e);
            return null;
        }
    }
    
    @Override
    public String toString()
    {
        return String.format("%s@%x",this.getClass().getSimpleName(),hashCode());
    }

    @Deprecated
    public String getHostHeader()
    {
        return _forcedHost.getValue();
    }
    
    /**
     * Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}.
     *
     * @param hostHeader
     *            The value of the host header to force.
     */
    @Deprecated
    public void setHostHeader(String hostHeader)
    {
        _forcedHost = new HostPortHttpField(hostHeader);
    }

    private final class RFC7239 extends QuotedCSV
    {
        HostPortHttpField _by;
        HostPortHttpField _for;
        HostPortHttpField _host;
        String _proto;
        
        private RFC7239()
        {
            super(false);
        }

        @Override
        protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
        {
            if (valueLength==0 && paramValue>paramName)
            {
                String name=StringUtil.asciiToLowerCase(buffer.substring(paramName,paramValue-1));
                String value=buffer.substring(paramValue);
                switch(name)
                {
                    case "by":
                        if (_by==null && !value.startsWith("_") && !"unknown".equals(value))
                            _by=new HostPortHttpField(value);
                        break;
                    case "for":
                        if (_for==null && !value.startsWith("_") && !"unknown".equals(value))
                            _for=new HostPortHttpField(value);
                        break;
                    case "host":
                        if (_host==null)
                            _host=new HostPortHttpField(value);
                        break;
                    case "proto":
                        if (_proto==null)
                            _proto=value;
                        break;
                }
            }
        }
    }
}
