Skip to content

Commit 35958dc

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 35958dc

File tree

6 files changed

+131
-23
lines changed

6 files changed

+131
-23
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

+28-16
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,38 @@ 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-
318-
// special case handling for X-Forwarded-Ssl:
319-
// apply it, but only if X-Forwarded-Proto is unset.
317+
ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);
318+
319+
// As of SPR-16863, Spring 5.1 can handle X-Forwarded-Ssl headers...
320+
if (isSpringAtLeast5_1()) {
321+
//return builder;
322+
return handleXForwardedSslHeader(request, builder);
323+
} else {
324+
return handleXForwardedSslHeader(request, builder);
325+
}
326+
}
320327

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");
328+
/**
329+
* Check if the current version of Spring Framework is 5.1 or higher.
330+
*
331+
* @return
332+
*/
333+
private static boolean isSpringAtLeast5_1() {
334+
335+
String versionOfSpringFramework = ApplicationContext.class.getPackage().getImplementationVersion();
324336

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

329-
return builder;
341+
return (majorVersion >= 5 && minorVersion >= 1) || (majorVersion > 5);
330342
}
331343

332344
/**
@@ -361,13 +373,13 @@ private static Collection<Affordance> findAffordances(MethodInvocation invocatio
361373
private static class CachingAnnotationMappingDiscoverer implements MappingDiscoverer {
362374

363375
private final @Delegate AnnotationMappingDiscoverer delegate;
364-
private final Map<String, UriTemplate> templates = new ConcurrentReferenceHashMap<>();
376+
private final Map<String, UriTemplate> templates = new ConcurrentReferenceHashMap<>();
365377

366378
public UriTemplate getMappingAsUriTemplate(Class<?> type, Method method) {
367379

368380
String mapping = delegate.getMapping(type, method);
369-
370-
return templates.computeIfAbsent(mapping, UriTemplate::new);
381+
382+
return templates.computeIfAbsent(mapping, UriTemplate::new);
371383
}
372384
}
373385

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

+25-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
}
@@ -199,6 +204,8 @@ public void usesForwardedSslAndHostIfHeaderIsSet() {
199204
request.addHeader("X-Forwarded-Host", "somethingDifferent");
200205
request.addHeader("X-Forwarded-Ssl", "on");
201206

207+
adaptRequestFromForwardedHeaders();
208+
202209
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
203210
assertThat(link.getHref(), startsWith("https://somethingDifferent"));
204211
}
@@ -270,6 +277,8 @@ public void usesForwardedHostAndPortFromHeader() {
270277

271278
request.addHeader("X-Forwarded-Host", "foobar:8088");
272279

280+
adaptRequestFromForwardedHeaders();
281+
273282
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
274283
assertThat(link.getHref(), startsWith("http://foobar:8088"));
275284
}
@@ -282,6 +291,8 @@ public void usesFirstHostOfXForwardedHost() {
282291

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

294+
adaptRequestFromForwardedHeaders();
295+
285296
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
286297
assertThat(link.getHref(), startsWith("http://barfoo:8888"));
287298
}
@@ -335,6 +346,8 @@ public void usesForwardedPortFromHeader() {
335346
request.addHeader("X-Forwarded-Port", "9090");
336347
request.setServerPort(8080);
337348

349+
adaptRequestFromForwardedHeaders();
350+
338351
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
339352

340353
assertThat(link.getHref(), startsWith("http://foobarhost:9090/"));
@@ -349,6 +362,8 @@ public void usesForwardedHostFromHeaderWithDefaultPort() {
349362
request.addHeader("X-Forwarded-Host", "foobarhost");
350363
request.setServerPort(8080);
351364

365+
adaptRequestFromForwardedHeaders();
366+
352367
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
353368
assertThat(link.getHref(), startsWith("http://foobarhost/"));
354369
}
@@ -410,12 +425,14 @@ public void usesRootMappingOfTargetClassForMethodsOfParent() throws Exception {
410425
* @see #257, #107
411426
*/
412427
@Test
413-
public void usesXForwardedProtoHeaderAsLinkSchema() {
428+
public void usesXForwardedProtoHeaderAsLinkSchema() throws ServletException, IOException {
414429

415430
for (String proto : Arrays.asList("http", "https")) {
416431

417432
setUp();
418433
request.addHeader("X-Forwarded-Proto", proto);
434+
435+
adaptRequestFromForwardedHeaders();
419436

420437
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
421438
assertThat(link.getHref(), startsWith(proto + "://"));
@@ -433,6 +450,8 @@ public void usesProtoValueFromForwardedHeaderAsLinkSchema() {
433450
setUp();
434451
request.addHeader("Forwarded", new String[] { "proto=" + proto });
435452

453+
adaptRequestFromForwardedHeaders();
454+
436455
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
437456
assertThat(link.getHref(), startsWith(proto.concat("://")));
438457
}
@@ -445,7 +464,9 @@ public void usesProtoValueFromForwardedHeaderAsLinkSchema() {
445464
public void favorsStandardForwardHeaderOverXForwardedProto() {
446465

447466
request.addHeader("X-Forwarded-Proto", "foo");
448-
request.addHeader(ForwardedHeader.NAME, "proto=bar");
467+
request.addHeader("Forwarded", "proto=bar");
468+
469+
adaptRequestFromForwardedHeaders();
449470

450471
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
451472
assertThat(link.getHref(), startsWith("bar://"));
@@ -527,6 +548,8 @@ public void supportsTwoProxiesAddingXForwardedPort() {
527548
request.addHeader("X-Forwarded-Port", "1443,8443");
528549
request.addHeader("X-Forwarded-Host", "proxy1,proxy2");
529550

551+
adaptRequestFromForwardedHeaders();
552+
530553
assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref(), startsWith("http://proxy1:1443"));
531554
}
532555

0 commit comments

Comments
 (0)