//
// ========================================================================
// 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.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
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.QuotedCSVParser;
import org.eclipse.jetty.server.HttpConfiguration.Customizer;
import org.eclipse.jetty.util.ArrayTrie;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.Trie;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import static java.lang.invoke.MethodType.methodType;
Customize Requests for Proxy Forwarding.
This customizer looks at at HTTP request for headers that indicate
it has been forwarded by one or more proxies. Specifically handled are
Forwarded
, as defined by rfc7239
X-Forwarded-Host
X-Forwarded-Server
X-Forwarded-For
X-Forwarded-Proto
X-Proxied-Https
If these headers are present, then the Request
object is updated so that the proxy is not seen as the other end point of the connection on which the request came
Headers can also be defined so that forwarded SSL Session IDs and Cipher
suites may be customised
See Also:
/**
* 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 boolean _proxyAsAuthority = false;
private boolean _forwardedPortAsAuthority = true;
private String _forwardedHeader = HttpHeader.FORWARDED.toString();
private String _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString();
private String _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString();
private String _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString();
private String _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString();
private String _forwardedPortHeader = HttpHeader.X_FORWARDED_PORT.toString();
private String _forwardedHttpsHeader = "X-Proxied-Https";
private String _forwardedCipherSuiteHeader = "Proxy-auth-cert";
private String _forwardedSslSessionIdHeader = "Proxy-ssl-id";
private boolean _sslIsSecure = true;
private Trie<MethodHandle> _handles;
public ForwardedRequestCustomizer()
{
updateHandles();
}
Returns: true if the proxy address obtained via X-Forwarded-Server
or RFC7239 "by" is used as the request authority. Default false
/**
* @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;
}
Params: - proxyAsAuthority – if true, use the proxy address obtained via
X-Forwarded-Server
or RFC7239 "by" as the request authority.
/**
* @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;
}
Params: - rfc7239only – Configure to only support the RFC7239 Forwarded header and to not support any
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.
/**
* @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;
_forwardedServerHeader = null;
_forwardedForHeader = null;
_forwardedPortHeader = 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 (_forwardedPortHeader == null)
_forwardedPortHeader = HttpHeader.X_FORWARDED_PORT.toString();
if (_forwardedProtoHeader == null)
_forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString();
if (_forwardedHttpsHeader == null)
_forwardedHttpsHeader = "X-Proxied-Https";
}
updateHandles();
}
public String getForcedHost()
{
return _forcedHost.getValue();
}
Set a forced valued for the host header to control what is returned by ServletRequest.getServerName()
and ServletRequest.getServerPort()
. Params: - hostAndPort – The value of the host header to force.
/**
* 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(new ForcedHostPort(hostAndPort));
}
Returns: The header name for RFC forwarded (default Forwarded)
/**
* @return The header name for RFC forwarded (default Forwarded)
*/
public String getForwardedHeader()
{
return _forwardedHeader;
}
Params: - forwardedHeader – The header name for RFC forwarded (default Forwarded)
/**
* @param forwardedHeader The header name for RFC forwarded (default Forwarded)
*/
public void setForwardedHeader(String forwardedHeader)
{
if (_forwardedHeader == null || !_forwardedHeader.equals(forwardedHeader))
{
_forwardedHeader = forwardedHeader;
updateHandles();
}
}
public String getForwardedHostHeader()
{
return _forwardedHostHeader;
}
Params: - forwardedHostHeader – The header name for forwarded hosts (default
X-Forwarded-Host
)
/**
* @param forwardedHostHeader The header name for forwarded hosts (default {@code X-Forwarded-Host})
*/
public void setForwardedHostHeader(String forwardedHostHeader)
{
if (_forwardedHostHeader == null || !_forwardedHostHeader.equalsIgnoreCase(forwardedHostHeader))
{
_forwardedHostHeader = forwardedHostHeader;
updateHandles();
}
}
Returns: the header name for forwarded server.
/**
* @return the header name for forwarded server.
*/
public String getForwardedServerHeader()
{
return _forwardedServerHeader;
}
Params: - forwardedServerHeader – The header name for forwarded server (default
X-Forwarded-Server
)
/**
* @param forwardedServerHeader The header name for forwarded server (default {@code X-Forwarded-Server})
*/
public void setForwardedServerHeader(String forwardedServerHeader)
{
if (_forwardedServerHeader == null || !_forwardedServerHeader.equalsIgnoreCase(forwardedServerHeader))
{
_forwardedServerHeader = forwardedServerHeader;
updateHandles();
}
}
Returns: the forwarded for header
/**
* @return the forwarded for header
*/
public String getForwardedForHeader()
{
return _forwardedForHeader;
}
Params: - forwardedRemoteAddressHeader – The header name for forwarded for (default
X-Forwarded-For
)
/**
* @param forwardedRemoteAddressHeader The header name for forwarded for (default {@code X-Forwarded-For})
*/
public void setForwardedForHeader(String forwardedRemoteAddressHeader)
{
if (_forwardedForHeader == null || !_forwardedForHeader.equalsIgnoreCase(forwardedRemoteAddressHeader))
{
_forwardedForHeader = forwardedRemoteAddressHeader;
updateHandles();
}
}
public String getForwardedPortHeader()
{
return _forwardedHostHeader;
}
Params: - forwardedPortHeader – The header name for forwarded hosts (default
X-Forwarded-Port
)
/**
* @param forwardedPortHeader The header name for forwarded hosts (default {@code X-Forwarded-Port})
*/
public void setForwardedPortHeader(String forwardedPortHeader)
{
if (_forwardedHostHeader == null || !_forwardedHostHeader.equalsIgnoreCase(forwardedPortHeader))
{
_forwardedHostHeader = forwardedPortHeader;
updateHandles();
}
}
Returns: if true, the X-Forwarded-Port header applies to the authority,
else it applies to the remote client address
/**
* @return if true, the X-Forwarded-Port header applies to the authority,
* else it applies to the remote client address
*/
public boolean getForwardedPortAsAuthority()
{
return _forwardedPortAsAuthority;
}
Set if the X-Forwarded-Port header will be used for Authority
Params: - forwardedPortAsAuthority – if true, the X-Forwarded-Port header applies to the authority,
else it applies to the remote client address
/**
* Set if the X-Forwarded-Port header will be used for Authority
*
* @param forwardedPortAsAuthority if true, the X-Forwarded-Port header applies to the authority,
* else it applies to the remote client address
*/
public void setForwardedPortAsAuthority(boolean forwardedPortAsAuthority)
{
_forwardedPortAsAuthority = forwardedPortAsAuthority;
}
Get the forwardedProtoHeader.
Returns: the forwardedProtoHeader (default X-Forwarded-Proto
)
/**
* Get the forwardedProtoHeader.
*
* @return the forwardedProtoHeader (default {@code X-Forwarded-Proto})
*/
public String getForwardedProtoHeader()
{
return _forwardedProtoHeader;
}
Set the forwardedProtoHeader.
Params: - forwardedProtoHeader – the forwardedProtoHeader to set (default
X-Forwarded-Proto
)
/**
* Set the forwardedProtoHeader.
*
* @param forwardedProtoHeader the forwardedProtoHeader to set (default {@code X-Forwarded-Proto})
*/
public void setForwardedProtoHeader(String forwardedProtoHeader)
{
if (_forwardedProtoHeader == null || !_forwardedProtoHeader.equalsIgnoreCase(forwardedProtoHeader))
{
_forwardedProtoHeader = forwardedProtoHeader;
updateHandles();
}
}
Returns: The header name holding a forwarded cipher suite (default Proxy-auth-cert
)
/**
* @return The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert})
*/
public String getForwardedCipherSuiteHeader()
{
return _forwardedCipherSuiteHeader;
}
Params: - forwardedCipherSuiteHeader – The header name holding a forwarded cipher suite (default
Proxy-auth-cert
)
/**
* @param forwardedCipherSuiteHeader The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert})
*/
public void setForwardedCipherSuiteHeader(String forwardedCipherSuiteHeader)
{
if (_forwardedCipherSuiteHeader == null || !_forwardedCipherSuiteHeader.equalsIgnoreCase(forwardedCipherSuiteHeader))
{
_forwardedCipherSuiteHeader = forwardedCipherSuiteHeader;
updateHandles();
}
}
Returns: The header name holding a forwarded SSL Session ID (default Proxy-ssl-id
)
/**
* @return The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id})
*/
public String getForwardedSslSessionIdHeader()
{
return _forwardedSslSessionIdHeader;
}
Params: - forwardedSslSessionIdHeader – The header name holding a forwarded SSL Session ID (default
Proxy-ssl-id
)
/**
* @param forwardedSslSessionIdHeader The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id})
*/
public void setForwardedSslSessionIdHeader(String forwardedSslSessionIdHeader)
{
if (_forwardedSslSessionIdHeader == null || !_forwardedSslSessionIdHeader.equalsIgnoreCase(forwardedSslSessionIdHeader))
{
_forwardedSslSessionIdHeader = forwardedSslSessionIdHeader;
updateHandles();
}
}
Returns: The header name holding a forwarded Https status indicator (on|off true|false) (default X-Proxied-Https
)
/**
* @return The header name holding a forwarded Https status indicator (on|off true|false) (default {@code X-Proxied-Https})
*/
public String getForwardedHttpsHeader()
{
return _forwardedHttpsHeader;
}
Params: - forwardedHttpsHeader – the header name holding a forwarded Https status indicator(default
X-Proxied-Https
)
/**
* @param forwardedHttpsHeader the header name holding a forwarded Https status indicator(default {@code X-Proxied-Https})
*/
public void setForwardedHttpsHeader(String forwardedHttpsHeader)
{
if (_forwardedHttpsHeader == null || !_forwardedHttpsHeader.equalsIgnoreCase(forwardedHttpsHeader))
{
_forwardedHttpsHeader = forwardedHttpsHeader;
updateHandles();
}
}
Returns: true if the presence of a SSL session or certificate header is sufficient
to indicate a secure request (default is true)
/**
* @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;
}
Params: - sslIsSecure – true if the presence of a SSL session or certificate header is sufficient
to indicate a secure request (default is true)
/**
* @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();
// Do a single pass through the header fields as it is a more efficient single iteration.
Forwarded forwarded = new Forwarded(request, config);
try
{
for (HttpField field : httpFields)
{
MethodHandle handle = _handles.get(field.getName());
if (handle != null)
handle.invoke(forwarded, field);
}
}
catch (Throwable e)
{
throw new RuntimeException(e);
}
if (forwarded._proto != null)
{
request.setScheme(forwarded._proto);
if (forwarded._proto.equalsIgnoreCase(config.getSecureScheme()))
request.setSecure(true);
}
if (forwarded._host != null)
{
httpFields.put(new HostPortHttpField(forwarded._host));
request.setAuthority(forwarded._host.getHost(), forwarded._host.getPort());
}
if (forwarded._for != null)
{
int port = forwarded._for.getPort() > 0 ? forwarded._for.getPort() : request.getRemotePort();
request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._for.getHost(), port));
}
}
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();
}
@Override
public String toString()
{
return String.format("%s@%x", this.getClass().getSimpleName(), hashCode());
}
public String getHostHeader()
{
return _forcedHost.getValue();
}
Set a forced valued for the host header to control what is returned by ServletRequest.getServerName()
and ServletRequest.getServerPort()
. Params: - hostHeader – The value of the host header to force.
/**
* 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.
*/
public void setHostHeader(String hostHeader)
{
_forcedHost = new HostPortHttpField(hostHeader);
}
private void updateHandles()
{
int size = 0;
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = methodType(Void.TYPE, HttpField.class);
while (true)
{
try
{
size += 128;
_handles = new ArrayTrie<>(size);
if (_forwardedCipherSuiteHeader != null && !_handles.put(_forwardedCipherSuiteHeader, lookup.findVirtual(Forwarded.class, "handleCipherSuite", type)))
continue;
if (_forwardedSslSessionIdHeader != null && !_handles.put(_forwardedSslSessionIdHeader, lookup.findVirtual(Forwarded.class, "handleSslSessionId", type)))
continue;
if (_forwardedHeader != null && !_handles.put(_forwardedHeader, lookup.findVirtual(Forwarded.class, "handleRFC7239", type)))
continue;
if (_forwardedForHeader != null && !_handles.put(_forwardedForHeader, lookup.findVirtual(Forwarded.class, "handleFor", type)))
continue;
if (_forwardedPortHeader != null && !_handles.put(_forwardedPortHeader, lookup.findVirtual(Forwarded.class, "handlePort", type)))
continue;
if (_forwardedHostHeader != null && !_handles.put(_forwardedHostHeader, lookup.findVirtual(Forwarded.class, "handleHost", type)))
continue;
if (_forwardedProtoHeader != null && !_handles.put(_forwardedProtoHeader, lookup.findVirtual(Forwarded.class, "handleProto", type)))
continue;
if (_forwardedHttpsHeader != null && !_handles.put(_forwardedHttpsHeader, lookup.findVirtual(Forwarded.class, "handleHttps", type)))
continue;
if (_forwardedServerHeader != null && !_handles.put(_forwardedServerHeader, lookup.findVirtual(Forwarded.class, "handleServer", type)))
continue;
break;
}
catch (NoSuchMethodException | IllegalAccessException e)
{
throw new IllegalStateException(e);
}
}
}
private static class ForcedHostPort extends HostPort
{
ForcedHostPort(String authority)
{
super(authority);
}
}
private static class PossiblyPartialHostPort extends HostPort
{
PossiblyPartialHostPort(String authority)
{
super(authority);
}
protected PossiblyPartialHostPort(String host, int port)
{
super(host, port);
}
}
private static class PortSetHostPort extends PossiblyPartialHostPort
{
PortSetHostPort(String host, int port)
{
super(host, port);
}
}
private static class Rfc7239HostPort extends HostPort
{
Rfc7239HostPort(String authority)
{
super(authority);
}
}
private class Forwarded extends QuotedCSVParser
{
HttpConfiguration _config;
Request _request;
boolean _protoRfc7239;
String _proto;
HostPort _for;
HostPort _host;
public Forwarded(Request request, HttpConfiguration config)
{
super(false);
_request = request;
_config = config;
if (_forcedHost != null)
_host = _forcedHost.getHostPort();
}
public void handleCipherSuite(HttpField field)
{
_request.setAttribute("javax.servlet.request.cipher_suite", field.getValue());
if (isSslIsSecure())
{
_request.setSecure(true);
_request.setScheme(_config.getSecureScheme());
}
}
public void handleSslSessionId(HttpField field)
{
_request.setAttribute("javax.servlet.request.ssl_session_id", field.getValue());
if (isSslIsSecure())
{
_request.setSecure(true);
_request.setScheme(_config.getSecureScheme());
}
}
public void handleHost(HttpField field)
{
if (_forwardedPortAsAuthority && !StringUtil.isEmpty(_forwardedPortHeader))
{
if (_host == null)
_host = new PossiblyPartialHostPort(getLeftMost(field.getValue()));
else if (_for instanceof PortSetHostPort)
_host = new HostPort(HostPort.normalizeHost(getLeftMost(field.getValue())), _host.getPort());
}
else if (_host == null)
{
_host = new HostPort(getLeftMost(field.getValue()));
}
}
public void handleServer(HttpField field)
{
if (_proxyAsAuthority)
return;
handleHost(field);
}
public void handleProto(HttpField field)
{
if (_proto == null)
_proto = getLeftMost(field.getValue());
}
public void handleFor(HttpField field)
{
if (!_forwardedPortAsAuthority && !StringUtil.isEmpty(_forwardedPortHeader))
{
if (_for == null)
_for = new PossiblyPartialHostPort(getLeftMost(field.getValue()));
else if (_for instanceof PortSetHostPort)
_for = new HostPort(HostPort.normalizeHost(getLeftMost(field.getValue())), _for.getPort());
}
else if (_for == null)
{
_for = new HostPort(getLeftMost(field.getValue()));
}
}
public void handlePort(HttpField field)
{
if (!_forwardedPortAsAuthority)
{
if (_for == null)
_for = new PortSetHostPort(_request.getRemoteHost(), field.getIntValue());
else if (_for instanceof PossiblyPartialHostPort && _for.getPort() <= 0)
_for = new HostPort(HostPort.normalizeHost(_for.getHost()), field.getIntValue());
}
else
{
if (_host == null)
_host = new PortSetHostPort(_request.getServerName(), field.getIntValue());
else if (_host instanceof PossiblyPartialHostPort && _host.getPort() <= 0)
_host = new HostPort(HostPort.normalizeHost(_host.getHost()), field.getIntValue());
}
}
public void handleHttps(HttpField field)
{
if (_proto == null && ("on".equalsIgnoreCase(field.getValue()) || "true".equalsIgnoreCase(field.getValue())))
_proto = HttpScheme.HTTPS.asString();
}
public void handleRFC7239(HttpField field)
{
addValue(field.getValue());
}
@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 (!_proxyAsAuthority)
break;
if (value.startsWith("_") || "unknown".equals(value))
break;
if (_proxyAsAuthority && (_host == null || !(_host instanceof Rfc7239HostPort)))
_host = new Rfc7239HostPort(value);
break;
case "for":
if (value.startsWith("_") || "unknown".equals(value))
break;
if (_for == null || !(_for instanceof Rfc7239HostPort))
_for = new Rfc7239HostPort(value);
break;
case "host":
if (value.startsWith("_") || "unknown".equals(value))
break;
if (_host == null || !(_host instanceof Rfc7239HostPort))
_host = new Rfc7239HostPort(value);
break;
case "proto":
if (_proto == null || !_protoRfc7239)
{
_protoRfc7239 = true;
_proto = value;
}
break;
default:
break;
}
}
}
}
}