Add support for Forwarded By HTTP headers

See gh-34683

Signed-off-by: Mengqi Xu <2663479778@qq.com>
This commit is contained in:
Mengqi Xu
2025-09-01 11:49:59 +02:00
committed by Brian Clozel
parent 1653ec3b44
commit 7b3c4e5893
7 changed files with 219 additions and 3 deletions
@@ -57,6 +57,8 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
private @Nullable InetSocketAddress remoteAddress;
private @Nullable InetSocketAddress localAddress;
private final Flux<DataBuffer> body;
private final ServerHttpRequest originalRequest;
@@ -131,10 +133,16 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
return this;
}
@Override
public ServerHttpRequest.Builder localAddress(InetSocketAddress localAddress) {
this.localAddress = localAddress;
return this;
}
@Override
public ServerHttpRequest build() {
return new MutatedServerHttpRequest(getUriToUse(), this.contextPath,
this.httpMethod, this.sslInfo, this.remoteAddress, this.headers, this.body, this.originalRequest);
this.httpMethod, this.sslInfo, this.remoteAddress, this.localAddress, this.headers, this.body, this.originalRequest);
}
private URI getUriToUse() {
@@ -182,16 +190,19 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
private final @Nullable InetSocketAddress remoteAddress;
private final @Nullable InetSocketAddress localAddress;
private final Flux<DataBuffer> body;
private final ServerHttpRequest originalRequest;
public MutatedServerHttpRequest(URI uri, @Nullable String contextPath,
HttpMethod method, @Nullable SslInfo sslInfo, @Nullable InetSocketAddress remoteAddress,
HttpMethod method, @Nullable SslInfo sslInfo, @Nullable InetSocketAddress remoteAddress, @Nullable InetSocketAddress localAddress,
HttpHeaders headers, Flux<DataBuffer> body, ServerHttpRequest originalRequest) {
super(method, uri, contextPath, headers);
this.remoteAddress = (remoteAddress != null ? remoteAddress : originalRequest.getRemoteAddress());
this.localAddress = (localAddress != null ? localAddress : originalRequest.getLocalAddress());
this.sslInfo = (sslInfo != null ? sslInfo : originalRequest.getSslInfo());
this.body = body;
this.originalRequest = originalRequest;
@@ -204,7 +215,7 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
@Override
public @Nullable InetSocketAddress getLocalAddress() {
return this.originalRequest.getLocalAddress();
return this.localAddress;
}
@Override
@@ -184,6 +184,12 @@ public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage
*/
Builder remoteAddress(InetSocketAddress remoteAddress);
/**
* Set the address of the local client.
* @since 7.x
*/
Builder localAddress(InetSocketAddress localAddress);
/**
* Build a {@link ServerHttpRequest} decorator with the mutated properties.
*/
@@ -73,6 +73,7 @@ import org.springframework.web.util.WebUtils;
* @author Eddú Meléndez
* @author Rob Winch
* @author Brian Clozel
* @author Mengqi Xu
* @since 4.3
* @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a>
* @see <a href="https://docs.spring.io/spring-framework/reference/web/webmvc/filters.html#filters-forwarded-headers">Forwarded Headers</a>
@@ -92,6 +93,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter {
FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
FORWARDED_HEADER_NAMES.add("X-Forwarded-For");
FORWARDED_HEADER_NAMES.add("X-Forwarded-By");
}
@@ -255,6 +257,8 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter {
private final @Nullable InetSocketAddress remoteAddress;
private final @Nullable InetSocketAddress localAddress;
private final ForwardedPrefixExtractor forwardedPrefixExtractor;
ForwardedHeaderExtractingRequest(HttpServletRequest servletRequest) {
@@ -272,6 +276,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter {
this.port = (port == -1 ? (this.secure ? 443 : 80) : port);
this.remoteAddress = ForwardedHeaderUtils.parseForwardedFor(uri, headers, request.getRemoteAddress());
this.localAddress = ForwardedHeaderUtils.parseForwardedBy(uri, headers, request.getLocalAddress());
// Use Supplier as Tomcat updates delegate request on FORWARD
Supplier<HttpServletRequest> requestSupplier = () -> (HttpServletRequest) getRequest();
@@ -330,6 +335,16 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter {
return (this.remoteAddress != null ? this.remoteAddress.getPort() : super.getRemotePort());
}
@Override
public @Nullable String getLocalAddr() {
return (this.localAddress != null ? this.localAddress.getHostString() : super.getLocalAddr());
}
@Override
public int getLocalPort() {
return (this.localAddress != null ? this.localAddress.getPort() : super.getLocalPort());
}
@SuppressWarnings("DataFlowIssue")
@Override
public @Nullable Object getAttribute(String name) {
@@ -55,6 +55,7 @@ import org.springframework.web.util.UriComponents;
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @author Mengqi Xu
* @since 5.1
* @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a>
* @see <a href="https://docs.spring.io/spring-framework/reference/web/webflux/reactive-spring.html#webflux-forwarded-headers">Forwarded Headers</a>
@@ -72,6 +73,7 @@ public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, S
FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
FORWARDED_HEADER_NAMES.add("X-Forwarded-For");
FORWARDED_HEADER_NAMES.add("X-Forwarded-By");
}
@@ -119,6 +121,11 @@ public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, S
if (remoteAddress != null) {
builder.remoteAddress(remoteAddress);
}
InetSocketAddress localAddress = request.getLocalAddress();
localAddress = ForwardedHeaderUtils.parseForwardedBy(originalUri, headers, localAddress);
if (localAddress != null) {
builder.localAddress(localAddress);
}
}
removeForwardedHeaders(builder);
request = builder.build();
@@ -54,6 +54,7 @@ public abstract class ForwardedHeaderUtils {
private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:for)=" + FORWARDED_VALUE);
private static final Pattern FORWARDED_BY_PATTERN = Pattern.compile("(?i:by)=" + FORWARDED_VALUE);
/**
* Adapt the scheme+host+port of the given {@link URI} from the "Forwarded" header
@@ -189,4 +190,57 @@ public abstract class ForwardedHeaderUtils {
return null;
}
/**
* Parse the first "Forwarded: by=..." or "X-Forwarded-By" header value to
* an {@code InetSocketAddress} representing the address of the server.
* @param uri the request {@code URI}
* @param headers the request headers that may contain forwarded headers
* @param localAddress the current local address
* @return an {@code InetSocketAddress} with the extracted host and port, or
* {@code null} if the headers are not present
* @see <a href="https://tools.ietf.org/html/rfc7239#section-5.1">RFC 7239, Section 5.1</a>
*/
public static @Nullable InetSocketAddress parseForwardedBy(
URI uri, HttpHeaders headers, @Nullable InetSocketAddress localAddress) {
int port = (localAddress != null ?
localAddress.getPort() : "https".equals(uri.getScheme()) ? 443 : 80);
String forwardedHeader = headers.getFirst("Forwarded");
if (StringUtils.hasText(forwardedHeader)) {
String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0];
Matcher matcher = FORWARDED_BY_PATTERN.matcher(forwardedToUse);
if (matcher.find()) {
String value = matcher.group(1).trim();
String host = value;
int portSeparatorIdx = value.lastIndexOf(':');
int squareBracketIdx = value.lastIndexOf(']');
if (portSeparatorIdx > squareBracketIdx) {
if (squareBracketIdx == -1 && value.indexOf(':') != portSeparatorIdx) {
throw new IllegalArgumentException("Invalid IPv4 address: " + value);
}
host = value.substring(0, portSeparatorIdx);
try {
port = Integer.parseInt(value, portSeparatorIdx + 1, value.length(), 10);
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException(
"Failed to parse a port from \"forwarded\"-type header value: " + value);
}
}
return InetSocketAddress.createUnresolved(host, port);
}
}
String byHeader = headers.getFirst("X-Forwarded-By");
if (StringUtils.hasText(byHeader)) {
String host = StringUtils.tokenizeToStringArray(byHeader, ",")[0];
boolean ipv6 = (host.indexOf(':') != -1);
host = (ipv6 && !host.startsWith("[") && !host.endsWith("]") ? "[" + host + "]" : host);
return InetSocketAddress.createUnresolved(host, port);
}
return null;
}
}