/* * Copyright 2009 Xebia and the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package fr.xebia.servlet.filter; import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** *
* Servlet filter to integrate "X-Forwarded-For" and "X-Forwarded-Proto" HTTP headers. *
** Most of the design of this Servlet Filter is a port of mod_remoteip, this servlet filter replaces the apparent client remote * IP address and hostname for the request with the IP address list presented by a proxy or a load balancer via a request headers (e.g. * "X-Forwarded-For"). *
** Another feature of this servlet filter is to replace the apparent scheme (http/https) and server port with the scheme presented by a * proxy or a load balancer via a request header (e.g. "X-Forwarded-Proto"). *
** This servlet filter proceeds as follows: *
*
* If the incoming request.getRemoteAddr() matches the servlet filter's list of internal proxies :
*
$remoteIPHeader (default value x-forwarded-for). Values are processed in right-to-left order.$protocolHeader (e.g. x-forwarded-for) equals to the value of
* protocolHeaderHttpsValue configuration parameter (default https) then request.isSecure = true,
* request.scheme = https and request.serverPort = 443. Note that 443 can be overwritten with the
* $httpsServerPort configuration parameter.* Configuration parameters: *
| XForwardedFilter property | *Description | *Equivalent mod_remoteip directive | *Format | *Default Value | *
|---|---|---|---|---|
| remoteIPHeader | *Name of the Http Header read by this servlet filter that holds the list of traversed IP addresses starting from the requesting client * | *RemoteIPHeader | *Compliant http header name | *x-forwarded-for | *
| allowedInternalProxies | *List of internal proxies ip adress. If they appear in the remoteIpHeader value, they will be trusted and will not appear
* in the proxiesHeader value |
* RemoteIPInternalProxy | *Comma delimited list of regular expressions (in the syntax supported by the {@link java.util.regex.Pattern} library) | *10\.\d{1,3}\.\d{1,3}\.\d{1,3}, 192\.168\.\d{1,3}\.\d{1,3}, 172\\.(?:1[6-9]|2\\d|3[0-1]).\\d{1,3}.\\d{1,3},
* 169\.254\.\d{1,3}\.\d{1,3}, 127\.\d{1,3}\.\d{1,3}\.\d{1,3} * By default, 10/8, 192.168/16, 172.16/12, 169.254/16 and 127/8 are allowed |
*
| proxiesHeader | *Name of the http header created by this servlet filter to hold the list of proxies that have been processed in the incoming
* remoteIPHeader |
* RemoteIPProxiesHeader | *Compliant http header name | *x-forwarded-by | *
| trustedProxies | *List of trusted proxies ip adress. If they appear in the remoteIpHeader value, they will be trusted and will appear in
* the proxiesHeader value |
* RemoteIPTrustedProxy | *Comma delimited list of regular expressions (in the syntax supported by the {@link java.util.regex.Pattern} library) | ** |
| protocolHeader | *Name of the http header read by this servlet filter that holds the flag that this request | *N/A | *Compliant http header name like X-Forwarded-Proto, X-Forwarded-Ssl or Front-End-Https |
* null |
*
| protocolHeaderHttpsValue | *Value of the protocolHeader to indicate that it is an Https request |
* N/A | *String like https or ON |
* https |
*
*
* Regular expression vs. IP address blocks: mod_remoteip allows to use address blocks (e.g.
* 192.168/16) to configure RemoteIPInternalProxy and RemoteIPTrustedProxy ; as the JVM doesnt have a
* library similar to apr_ipsubnet_test.
*
* Sample with internal proxies *
** XForwardedFilter configuration: *
*
* <filter>
* <filter-name>XForwardedFilter</filter-name>
* <filter-class>fr.xebia.servlet.filter.XForwardedFilter</filter-class>
* <init-param>
* <param-name>allowedInternalProxies</param-name><param-value>192\.168\.0\.10, 192\.168\.0\.11</param-value>
* </init-param>
* <init-param>
* <param-name>remoteIPHeader</param-name><param-value>x-forwarded-for</param-value>
* </init-param>
* <init-param>
* <param-name>remoteIPProxiesHeader</param-name><param-value>x-forwarded-by</param-value>
* </init-param>
* <init-param>
* <param-name>protocolHeader</param-name><param-value>x-forwarded-proto</param-value>
* </init-param>
* </filter>
*
* <filter-mapping>
* <filter-name>XForwardedFilter</filter-name>
* <url-pattern>/*</url-pattern>
* <dispatcher>REQUEST</dispatcher>
* </filter-mapping>
* * Request values: *
| property | *Value Before XForwardedFilter | *Value After XForwardedFilter | *
|---|---|---|
| request.remoteAddr | *192.168.0.10 | *140.211.11.130 | *
| request.header['x-forwarded-for'] | *140.211.11.130, 192.168.0.10 | *null | *
| request.header['x-forwarded-by'] | *null | *null | *
| request.header['x-forwarded-proto'] | *https | *https | *
| request.scheme | *http | *https | *
| request.secure | *false | *true | *
| request.serverPort | *80 | *443 | *
x-forwarded-by header is null because only internal proxies as been traversed by the request.
* x-forwarded-by is null because all the proxies are trusted or internal.
*
* * Sample with trusted proxies *
** XForwardedFilter configuration: *
*
* <filter>
* <filter-name>XForwardedFilter</filter-name>
* <filter-class>fr.xebia.servlet.filter.XForwardedFilter</filter-class>
* <init-param>
* <param-name>allowedInternalProxies</param-name><param-value>192\.168\.0\.10, 192\.168\.0\.11</param-value>
* </init-param>
* <init-param>
* <param-name>remoteIPHeader</param-name><param-value>x-forwarded-for</param-value>
* </init-param>
* <init-param>
* <param-name>remoteIPProxiesHeader</param-name><param-value>x-forwarded-by</param-value>
* </init-param>
* <init-param>
* <param-name>trustedProxies</param-name><param-value>proxy1, proxy2</param-value>
* </init-param>
* </filter>
*
* <filter-mapping>
* <filter-name>XForwardedFilter</filter-name>
* <url-pattern>/*</url-pattern>
* <dispatcher>REQUEST</dispatcher>
* </filter-mapping>
* * Request values: *
| property | *Value Before XForwardedFilter | *Value After XForwardedFilter | *
|---|---|---|
| request.remoteAddr | *192.168.0.10 | *140.211.11.130 | *
| request.header['x-forwarded-for'] | *140.211.11.130, proxy1, proxy2 | *null | *
| request.header['x-forwarded-by'] | *null | *proxy1, proxy2 | *
proxy1 and proxy2 are both trusted proxies that come in x-forwarded-for header, they both
* are migrated in x-forwarded-by header. x-forwarded-by is null because all the proxies are trusted or internal.
*
* * Sample with internal and trusted proxies *
** XForwardedFilter configuration: *
*
* <filter>
* <filter-name>XForwardedFilter</filter-name>
* <filter-class>fr.xebia.servlet.filter.XForwardedFilter</filter-class>
* <init-param>
* <param-name>allowedInternalProxies</param-name><param-value>192\.168\.0\.10, 192\.168\.0\.11</param-value>
* </init-param>
* <init-param>
* <param-name>remoteIPHeader</param-name><param-value>x-forwarded-for</param-value>
* </init-param>
* <init-param>
* <param-name>remoteIPProxiesHeader</param-name><param-value>x-forwarded-by</param-value>
* </init-param>
* <init-param>
* <param-name>trustedProxies</param-name><param-value>proxy1, proxy2</param-value>
* </init-param>
* </filter>
*
* <filter-mapping>
* <filter-name>XForwardedFilter</filter-name>
* <url-pattern>/*</url-pattern>
* <dispatcher>REQUEST</dispatcher>
* </filter-mapping>
* * Request values: *
| property | *Value Before XForwardedFilter | *Value After XForwardedFilter | *
|---|---|---|
| request.remoteAddr | *192.168.0.10 | *140.211.11.130 | *
| request.header['x-forwarded-for'] | *140.211.11.130, proxy1, proxy2, 192.168.0.10 | *null | *
| request.header['x-forwarded-by'] | *null | *proxy1, proxy2 | *
proxy1 and proxy2 are both trusted proxies that come in x-forwarded-for header, they both
* are migrated in x-forwarded-by header. As 192.168.0.10 is an internal proxy, it does not appear in
* x-forwarded-by. x-forwarded-by is null because all the proxies are trusted or internal.
*
* * Sample with an untrusted proxy *
** XForwardedFilter configuration: *
*
* <filter>
* <filter-name>XForwardedFilter</filter-name>
* <filter-class>fr.xebia.servlet.filter.XForwardedFilter</filter-class>
* <init-param>
* <param-name>allowedInternalProxies</param-name><param-value>192\.168\.0\.10, 192\.168\.0\.11</param-value>
* </init-param>
* <init-param>
* <param-name>remoteIPHeader</param-name><param-value>x-forwarded-for</param-value>
* </init-param>
* <init-param>
* <param-name>remoteIPProxiesHeader</param-name><param-value>x-forwarded-by</param-value>
* </init-param>
* <init-param>
* <param-name>trustedProxies</param-name><param-value>proxy1, proxy2</param-value>
* </init-param>
* </filter>
*
* <filter-mapping>
* <filter-name>XForwardedFilter</filter-name>
* <url-pattern>/*</url-pattern>
* <dispatcher>REQUEST</dispatcher>
* </filter-mapping>
* * Request values: *
| property | *Value Before XForwardedFilter | *Value After XForwardedFilter | *
|---|---|---|
| request.remoteAddr | *192.168.0.10 | *untrusted-proxy | *
| request.header['x-forwarded-for'] | *140.211.11.130, untrusted-proxy, proxy1 | *140.211.11.130 | *
| request.header['x-forwarded-by'] | *null | *proxy1 | *
x-forwarded-by holds the trusted proxy proxy1. x-forwarded-by holds
* 140.211.11.130 because untrusted-proxy is not trusted and thus, we can not trust that
* untrusted-proxy is the actual remote ip. request.remoteAddr is untrusted-proxy that is an IP
* verified by proxy1.
*
* null)
*/
protected static Pattern[] commaDelimitedListToPatternArray(String commaDelimitedPatterns) {
String[] patterns = commaDelimitedListToStringArray(commaDelimitedPatterns);
Listnull)
*/
protected static String[] commaDelimitedListToStringArray(String commaDelimitedStrings) {
return (commaDelimitedStrings == null || commaDelimitedStrings.length() == 0) ? new String[0] : commaSeparatedValuesPattern
.split(commaDelimitedStrings);
}
/**
* Convert an array of strings in a comma delimited string
*/
protected static String listToCommaDelimitedString(Listtrue if the given str matches at least one of the given patterns.
*/
protected static boolean matchesOne(String str, Pattern... patterns) {
for (Pattern pattern : patterns) {
if (pattern.matcher(str).matches()) {
return true;
}
}
return false;
}
/**
* @see #setHttpServerPort(int)
*/
private int httpServerPort = 80;
/**
* @see #setHttpsServerPort(int)
*/
private int httpsServerPort = 443;
/**
* @see #setInternalProxies(String)
*/
private Pattern[] allowedInternalProxies = new Pattern[] { Pattern.compile("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"),
Pattern.compile("192\\.168\\.\\d{1,3}\\.\\d{1,3}"), Pattern.compile("172\\.(?:1[6-9]|2\\d|3[0-1]).\\d{1,3}.\\d{1,3}"),
Pattern.compile("169\\.254\\.\\d{1,3}\\.\\d{1,3}"), Pattern.compile("127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}") };
/**
* @see #setProtocolHeader(String)
*/
private String protocolHeader = null;
private String protocolHeaderSslValue = "https";
/**
* @see #setProxiesHeader(String)
*/
private String proxiesHeader = "X-Forwarded-By";
/**
* @see #setRemoteIPHeader(String)
*/
private String remoteIPHeader = "X-Forwarded-For";
/**
* @see #setTrustedProxies(String)
*/
private Pattern[] trustedProxies = new Pattern[0];
public void destroy() {
}
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (matchesOne(request.getRemoteAddr(), allowedInternalProxies)) {
String remoteIp = null;
// In java 6, proxiesHeaderValue should be declared as a java.util.Deque
LinkedListrequest in a {@link XForwardedRequest} if the http header x-forwareded-for is not empty.
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
} else {
chain.doFilter(request, response);
}
}
public int getHttpsServerPort() {
return httpsServerPort;
}
public Pattern[] getInternalProxies() {
return allowedInternalProxies;
}
public String getProtocolHeader() {
return protocolHeader;
}
public String getProtocolHeaderSslValue() {
return protocolHeaderSslValue;
}
public String getProxiesHeader() {
return proxiesHeader;
}
public String getRemoteIPHeader() {
return remoteIPHeader;
}
public Pattern[] getTrustedProxies() {
return trustedProxies;
}
public void init(FilterConfig filterConfig) throws ServletException {
if (filterConfig.getInitParameter(INTERNAL_PROXIES_PARAMETER) != null) {
setAllowedInternalProxies(filterConfig.getInitParameter(INTERNAL_PROXIES_PARAMETER));
}
if (filterConfig.getInitParameter(PROTOCOL_HEADER_PARAMETER) != null) {
setProtocolHeader(filterConfig.getInitParameter(PROTOCOL_HEADER_PARAMETER));
}
if (filterConfig.getInitParameter(PROTOCOL_HEADER_SSL_VALUE_PARAMETER) != null) {
setProtocolHeaderSslValue(filterConfig.getInitParameter(PROTOCOL_HEADER_SSL_VALUE_PARAMETER));
}
if (filterConfig.getInitParameter(PROXIES_HEADER_PARAMETER) != null) {
setProxiesHeader(filterConfig.getInitParameter(PROXIES_HEADER_PARAMETER));
}
if (filterConfig.getInitParameter(REMOTE_IP_HEADER_PARAMETER) != null) {
setRemoteIPHeader(filterConfig.getInitParameter(REMOTE_IP_HEADER_PARAMETER));
}
if (filterConfig.getInitParameter(TRUSTED_PROXIES_PARAMETER) != null) {
setTrustedProxies(filterConfig.getInitParameter(TRUSTED_PROXIES_PARAMETER));
}
if (filterConfig.getInitParameter(HTTP_SERVER_PORT_PARAMETER) != null) {
try {
setHttpServerPort(Integer.parseInt(filterConfig.getInitParameter(HTTP_SERVER_PORT_PARAMETER)));
} catch (NumberFormatException e) {
throw new NumberFormatException("Illegal " + HTTP_SERVER_PORT_PARAMETER + " : " + e.getMessage());
}
}
if (filterConfig.getInitParameter(HTTPS_SERVER_PORT_PARAMETER) != null) {
try {
setHttpsServerPort(Integer.parseInt(filterConfig.getInitParameter(HTTPS_SERVER_PORT_PARAMETER)));
} catch (NumberFormatException e) {
throw new NumberFormatException("Illegal " + HTTPS_SERVER_PORT_PARAMETER + " : " + e.getMessage());
}
}
}
/**
* * Comma delimited list of internal proxies. Expressed with regular expressions. *
** Default value : 10\.\d{1,3}\.\d{1,3}\.\d{1,3}, 192\.168\.\d{1,3}\.\d{1,3}, 172\\.(?:1[6-9]|2\\d|3[0-1]).\\d{1,3}.\\d{1,3}, * 169\.254\.\d{1,3}\.\d{1,3}, 127\.\d{1,3}\.\d{1,3}\.\d{1,3} *
*/ public void setAllowedInternalProxies(String allowedInternalProxies) { this.allowedInternalProxies = commaDelimitedListToPatternArray(allowedInternalProxies); } /** ** Server Port value if the {@link #protocolHeader} does not indicate HTTPS *
** Default value : 80 *
*/ public void setHttpServerPort(int httpServerPort) { this.httpServerPort = httpServerPort; } /** ** Server Port value if the {@link #protocolHeader} indicates HTTPS *
** Default value : 443 *
*/ public void setHttpsServerPort(int httpsServerPort) { this.httpsServerPort = httpsServerPort; } /** *
* Header that holds the incoming protocol, usally named X-Forwarded-Proto. If null, request.scheme and
* request.secure will not be modified.
*
* Default value : null
*
* Case insensitive value of the protocol header to indicate that the incoming http request uses SSL. *
*
* Default value : HTTPS
*
* The proxiesHeader directive specifies a header into which mod_remoteip will collect a list of all of the intermediate client IP * addresses trusted to resolve the actual remote IP. Note that intermediate RemoteIPTrustedProxy addresses are recorded in this header, * while any intermediate RemoteIPInternalProxy addresses are discarded. *
** Name of the http header that holds the list of trusted proxies that has been traversed by the http request. *
** The value of this header can be comma delimited. *
*
* Default value : X-Forwarded-By
*
* Name of the http header from which the remote ip is extracted. *
** The value of this header can be comma delimited. *
*
* Default value : X-Forwarded-For
*
* Comma delimited list of proxies that are trusted when they appear in the {@link #remoteIPHeader} header. Can be expressed as a * regular expression. *
** Default value : empty list, no external proxy is trusted. *
*/ public void setTrustedProxies(String trustedProxies) { this.trustedProxies = commaDelimitedListToPatternArray(trustedProxies); } }