Skip to content

Commit 45ae588

Browse files
committed
#713 - Centralize all Forward header handling
Spring 5.1 is centralizing all Forward header handling. This moves critical bits into one location, making it easy to completely remove once we rebaseline against this version. Also adds a test profile to ensure Spring 5.1 doesn't break anything.
1 parent ef3e3c4 commit 45ae588

File tree

6 files changed

+133
-22
lines changed

6 files changed

+133
-22
lines changed

.travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ env:
55
matrix:
66
- PROFILE=non-existant
77
- PROFILE=spring5-next
8+
- PROFILE=spring51-next
89
addons:
910
apt:
1011
packages:

pom.xml

+14
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@
9797
</repositories>
9898
</profile>
9999

100+
<profile>
101+
<id>spring51-next</id>
102+
<properties>
103+
<spring.version>5.1.0.BUILD-SNAPSHOT</spring.version>
104+
<jackson.version>2.9.2</jackson.version>
105+
</properties>
106+
<repositories>
107+
<repository>
108+
<id>spring-libs-snapshot</id>
109+
<url>http://repo.spring.io/libs-snapshot</url>
110+
</repository>
111+
</repositories>
112+
</profile>
113+
100114
<profile>
101115

102116
<!-- Profile to be run on the CI server, JARs JavaDocs -->

src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java

+26-15
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.hateoas.mvc;
1717

18-
import static org.springframework.util.StringUtils.*;
18+
import static org.springframework.hateoas.mvc.ForwardedHeader.handleXForwardedSslHeader;
1919

2020
import lombok.RequiredArgsConstructor;
2121
import lombok.experimental.Delegate;
@@ -28,6 +28,7 @@
2828

2929
import javax.servlet.http.HttpServletRequest;
3030

31+
import org.springframework.context.ApplicationContext;
3132
import org.springframework.core.io.support.SpringFactoriesLoader;
3233
import org.springframework.hateoas.Affordance;
3334
import org.springframework.hateoas.Link;
@@ -306,27 +307,37 @@ public String toString() {
306307
*
307308
* @return
308309
*/
309-
public static UriComponentsBuilder getBuilder() {
310+
public static UriComponentsBuilder getBuilder() {
310311

311312
if (RequestContextHolder.getRequestAttributes() == null) {
312313
return UriComponentsBuilder.fromPath("/");
313314
}
314315

315316
HttpServletRequest request = getCurrentRequest();
316-
UriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);
317+
ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);
317318

318-
// special case handling for X-Forwarded-Ssl:
319-
// apply it, but only if X-Forwarded-Proto is unset.
319+
// Spring 5.1 can handle X-Forwarded-Ssl headers...
320+
if (isSpringAtLeast5_1()) {
321+
return builder;
322+
} else {
323+
return handleXForwardedSslHeader(request, builder);
324+
}
325+
}
320326

321-
String forwardedSsl = request.getHeader("X-Forwarded-Ssl");
322-
ForwardedHeader forwarded = ForwardedHeader.of(request.getHeader(ForwardedHeader.NAME));
323-
String proto = hasText(forwarded.getProto()) ? forwarded.getProto() : request.getHeader("X-Forwarded-Proto");
327+
/**
328+
* Check if the current version of Spring Framework is 5.1 or higher.
329+
*
330+
* @return
331+
*/
332+
private static boolean isSpringAtLeast5_1() {
333+
334+
String versionOfSpringFramework = ApplicationContext.class.getPackage().getImplementationVersion();
324335

325-
if (!hasText(proto) && hasText(forwardedSsl) && forwardedSsl.equalsIgnoreCase("on")) {
326-
builder.scheme("https");
327-
}
336+
String[] parts = versionOfSpringFramework.split("\\.");
337+
int majorVersion = Integer.parseInt(parts[0]);
338+
int minorVersion = Integer.parseInt(parts[1]);
328339

329-
return builder;
340+
return (majorVersion >= 5 && minorVersion >= 1) || (majorVersion > 5);
330341
}
331342

332343
/**
@@ -361,13 +372,13 @@ private static Collection<Affordance> findAffordances(MethodInvocation invocatio
361372
private static class CachingAnnotationMappingDiscoverer implements MappingDiscoverer {
362373

363374
private final @Delegate AnnotationMappingDiscoverer delegate;
364-
private final Map<String, UriTemplate> templates = new ConcurrentReferenceHashMap<>();
375+
private final Map<String, UriTemplate> templates = new ConcurrentReferenceHashMap<>();
365376

366377
public UriTemplate getMappingAsUriTemplate(Class<?> type, Method method) {
367378

368379
String mapping = delegate.getMapping(type, method);
369-
370-
return templates.computeIfAbsent(mapping, UriTemplate::new);
380+
381+
return templates.computeIfAbsent(mapping, UriTemplate::new);
371382
}
372383
}
373384

src/main/java/org/springframework/hateoas/mvc/ForwardedHeader.java

+37-5
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,70 @@
1515
*/
1616
package org.springframework.hateoas.mvc;
1717

18+
import static org.springframework.util.StringUtils.hasText;
19+
1820
import java.util.Arrays;
1921
import java.util.Collections;
2022
import java.util.Map;
2123
import java.util.stream.Collectors;
2224

