Skip to content

Commit a8a1fc6

Browse files
committed
Earlier processing of forwarded headers
Forwarded headers are now processed before ServerWebExchange is created through ForwardedHeaderTransformer which has the same logic as the ForwardedHeaderFilter but works on the request only. ForwardedHeaderFilter is deprecated as of 5.1 but if registered it is removed from the list of filters and ForwardedHeaderTransformer is used instead. Issue: SPR-17072
1 parent 0878e43 commit a8a1fc6

File tree

10 files changed

+475
-300
lines changed

10 files changed

+475
-300
lines changed

spring-web/src/main/java/org/springframework/web/filter/reactive/ForwardedHeaderFilter.java

Lines changed: 16 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,115 +16,42 @@
1616

1717
package org.springframework.web.filter.reactive;
1818

19-
import java.net.URI;
20-
import java.util.LinkedHashSet;
21-
import java.util.Set;
22-
2319
import reactor.core.publisher.Mono;
2420

25-
import org.springframework.http.HttpHeaders;
2621
import org.springframework.http.server.reactive.ServerHttpRequest;
27-
import org.springframework.lang.Nullable;
2822
import org.springframework.web.server.ServerWebExchange;
2923
import org.springframework.web.server.WebFilter;
3024
import org.springframework.web.server.WebFilterChain;
31-
import org.springframework.web.util.UriComponentsBuilder;
25+
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
3226

