diff --git a/java/org/apache/catalina/connector/XForwardedRequest.java b/java/org/apache/catalina/connector/XForwardedRequest.java new file mode 100644 index 000000000000..e2a8ad8f2373 --- /dev/null +++ b/java/org/apache/catalina/connector/XForwardedRequest.java @@ -0,0 +1,232 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.catalina.connector; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.apache.catalina.Globals; +import org.apache.catalina.util.RequestUtil; +import org.apache.tomcat.util.http.FastHttpDateFormat; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class XForwardedRequest extends HttpServletRequestWrapper { + + protected final Map> headers; + + protected String localName; + + protected int localPort; + + protected String remoteAddr; + + protected String remoteHost; + + protected String scheme; + + protected boolean secure; + + protected String serverName; + + protected int serverPort; + + protected String requestId; + + public XForwardedRequest(HttpServletRequest request) { + super(request); + this.localName = request.getLocalName(); + this.localPort = request.getLocalPort(); + this.remoteAddr = request.getRemoteAddr(); + this.remoteHost = request.getRemoteHost(); + this.scheme = request.getScheme(); + this.secure = request.isSecure(); + this.serverName = request.getServerName(); + this.serverPort = request.getServerPort(); + + headers = new HashMap<>(); + for (Enumeration headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) { + String header = headerNames.nextElement(); + headers.put(header, Collections.list(request.getHeaders(header))); + } + } + + @Override + public long getDateHeader(String name) { + String value = getHeader(name); + if (value == null) { + return -1; + } + long date = FastHttpDateFormat.parseDate(value); + if (date == -1) { + throw new IllegalArgumentException(value); + } + return date; + } + + @Override + public String getHeader(String name) { + Map.Entry> header = getHeaderEntry(name); + if (header == null || header.getValue() == null || header.getValue().isEmpty()) { + return null; + } + return header.getValue().get(0); + } + + protected Map.Entry> getHeaderEntry(String name) { + for (Map.Entry> entry : headers.entrySet()) { + if (entry.getKey().equalsIgnoreCase(name)) { + return entry; + } + } + return null; + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + public int getHeaderCount() { + return headers.size(); + } + + @Override + public Enumeration getHeaders(String name) { + Map.Entry> header = getHeaderEntry(name); + if (header == null || header.getValue() == null) { + return Collections.enumeration(Collections.emptyList()); + } + return Collections.enumeration(header.getValue()); + } + + @Override + public int getIntHeader(String name) { + String value = getHeader(name); + if (value == null) { + return -1; + } + return Integer.parseInt(value); + } + + @Override + public String getLocalName() { + return localName; + } + + @Override + public int getLocalPort() { + return localPort; + } + + @Override + public String getRemoteAddr() { + return this.remoteAddr; + } + + @Override + public String getRemoteHost() { + return this.remoteHost; + } + + @Override + public String getRequestId() { + if (this.requestId != null) { + return this.requestId; + } + + return super.getRequest().getRequestId(); + } + + @Override + public String getScheme() { + return scheme; + } + + @Override + public String getServerName() { + return serverName; + } + + @Override + public int getServerPort() { + return serverPort; + } + + public void removeHeader(String name) { + Map.Entry> header = getHeaderEntry(name); + if (header != null) { + headers.remove(header.getKey()); + } + } + + public void setHeader(String name, String value) { + List values = Collections.singletonList(value); + Map.Entry> header = getHeaderEntry(name); + if (header == null) { + headers.put(name, values); + } else { + header.setValue(values); + } + + } + + public void setLocalName(String localName) { + this.localName = localName; + } + + public void setLocalPort(int localPort) { + this.localPort = localPort; + } + + public void setRemoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + public void setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public void setSecure(boolean secure) { + super.getRequest().setAttribute(Globals.REMOTE_IP_FILTER_SECURE, Boolean.valueOf(secure)); + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + @Override + public StringBuffer getRequestURL() { + return RequestUtil.getRequestURL(this); + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/filters/RemoteIpFilter.java b/java/org/apache/catalina/filters/RemoteIpFilter.java index 34af331dcfb6..cc0ec2a75684 100644 --- a/java/org/apache/catalina/filters/RemoteIpFilter.java +++ b/java/org/apache/catalina/filters/RemoteIpFilter.java @@ -21,13 +21,9 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayDeque; -import java.util.Collections; import java.util.Deque; import java.util.Enumeration; -import java.util.HashMap; import java.util.LinkedList; -import java.util.List; -import java.util.Map; import java.util.regex.Pattern; import jakarta.servlet.FilterChain; @@ -36,16 +32,14 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import org.apache.catalina.AccessLog; import org.apache.catalina.Globals; -import org.apache.catalina.util.RequestUtil; +import org.apache.catalina.connector.XForwardedRequest; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.buf.StringUtils; -import org.apache.tomcat.util.http.FastHttpDateFormat; import org.apache.tomcat.util.http.parser.Host; import org.apache.tomcat.util.res.StringManager; @@ -107,6 +101,14 @@ * x-forwarded-for * * + * requestIdHeader + * Name of the Http Header read by this servlet filter that holds a request ID passed by a proxy or the requesting + * client + * RequestIdHeader + * Compliant http header name + * x-request-id + * + * * internalProxies * Regular expression that matches the IP addresses of internal proxies. If they appear in the * remoteIpHeader value, they will be trusted and will not appear in the proxiesHeader @@ -210,6 +212,10 @@ * <param-name>protocolHeader</param-name> * <param-value>x-forwarded-proto</param-value> * </init-param> + * <init-param> + * <param-name>requestIdHeader</param-name> + * <param-value>x-request-id</param-value> + * </init-param> * </filter> * * <filter-mapping> @@ -456,190 +462,6 @@ public class RemoteIpFilter extends GenericFilter { private static final long serialVersionUID = 1L; - public static class XForwardedRequest extends HttpServletRequestWrapper { - - protected final Map> headers; - - protected String localName; - - protected int localPort; - - protected String remoteAddr; - - protected String remoteHost; - - protected String scheme; - - protected boolean secure; - - protected String serverName; - - protected int serverPort; - - public XForwardedRequest(HttpServletRequest request) { - super(request); - this.localName = request.getLocalName(); - this.localPort = request.getLocalPort(); - this.remoteAddr = request.getRemoteAddr(); - this.remoteHost = request.getRemoteHost(); - this.scheme = request.getScheme(); - this.secure = request.isSecure(); - this.serverName = request.getServerName(); - this.serverPort = request.getServerPort(); - - headers = new HashMap<>(); - for (Enumeration headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) { - String header = headerNames.nextElement(); - headers.put(header, Collections.list(request.getHeaders(header))); - } - } - - @Override - public long getDateHeader(String name) { - String value = getHeader(name); - if (value == null) { - return -1; - } - long date = FastHttpDateFormat.parseDate(value); - if (date == -1) { - throw new IllegalArgumentException(value); - } - return date; - } - - @Override - public String getHeader(String name) { - Map.Entry> header = getHeaderEntry(name); - if (header == null || header.getValue() == null || header.getValue().isEmpty()) { - return null; - } - return header.getValue().get(0); - } - - protected Map.Entry> getHeaderEntry(String name) { - for (Map.Entry> entry : headers.entrySet()) { - if (entry.getKey().equalsIgnoreCase(name)) { - return entry; - } - } - return null; - } - - @Override - public Enumeration getHeaderNames() { - return Collections.enumeration(headers.keySet()); - } - - @Override - public Enumeration getHeaders(String name) { - Map.Entry> header = getHeaderEntry(name); - if (header == null || header.getValue() == null) { - return Collections.enumeration(Collections.emptyList()); - } - return Collections.enumeration(header.getValue()); - } - - @Override - public int getIntHeader(String name) { - String value = getHeader(name); - if (value == null) { - return -1; - } - return Integer.parseInt(value); - } - - @Override - public String getLocalName() { - return localName; - } - - @Override - public int getLocalPort() { - return localPort; - } - - @Override - public String getRemoteAddr() { - return this.remoteAddr; - } - - @Override - public String getRemoteHost() { - return this.remoteHost; - } - - @Override - public String getScheme() { - return scheme; - } - - @Override - public String getServerName() { - return serverName; - } - - @Override - public int getServerPort() { - return serverPort; - } - - public void removeHeader(String name) { - Map.Entry> header = getHeaderEntry(name); - if (header != null) { - headers.remove(header.getKey()); - } - } - - public void setHeader(String name, String value) { - List values = Collections.singletonList(value); - Map.Entry> header = getHeaderEntry(name); - if (header == null) { - headers.put(name, values); - } else { - header.setValue(values); - } - - } - - public void setLocalName(String localName) { - this.localName = localName; - } - - public void setLocalPort(int localPort) { - this.localPort = localPort; - } - - public void setRemoteAddr(String remoteAddr) { - this.remoteAddr = remoteAddr; - } - - public void setRemoteHost(String remoteHost) { - this.remoteHost = remoteHost; - } - - public void setScheme(String scheme) { - this.scheme = scheme; - } - - public void setSecure(boolean secure) { - super.getRequest().setAttribute(Globals.REMOTE_IP_FILTER_SECURE, Boolean.valueOf(secure)); - } - - public void setServerName(String serverName) { - this.serverName = serverName; - } - - public void setServerPort(int serverPort) { - this.serverPort = serverPort; - } - - @Override - public StringBuffer getRequestURL() { - return RequestUtil.getRequestURL(this); - } - } - - protected static final String HTTP_SERVER_PORT_PARAMETER = "httpServerPort"; protected static final String HTTPS_SERVER_PORT_PARAMETER = "httpsServerPort"; @@ -667,6 +489,8 @@ public StringBuffer getRequestURL() { protected static final String REMOTE_IP_HEADER_PARAMETER = "remoteIpHeader"; + protected static final String REQUEST_ID_HEADER_PARAMETER = "requestIdHeader"; + protected static final String TRUSTED_PROXIES_PARAMETER = "trustedProxies"; protected static final String ENABLE_LOOKUPS_PARAMETER = "enableLookups"; @@ -717,6 +541,11 @@ public StringBuffer getRequestURL() { */ private String remoteIpHeader = "X-Forwarded-For"; + /** + * @see #setRequestIdHeader(String) + */ + private String requestIdHeader = ""; + /** * @see #setRequestAttributesEnabled(boolean) */ @@ -843,6 +672,14 @@ public void doFilter(HttpServletRequest request, HttpServletResponse response, F } } } + + if (!requestIdHeader.isEmpty()) { + String requestIdHeaderValue = request.getHeader(requestIdHeader); + if (requestIdHeaderValue != null) { + xRequest.setRequestId(requestIdHeaderValue); + } + } + request.setAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE, Boolean.TRUE); if (log.isTraceEnabled()) { @@ -1016,6 +853,10 @@ public void init() throws ServletException { setRemoteIpHeader(getInitParameter(REMOTE_IP_HEADER_PARAMETER)); } + if (getInitParameter(REQUEST_ID_HEADER_PARAMETER) != null) { + setRequestIdHeader(getInitParameter(REQUEST_ID_HEADER_PARAMETER)); + } + if (getInitParameter(TRUSTED_PROXIES_PARAMETER) != null) { setTrustedProxies(getInitParameter(TRUSTED_PROXIES_PARAMETER)); } @@ -1221,6 +1062,17 @@ public void setRemoteIpHeader(String remoteIpHeader) { this.remoteIpHeader = remoteIpHeader; } + /** + *

Name of the http header from which the request id is extracted.

+ * + *

Request id propagation is disabled by default. Set a value, e.g. X-Request-Id, to enable it.

+ * + * @param requestIdHeader The header name + */ + public void setRequestIdHeader(String requestIdHeader) { + this.requestIdHeader = requestIdHeader; + } + /** * Should this filter set request attributes for IP address, Hostname, protocol and port used for the request? This * are typically used in conjunction with an {@link AccessLog} which will otherwise log the original values. Default diff --git a/test/org/apache/catalina/filters/TestRemoteIpFilter.java b/test/org/apache/catalina/filters/TestRemoteIpFilter.java index 4effc5b3dfad..3b9f11b212a9 100644 --- a/test/org/apache/catalina/filters/TestRemoteIpFilter.java +++ b/test/org/apache/catalina/filters/TestRemoteIpFilter.java @@ -37,6 +37,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.apache.catalina.connector.XForwardedRequest; import org.junit.Assert; import org.junit.Test; @@ -179,11 +180,11 @@ public void testCommaDelimitedListToStringArrayNullList() { @Test public void testHeaderNamesCaseInsensitivity() { - RemoteIpFilter.XForwardedRequest request = new RemoteIpFilter.XForwardedRequest(new MockHttpServletRequest()); + XForwardedRequest request = new XForwardedRequest(new MockHttpServletRequest()); request.setHeader("myheader", "lower Case"); request.setHeader("MYHEADER", "UPPER CASE"); request.setHeader("MyHeader", "Camel Case"); - Assert.assertEquals(1, request.headers.size()); + Assert.assertEquals(1, request.getHeaderCount()); Assert.assertEquals("Camel Case", request.getHeader("myheader")); } diff --git a/webapps/docs/config/filter.xml b/webapps/docs/config/filter.xml index a3d1af57dc9d..d8db61b6b0e9 100644 --- a/webapps/docs/config/filter.xml +++ b/webapps/docs/config/filter.xml @@ -1445,6 +1445,10 @@ FINE: Request "/docs/config/manager.html" with response status "200" protocolHeader x-forwarded-proto + + requestIdHeader + x-request-id + ]]>

Request values:

@@ -1685,6 +1689,13 @@ FINE: Request "/docs/config/manager.html" with response status "200" specified, the default of x-forwarded-for is used.

+ +

Name of the HTTP Header read by this valve that holds request ID + if passed by the Proxy server or requesting client. Request ID propagation + is disabled by default, but can be enabled by setting this attribute, + e.g. to x-request-id.

+
+

Regular expression (using java.util.regex) that a proxy's IP address must match to be considered an internal proxy.