25+
import javax.servlet.http.HttpServletRequest;
26+
2327
import org.springframework.util.Assert;
2428
import org.springframework.util.StringUtils;
29+
import org.springframework.web.util.UriComponentsBuilder;
2530

2631
/**
2732
* Value object to partially implement the {@literal Forwarded} header defined in RFC 7239.
2833
*
2934
* @author Oliver Gierke
3035
* @see http://tools.ietf.org/html/rfc7239
36+
* @deprecated In Spring 5.1, all Forwarded headers will by handled by Spring MVC.
3137
*/
38+
@Deprecated
3239
class ForwardedHeader {
3340

34-
public static String NAME = "Forwarded";
3541
private static final ForwardedHeader NO_HEADER = new ForwardedHeader(Collections.emptyMap());
36-
3742
private final Map<String, String> elements;
3843

3944
private ForwardedHeader(Map<String, String> elements) {
4045
this.elements = elements;
4146
}
4247

48+
/**
49+
* Utility method to pull handling of {@literal X-Forwarded-Ssl} into a class that will be removed when
50+
* rebaselined against Spring 5.1
51+
*
52+
* @param request
53+
* @param builder
54+
* @return
55+
* @deprecated No longer needed with Spring 5.1
56+
*/
57+
@Deprecated
58+
public static UriComponentsBuilder handleXForwardedSslHeader(HttpServletRequest request, UriComponentsBuilder builder) {
59+
60+
// special case handling for X-Forwarded-Ssl:
61+
// apply it, but only if X-Forwarded-Proto is unset.
62+
63+
String forwardedSsl = request.getHeader("X-Forwarded-Ssl");
64+
ForwardedHeader forwarded = ForwardedHeader.of(request.getHeader("Forwarded"));
65+
String proto = hasText(forwarded.getProto()) ? forwarded.getProto() : request.getHeader("X-Forwarded-Proto");
66+
67+
if (!hasText(proto) && hasText(forwardedSsl) && forwardedSsl.equalsIgnoreCase("on")) {
68+
builder.scheme("https");
69+
}
70+
71+
return builder;
72+
73+
}
74+
4375
/**
4476
* Creates a new {@link ForwardedHeader} from the given source.
4577
*
4678
* @param source can be {@literal null}.
4779
* @return
4880
*/
49-
public static ForwardedHeader of(String source) {
81+
static ForwardedHeader of(String source) {
5082

5183
if (!StringUtils.hasText(source)) {
5284
return NO_HEADER;
@@ -67,7 +99,7 @@ public static ForwardedHeader of(String source) {
6799
*
68100
* @return
69101
*/
70-
public String getProto() {
102+
String getProto() {
71103
return elements.get("proto");
72104
}
73105

@@ -76,7 +108,7 @@ public String getProto() {
76108
*
77109
* @return
78110
*/
79-
public String getHost() {
111+
String getHost() {
80112
return elements.get("host");
81113
}
82114
}

src/test/java/org/springframework/hateoas/TestUtils.java

+26
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,18 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import java.io.IOException;
21+
22+
import javax.servlet.ServletException;
23+
import javax.servlet.http.HttpServletRequest;
24+
2025
import org.junit.Before;
26+
import org.springframework.mock.web.MockFilterChain;
2127
import org.springframework.mock.web.MockHttpServletRequest;
28+
import org.springframework.mock.web.MockHttpServletResponse;
2229
import org.springframework.web.context.request.RequestContextHolder;
2330
import org.springframework.web.context.request.ServletRequestAttributes;
31+
import org.springframework.web.filter.ForwardedHeaderFilter;
2432

2533
/**
2634
* Utility class to ease testing.
@@ -43,6 +51,24 @@ protected void assertPointsToMockServer(Link link) {
4351
assertThat(link.getHref()).startsWith("http://localhost");
4452
}
4553

54+
/**
55+
* Provide a mechanism to simulate inserting a {@link ForwardedHeaderFilter} into the servlet
56+
* filter chain, so {@literal Forwarded} headers are properly inserted into the test web request.
57+
*
58+
* @see https://jira.spring.io/browse/SPR-16668
59+
*/
60+
protected void adaptRequestFromForwardedHeaders() {
61+
62+
MockFilterChain chain = new MockFilterChain();
63+
try {
64+
new ForwardedHeaderFilter().doFilter(this.request, new MockHttpServletResponse(), chain);
65+
} catch (ServletException | IOException e) {
66+
throw new RuntimeException(e);
67+
}
68+
HttpServletRequest adaptedRequest = (HttpServletRequest) chain.getRequest();
69+
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(adaptedRequest));
70+
}
71+
4672
public static void assertEqualAndSameHashCode(Object left, Object right) {
4773

4874
assertThat(left).isEqualTo(right);

src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java

+29-2
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020
import static org.hamcrest.Matchers.*;
2121
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
2222

23+
import java.io.IOException;
2324
import java.lang.reflect.Method;
2425
import java.util.Arrays;
2526
import java.util.List;
2627
import java.util.Optional;
2728

29+
import javax.servlet.ServletException;
30+
2831
import org.junit.Rule;
2932
import org.junit.Test;
3033
import org.junit.rules.ExpectedException;
@@ -162,6 +165,8 @@ public void usesForwardedHostAsHostIfHeaderIsSet() {
162165

163166
request.addHeader("X-Forwarded-Host", "somethingDifferent");
164167

168+
adaptRequestFromForwardedHeaders();
169+
165170
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
166171
assertThat(link.getHref(), startsWith("http://somethingDifferent"));
167172
}
@@ -174,6 +179,8 @@ public void usesForwardedSslIfHeaderIsSet() {
174179

175180
request.addHeader("X-Forwarded-Ssl", "on");
176181

182+
adaptRequestFromForwardedHeaders();
183+
177184
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
178185
assertThat(link.getHref(), startsWith("https://"));
179186
}
@@ -186,6 +193,8 @@ public void usesForwardedSslIfHeaderIsSetOff() {
186193

187194
request.addHeader("X-Forwarded-Ssl", "off");
188195

196+
adaptRequestFromForwardedHeaders();
197+
189198
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
190199
assertThat(link.getHref(), startsWith("http://"));
191200
}
@@ -199,6 +208,8 @@ public void usesForwardedSslAndHostIfHeaderIsSet() {
199208
request.addHeader("X-Forwarded-Host", "somethingDifferent");
200209
request.addHeader("X-Forwarded-Ssl", "on");
201210

211+
adaptRequestFromForwardedHeaders();
212+
202213
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
203214
assertThat(link.getHref(), startsWith("https://somethingDifferent"));
204215
}
@@ -270,6 +281,8 @@ public void usesForwardedHostAndPortFromHeader() {
270281

271282
request.addHeader("X-Forwarded-Host", "foobar:8088");
272283

284+
adaptRequestFromForwardedHeaders();
285+
273286
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
274287
assertThat(link.getHref(), startsWith("http://foobar:8088"));
275288
}
@@ -282,6 +295,8 @@ public void usesFirstHostOfXForwardedHost() {
282295

283296
request.addHeader("X-Forwarded-Host", "barfoo:8888, localhost:8088");
284297

298+
adaptRequestFromForwardedHeaders();
299+
285300
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
286301
assertThat(link.getHref(), startsWith("http://barfoo:8888"));
287302
}
@@ -335,6 +350,8 @@ public void usesForwardedPortFromHeader() {
335350
request.addHeader("X-Forwarded-Port", "9090");
336351
request.setServerPort(8080);
337352

353+
adaptRequestFromForwardedHeaders();
354+
338355
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
339356

340357
assertThat(link.getHref(), startsWith("http://foobarhost:9090/"));
@@ -349,6 +366,8 @@ public void usesForwardedHostFromHeaderWithDefaultPort() {
349366
request.addHeader("X-Forwarded-Host", "foobarhost");
350367
request.setServerPort(8080);
351368

369+
adaptRequestFromForwardedHeaders();
370+
352371
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
353372
assertThat(link.getHref(), startsWith("http://foobarhost/"));
354373
}
@@ -410,12 +429,14 @@ public void usesRootMappingOfTargetClassForMethodsOfParent() throws Exception {
410429
* @see #257, #107
411430
*/
412431
@Test
413-
public void usesXForwardedProtoHeaderAsLinkSchema() {
432+
public void usesXForwardedProtoHeaderAsLinkSchema() throws ServletException, IOException {
414433

415434
for (String proto : Arrays.asList("http", "https")) {
416435

417436
setUp();
418437
request.addHeader("X-Forwarded-Proto", proto);
438+
439+
adaptRequestFromForwardedHeaders();
419440

420441
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
421442
assertThat(link.getHref(), startsWith(proto + "://"));
@@ -433,6 +454,8 @@ public void usesProtoValueFromForwardedHeaderAsLinkSchema() {
433454
setUp();
434455
request.addHeader("Forwarded", new String[] { "proto=" + proto });
435456

457+
adaptRequestFromForwardedHeaders();
458+
436459
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
437460
assertThat(link.getHref(), startsWith(proto.concat("://")));
438461
}
@@ -445,7 +468,9 @@ public void usesProtoValueFromForwardedHeaderAsLinkSchema() {
445468
public void favorsStandardForwardHeaderOverXForwardedProto() {
446469

447470
request.addHeader("X-Forwarded-Proto", "foo");
448-
request.addHeader(ForwardedHeader.NAME, "proto=bar");
471+
request.addHeader("Forwarded", "proto=bar");
472+
473+
adaptRequestFromForwardedHeaders();
449474

450475
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
451476
assertThat(link.getHref(), startsWith("bar://"));
@@ -527,6 +552,8 @@ public void supportsTwoProxiesAddingXForwardedPort() {
527552
request.addHeader("X-Forwarded-Port", "1443,8443");
528553
request.addHeader("X-Forwarded-Host", "proxy1,proxy2");
529554

555+
adaptRequestFromForwardedHeaders();
556+
530557
assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref(), startsWith("http://proxy1:1443"));
531558
}
532559

0 commit comments

Comments
 (0)