From 004218d301ddcba8f4a31079b44c74c85d6113df Mon Sep 17 00:00:00 2001
From: markt
+ * 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 | + *
| internalProxies | + *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}, 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, 169.254/16 and 127/8 are allowed ; 172.16/12 has not been enabled by default because it is complex to + * describe with regular expressions |
+ *
| 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>internalProxies</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>internalProxies</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>internalProxies</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>internalProxies</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 #setHttpsServerPort(int)
+ */
+ private int httpsServerPort = 443;
+
+ /**
+ * @see #setInternalProxies(String)
+ */
+ private Pattern[] internalProxies = 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("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() {
+ // NOOP
+ }
+
+ public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+
+ if (matchesOne(request.getRemoteAddr(), internalProxies)) {
+ 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 internalProxies;
+ }
+
+ 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) {
+ setInternalProxies(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(HTTPS_SERVER_PORT_PARAMETER) != null) {
+ try {
+ setHttpsServerPort(Integer.parseInt(filterConfig.getInitParameter(HTTPS_SERVER_PORT_PARAMETER)));
+ } catch (NumberFormatException e) {
+ throw new NumberFormatException("Illegal serverPort : " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * + * Server Port value if the {@link #protocolHeader} indicates HTTPS + *
+ *+ * Default value : 443 + *
+ */ + public void setHttpsServerPort(int httpsServerPort) { + this.httpsServerPort = httpsServerPort; + } + + /** + *+ * Comma delimited list of internal proxies. Can be expressed with regular expressions. + *
+ *+ * Default value : 10\.\d{1,3}\.\d{1,3}\.\d{1,3}, 192\.168\.\d{1,3}\.\d{1,3}, 127\.\d{1,3}\.\d{1,3}\.\d{1,3} + *
+ */ + public void setInternalProxies(String internalProxies) { + this.internalProxies = commaDelimitedListToPatternArray(internalProxies); + } + + /** + *
+ * 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); + } +} diff --git a/webapps/docs/config/filter.xml b/webapps/docs/config/filter.xml index d4e6248c9..f28ecf31b 100644 --- a/webapps/docs/config/filter.xml +++ b/webapps/docs/config/filter.xml @@ -194,6 +194,92 @@ +Tomcat port of + mod_remoteip, + this 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 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").
+ +If used in conjunction with Remote Address/Host filters then this filter + should be defined first to ensure that the correct client IP address is + presented to the Remote Address/Host filters.
+ +The filter class name for the Remote IP Filter is
+ org.apache.catalina.filters.RemoteIpFilter
+ .
The Remote IP Filter supports the + following initialisation parameters:
+ +Name of the HTTP Header read by this valve that holds the list of
+ traversed IP addresses starting from the requesting client. If not
+ specified, the default of x-forwarded-for is used.
List of internal proxies' IP addresses as comma separated regular
+ expressions. If they appear in the remoteIpHeader
+ value, they will be trusted and will not appear in the
+ proxiesHeader value. If not specified the default value
+ of 10\.\d{1,3}\.\d{1,3}\.\d{1,3}, 192\.168\.\d{1,3}\.\d{1,3},
+ 169\.254\.\d{1,3}\.\d{1,3}, 127\.\d{1,3}\.\d{1,3}\.\d{1,3} will
+ be used.
Name of the HTTP header created by this valve to hold the list of
+ proxies that have been processed in the incoming
+ remoteIpHeader. If not specified, the default of
+ x-forwarded-by is used.
List of trusted proxies' IP addresses as comma separated regular + expressions. If they appear in the remoteIpHeader + value, they will be trusted and will appear in the + proxiesHeader value. If not specified, no proxies will + be trusted.
+Name of the HTTP Header read by this valve that holds the protocol
+ used by the client to connect to the proxy. If not specified, the
+ default of null is used.
Value of the protocolHeader to indicate that it is
+ an HTTPS request. If not specified, the default of https is
+ used.