This commit is contained in:
Mengqi Xu 2025-07-01 21:25:01 +05:00 committed by GitHub
commit 8521008f99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 219 additions and 3 deletions

View File

@ -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

View File

@ -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.
*/

View File

@ -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) {

View File

@ -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();

View File

@ -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;
}
}

View File

@ -49,6 +49,7 @@ import static org.mockito.Mockito.mock;
* @author Rob Winch
* @author Brian Clozel
* @author Sebastien Deleuze
* @author Mengqi Xu
*/
class ForwardedHeaderFilterTests {
@ -66,6 +67,8 @@ class ForwardedHeaderFilterTests {
private static final String X_FORWARDED_FOR = "x-forwarded-for";
private static final String X_FORWARDED_BY = "x-forwarded-by";
private final ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
@ -93,6 +96,7 @@ class ForwardedHeaderFilterTests {
testShouldFilter(X_FORWARDED_SSL);
testShouldFilter(X_FORWARDED_PREFIX);
testShouldFilter(X_FORWARDED_FOR);
testShouldFilter(X_FORWARDED_BY);
}
private void testShouldFilter(String headerName) {
@ -115,6 +119,7 @@ class ForwardedHeaderFilterTests {
this.request.addHeader(X_FORWARDED_PORT, "443");
this.request.addHeader("foo", "bar");
this.request.addHeader(X_FORWARDED_FOR, "[203.0.113.195]");
this.request.addHeader(X_FORWARDED_BY, "[203.0.113.196]");
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
HttpServletRequest actual = (HttpServletRequest) this.filterChain.getRequest();
@ -126,11 +131,13 @@ class ForwardedHeaderFilterTests {
assertThat(actual.getServerPort()).isEqualTo(443);
assertThat(actual.isSecure()).isTrue();
assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("[203.0.113.195]");
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("[203.0.113.196]");
assertThat(actual.getHeader(X_FORWARDED_PROTO)).isNull();
assertThat(actual.getHeader(X_FORWARDED_HOST)).isNull();
assertThat(actual.getHeader(X_FORWARDED_PORT)).isNull();
assertThat(actual.getHeader(X_FORWARDED_FOR)).isNull();
assertThat(actual.getHeader(X_FORWARDED_BY)).isNull();
assertThat(actual.getHeader("foo")).isEqualTo("bar");
}
@ -143,6 +150,7 @@ class ForwardedHeaderFilterTests {
this.request.addHeader(X_FORWARDED_SSL, "on");
this.request.addHeader("foo", "bar");
this.request.addHeader(X_FORWARDED_FOR, "203.0.113.195");
this.request.addHeader(X_FORWARDED_BY, "203.0.113.196");
this.filter.setRemoveOnly(true);
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
@ -156,12 +164,14 @@ class ForwardedHeaderFilterTests {
assertThat(actual.isSecure()).isFalse();
assertThat(actual.getRemoteAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_ADDR);
assertThat(actual.getRemoteHost()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_HOST);
assertThat(actual.getLocalAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_ADDR);
assertThat(actual.getHeader(X_FORWARDED_PROTO)).isNull();
assertThat(actual.getHeader(X_FORWARDED_HOST)).isNull();
assertThat(actual.getHeader(X_FORWARDED_PORT)).isNull();
assertThat(actual.getHeader(X_FORWARDED_SSL)).isNull();
assertThat(actual.getHeader(X_FORWARDED_FOR)).isNull();
assertThat(actual.getHeader(X_FORWARDED_BY)).isNull();
assertThat(actual.getHeader("foo")).isEqualTo("bar");
}
@ -541,6 +551,83 @@ class ForwardedHeaderFilterTests {
}
@Nested
class ForwardedBy {
@Test
void xForwardedForEmpty() throws Exception {
request.addHeader(X_FORWARDED_BY, "");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getLocalAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_ADDR);
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
}
@Test
void xForwardedForSingleIdentifier() throws Exception {
request.addHeader(X_FORWARDED_BY, "203.0.113.195");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
}
@Test
void xForwardedForMultipleIdentifiers() throws Exception {
request.addHeader(X_FORWARDED_BY, "203.0.113.195, 70.41.3.18, 150.172.238.178");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
}
@Test
void forwardedForIpV4Identifier() throws Exception {
request.addHeader(FORWARDED, "By=203.0.113.195");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
}
@Test
void forwardedForIpV6Identifier() throws Exception {
request.addHeader(FORWARDED, "By=\"[2001:db8:cafe::17]\"");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("[2001:db8:cafe::17]");
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
}
@Test
void forwardedForIpV4IdentifierWithPort() throws Exception {
request.addHeader(FORWARDED, "By=\"203.0.113.195:47011\"");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
assertThat(actual.getLocalPort()).isEqualTo(47011);
}
@Test
void forwardedForIpV6IdentifierWithPort() throws Exception {
request.addHeader(FORWARDED, "By=\"[2001:db8:cafe::17]:47011\"");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("[2001:db8:cafe::17]");
assertThat(actual.getLocalPort()).isEqualTo(47011);
}
@Test
void forwardedForMultipleIdentifiers() throws Exception {
request.addHeader(FORWARDED, "by=203.0.113.195;proto=http, by=\"[2001:db8:cafe::17]\", by=unknown");
HttpServletRequest actual = filterAndGetWrappedRequest();
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
}
}
@Nested
class SendRedirect {

View File

@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @author Mengqi Xu
*/
class ForwardedHeaderTransformerTests {
@ -52,6 +53,7 @@ class ForwardedHeaderTransformerTests {
headers.add("X-Forwarded-Prefix", "prefix");
headers.add("X-Forwarded-Ssl", "on");
headers.add("X-Forwarded-For", "203.0.113.195");
headers.add("X-Forwarded-By", "203.0.113.196");
ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
assertForwardedHeadersRemoved(request);
@ -233,6 +235,40 @@ class ForwardedHeaderTransformerTests {
assertThat(request.getRemoteAddress().getHostName()).isEqualTo("203.0.113.195");
}
@Test
void forwardedBy() {
HttpHeaders headers = new HttpHeaders();
headers.add("Forwarded", "by=\"203.0.113.195:4711\";host=84.198.58.199;proto=https");
InetSocketAddress localAddress = new InetSocketAddress("example.client", 47011);
ServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, URI.create("https://example.com/a%20b?q=a%2Bb"))
.localAddress(localAddress)
.headers(headers)
.build();
request = this.requestMutator.apply(request);
assertThat(request.getLocalAddress()).isNotNull();
assertThat(request.getLocalAddress().getHostName()).isEqualTo("203.0.113.195");
assertThat(request.getLocalAddress().getPort()).isEqualTo(4711);
}
@Test
void xForwardedBy() {
HttpHeaders headers = new HttpHeaders();
headers.add("x-forwarded-by", "203.0.113.195, 70.41.3.18, 150.172.238.178");
ServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, URI.create("https://example.com/a%20b?q=a%2Bb"))
.headers(headers)
.build();
request = this.requestMutator.apply(request);
assertThat(request.getLocalAddress()).isNotNull();
assertThat(request.getLocalAddress().getHostName()).isEqualTo("203.0.113.195");
}
private MockServerHttpRequest getRequest(HttpHeaders headers) {
return MockServerHttpRequest.get(BASE_URL).headers(headers).build();