3327
/**
34-
* Extract values from "Forwarded" and "X-Forwarded-*" headers, and use them to
35-
* override {@link ServerHttpRequest#getURI()} to reflect the client-originated
36-
* protocol and address.
28+
* Extract values from "Forwarded" and "X-Forwarded-*" headers to override the
29+
* request URI (i.e. {@link ServerHttpRequest#getURI()}) so it reflects the
30+
* client-originated protocol and address.
3731
*
38-
* <p>This filter can also be used in a {@link #setRemoveOnly removeOnly} mode
39-
* where "Forwarded" and "X-Forwarded-*" headers are eliminated, and not used.
32+
* <p>Alternatively if {@link #setRemoveOnly removeOnly} is set to "true", then
33+
* "Forwarded" and "X-Forwarded-*" headers are only removed, and not used.
4034
*
4135
* @author Arjen Poutsma
4236
* @author Rossen Stoyanchev
37+
* @deprecated as of 5.1 this filter is deprecated in favor of using
38+
* {@link ForwardedHeaderTransformer} which can be declared as a bean with the
39+
* name "forwardedHeaderTransformer" or registered explicitly in
40+
* {@link org.springframework.web.server.adapter.WebHttpHandlerBuilder
41+
* WebHttpHandlerBuilder}.
4342
* @since 5.0
4443
* @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a>
4544
*/
46-
public class ForwardedHeaderFilter implements WebFilter {
47-
48-
static final Set<String> FORWARDED_HEADER_NAMES = new LinkedHashSet<>(5);
49-
50-
static {
51-
FORWARDED_HEADER_NAMES.add("Forwarded");
52-
FORWARDED_HEADER_NAMES.add("X-Forwarded-Host");
53-
FORWARDED_HEADER_NAMES.add("X-Forwarded-Port");
54-
FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto");
55-
FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
56-
FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
57-
}
58-
59-
60-
private boolean removeOnly;
61-
62-
63-
/**
64-
* Enables mode in which any "Forwarded" or "X-Forwarded-*" headers are
65-
* removed only and the information in them ignored.
66-
* @param removeOnly whether to discard and ignore forwarded headers
67-
*/
68-
public void setRemoveOnly(boolean removeOnly) {
69-
this.removeOnly = removeOnly;
70-
}
71-
45+
@Deprecated
46+
public class ForwardedHeaderFilter extends ForwardedHeaderTransformer implements WebFilter {
7247

7348
@Override
7449
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
7550
ServerHttpRequest request = exchange.getRequest();
76-
if (!hasForwardedHeaders(request)) {
77-
return chain.filter(exchange);
78-
}
79-
80-
ServerWebExchange mutatedExchange;
81-
if (this.removeOnly) {
82-
mutatedExchange = exchange.mutate().request(this::removeForwardedHeaders).build();
83-
}
84-
else {
85-
mutatedExchange = exchange.mutate()
86-
.request(builder -> {
87-
URI uri = UriComponentsBuilder.fromHttpRequest(request).build().toUri();
88-
builder.uri(uri);
89-
String prefix = getForwardedPrefix(request);
90-
if (prefix != null) {
91-
builder.path(prefix + uri.getPath());
92-
builder.contextPath(prefix);
93-
}
94-
removeForwardedHeaders(builder);
95-
})
96-
.build();
97-
}
98-
99-
return chain.filter(mutatedExchange);
100-
}
101-
102-
private boolean hasForwardedHeaders(ServerHttpRequest request) {
103-
HttpHeaders headers = request.getHeaders();
104-
for (String headerName : FORWARDED_HEADER_NAMES) {
105-
if (headers.containsKey(headerName)) {
106-
return true;
107-
}
51+
if (hasForwardedHeaders(request)) {
52+
exchange = exchange.mutate().request(apply(request)).build();
10853
}
109-
return false;
110-
}
111-
112-
@Nullable
113-
private static String getForwardedPrefix(ServerHttpRequest request) {
114-
HttpHeaders headers = request.getHeaders();
115-
String prefix = headers.getFirst("X-Forwarded-Prefix");
116-
if (prefix != null) {
117-
int endIndex = prefix.length();
118-
while (endIndex > 1 && prefix.charAt(endIndex - 1) == '/') {
119-
endIndex--;
120-
}
121-
prefix = (endIndex != prefix.length() ? prefix.substring(0, endIndex) : prefix);
122-
}
123-
return prefix;
124-
}
125-
126-
private ServerHttpRequest.Builder removeForwardedHeaders(ServerHttpRequest.Builder builder) {
127-
return builder.headers(map -> FORWARDED_HEADER_NAMES.forEach(map::remove));
54+
return chain.filter(exchange);
12855
}
12956

13057
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.server.adapter;
17+
18+
import java.net.URI;
19+
import java.util.LinkedHashSet;
20+
import java.util.Set;
21+
import java.util.function.Function;
22+
23+
import org.springframework.http.HttpHeaders;
24+
import org.springframework.http.server.reactive.ServerHttpRequest;
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.web.util.UriComponentsBuilder;
27+
28+
/**
29+
* Extract values from "Forwarded" and "X-Forwarded-*" headers to override the
30+
* request URI (i.e. {@link ServerHttpRequest#getURI()}) so it reflects the
31+
* client-originated protocol and address.
32+
*
33+
* <p>Alternatively if {@link #setRemoveOnly removeOnly} is set to "true", then
34+
* "Forwarded" and "X-Forwarded-*" headers are only removed, and not used.
35+
*
36+
* @author Rossen Stoyanchev
37+
* @since 5.1
38+
* @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a>
39+
*/
40+
public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, ServerHttpRequest> {
41+
42+
static final Set<String> FORWARDED_HEADER_NAMES = new LinkedHashSet<>(5);
43+
44+
static {
45+
FORWARDED_HEADER_NAMES.add("Forwarded");
46+
FORWARDED_HEADER_NAMES.add("X-Forwarded-Host");
47+
FORWARDED_HEADER_NAMES.add("X-Forwarded-Port");
48+
FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto");
49+
FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
50+
FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
51+
}
52+
53+
54+
private boolean removeOnly;
55+
56+
57+
/**
58+
* Enables mode in which any "Forwarded" or "X-Forwarded-*" headers are
59+
* removed only and the information in them ignored.
60+
* @param removeOnly whether to discard and ignore forwarded headers
61+
*/
62+
public void setRemoveOnly(boolean removeOnly) {
63+
this.removeOnly = removeOnly;
64+
}
65+
66+
/**
67+
* Whether the "remove only" mode is on.
68+
*/
69+
public boolean isRemoveOnly() {
70+
return this.removeOnly;
71+
}
72+
73+
74+
/**
75+
* Apply and remove, or remove Forwarded type headers.
76+
* @param request the request
77+
*/
78+
@Override
79+
public ServerHttpRequest apply(ServerHttpRequest request) {
80+
81+
if (hasForwardedHeaders(request)) {
82+
ServerHttpRequest.Builder builder = request.mutate();
83+
if (!this.removeOnly) {
84+
URI uri = UriComponentsBuilder.fromHttpRequest(request).build().toUri();
85+
builder.uri(uri);
86+
String prefix = getForwardedPrefix(request);
87+
if (prefix != null) {
88+
builder.path(prefix + uri.getPath());
89+
builder.contextPath(prefix);
90+
}
91+
}
92+
removeForwardedHeaders(builder);
93+
request = builder.build();
94+
}
95+
96+
return request;
97+
}
98+
99+
/**
100+
* Whether the request has any Forwarded headers.
101+
* @param request the request
102+
*/
103+
protected boolean hasForwardedHeaders(ServerHttpRequest request) {
104+
HttpHeaders headers = request.getHeaders();
105+
for (String headerName : FORWARDED_HEADER_NAMES) {
106+
if (headers.containsKey(headerName)) {
107+
return true;
108+
}
109+
}
110+
return false;
111+
}
112+
113+
private void removeForwardedHeaders(ServerHttpRequest.Builder builder) {
114+
builder.headers(map -> FORWARDED_HEADER_NAMES.forEach(map::remove));
115+
}
116+
117+
@Nullable
118+
private static String getForwardedPrefix(ServerHttpRequest request) {
119+
HttpHeaders headers = request.getHeaders();
120+
String prefix = headers.getFirst("X-Forwarded-Prefix");
121+
if (prefix != null) {
122+
int endIndex = prefix.length();
123+
while (endIndex > 1 && prefix.charAt(endIndex - 1) == '/') {
124+
endIndex--;
125+
}
126+
prefix = (endIndex != prefix.length() ? prefix.substring(0, endIndex) : prefix);
127+
}
128+
return prefix;
129+
}
130+
131+
}

spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
9292

9393
private LocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
9494

95+
@Nullable
96+
private ForwardedHeaderTransformer forwardedHeaderTransformer;
97+
9598
@Nullable
9699
private ApplicationContext applicationContext;
97100

@@ -169,6 +172,27 @@ public LocaleContextResolver getLocaleContextResolver() {
169172
return this.localeContextResolver;
170173
}
171174

175+
/**
176+
* Enable processing of forwarded headers, either extracting and removing,
177+
* or remove only.
178+
* <p>By default this is not set.
179+
* @param transformer the transformer to use
180+
* @since 5.1
181+
*/
182+
public void setForwardedHeaderTransformer(ForwardedHeaderTransformer transformer) {
183+
Assert.notNull(transformer, "ForwardedHeaderTransformer is required");
184+
this.forwardedHeaderTransformer = transformer;
185+
}
186+
187+
/**
188+
* Return the configured {@link ForwardedHeaderTransformer}.
189+
* @since 5.1
190+
*/
191+
@Nullable
192+
public ForwardedHeaderTransformer getForwardedHeaderTransformer() {
193+
return this.forwardedHeaderTransformer;
194+
}
195+
172196
/**
173197
* Configure the {@code ApplicationContext} associated with the web application,
174198
* if it was initialized with one via
@@ -208,6 +232,10 @@ public void afterPropertiesSet() {
208232
@Override
209233
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
210234

235+
if (this.forwardedHeaderTransformer != null) {
236+
request = this.forwardedHeaderTransformer.apply(request);
237+
}
238+
211239
ServerWebExchange exchange = createExchange(request, response);
212240
logExchange(exchange);
213241

0 commit comments

Comments
 (0)