From 4611d058c8e702a2bddc5ecfab7241860b3b2e38 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 21 May 2015 11:43:56 +0200 Subject: [PATCH] Support Forwarded-Header in UriComponentsBuilder This commit introduces support for RFC 7239: Forwarded HTTP Extension in the UriComponentsBuilder. Unfortunately, RFC 7239 is not a complete replacement for the X-Forwarded-* headers: specifically, there is not direct replacement for X-Forwarded-Port. The JIRA contains more information. Issue: SPR-11856 --- .../web/util/UriComponentsBuilder.java | 62 +++++++++++------ .../web/util/UriComponentsBuilderTests.java | 68 +++++++++++++++++++ 2 files changed, 109 insertions(+), 21 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index 95ec54bdb6..9be3f71d07 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -88,6 +88,10 @@ public class UriComponentsBuilder implements Cloneable { "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" + PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?"); + private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?"); + + private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?"); + private String scheme; @@ -267,7 +271,9 @@ public class UriComponentsBuilder implements Cloneable { /** * Create a new {@code UriComponents} object from the URI associated with * the given HttpRequest while also overlaying with values from the headers - * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if present. + * "Forwarded" (RFC 7239, or + * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if "Forwarded" is + * not found. * @param request the source request * @return the URI components of the URI * @since 4.1.5 @@ -280,31 +286,45 @@ public class UriComponentsBuilder implements Cloneable { String host = uri.getHost(); int port = uri.getPort(); - String hostHeader = request.getHeaders().getFirst("X-Forwarded-Host"); - if (StringUtils.hasText(hostHeader)) { - String[] hosts = StringUtils.commaDelimitedListToStringArray(hostHeader); - String hostToUse = hosts[0]; - if (hostToUse.contains(":")) { - String[] hostAndPort = StringUtils.split(hostToUse, ":"); - host = hostAndPort[0]; - port = Integer.parseInt(hostAndPort[1]); + String forwardedHeader = request.getHeaders().getFirst("Forwarded"); + if (StringUtils.hasText(forwardedHeader)) { + String forwardedToUse = StringUtils.commaDelimitedListToStringArray(forwardedHeader)[0]; + Matcher m = FORWARDED_HOST_PATTERN.matcher(forwardedToUse); + if (m.find()) { + host = m.group(1).trim(); } - else { - host = hostToUse; - port = -1; + m = FORWARDED_PROTO_PATTERN.matcher(forwardedToUse); + if (m.find()) { + scheme = m.group(1).trim(); } } + else { + String hostHeader = request.getHeaders().getFirst("X-Forwarded-Host"); + if (StringUtils.hasText(hostHeader)) { + String[] hosts = StringUtils.commaDelimitedListToStringArray(hostHeader); + String hostToUse = hosts[0]; + if (hostToUse.contains(":")) { + String[] hostAndPort = StringUtils.split(hostToUse, ":"); + host = hostAndPort[0]; + port = Integer.parseInt(hostAndPort[1]); + } + else { + host = hostToUse; + port = -1; + } + } - String portHeader = request.getHeaders().getFirst("X-Forwarded-Port"); - if (StringUtils.hasText(portHeader)) { - String[] ports = StringUtils.commaDelimitedListToStringArray(portHeader); - port = Integer.parseInt(ports[0]); - } + String portHeader = request.getHeaders().getFirst("X-Forwarded-Port"); + if (StringUtils.hasText(portHeader)) { + String[] ports = StringUtils.commaDelimitedListToStringArray(portHeader); + port = Integer.parseInt(ports[0]); + } - String protocolHeader = request.getHeaders().getFirst("X-Forwarded-Proto"); - if (StringUtils.hasText(protocolHeader)) { - String[] protocols = StringUtils.commaDelimitedListToStringArray(protocolHeader); - scheme = protocols[0]; + String protocolHeader = request.getHeaders().getFirst("X-Forwarded-Proto"); + if (StringUtils.hasText(protocolHeader)) { + String[] protocols = StringUtils.commaDelimitedListToStringArray(protocolHeader); + scheme = protocols[0]; + } } builder.scheme(scheme); diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index f6753f0528..f88bc1d929 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -673,4 +673,72 @@ public class UriComponentsBuilderTests { assertEquals("f2", result2.getFragment()); } + // SPR-11856 + + @Test + public void fromHttpRequestForwardedHeader() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Forwarded", "proto=https; host=84.198.58.199"); + request.setScheme("http"); + request.setServerName("example.com"); + request.setRequestURI("/rest/mobile/users/1"); + + HttpRequest httpRequest = new ServletServerHttpRequest(request); + UriComponents result = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); + + assertEquals("https", result.getScheme()); + assertEquals("84.198.58.199", result.getHost()); + assertEquals("/rest/mobile/users/1", result.getPath()); + } + + @Test + public void fromHttpRequestForwardedHeaderQuoted() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Forwarded", "proto=\"https\"; host=\"84.198.58.199\""); + request.setScheme("http"); + request.setServerName("example.com"); + request.setRequestURI("/rest/mobile/users/1"); + + HttpRequest httpRequest = new ServletServerHttpRequest(request); + UriComponents result = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); + + assertEquals("https", result.getScheme()); + assertEquals("84.198.58.199", result.getHost()); + assertEquals("/rest/mobile/users/1", result.getPath()); + } + + @Test + public void fromHttpRequestMultipleForwardedHeader() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Forwarded", "host=84.198.58.199;proto=https"); + request.addHeader("Forwarded", "proto=ftp; host=1.2.3.4"); + request.setScheme("http"); + request.setServerName("example.com"); + request.setRequestURI("/rest/mobile/users/1"); + + HttpRequest httpRequest = new ServletServerHttpRequest(request); + UriComponents result = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); + + assertEquals("https", result.getScheme()); + assertEquals("84.198.58.199", result.getHost()); + assertEquals("/rest/mobile/users/1", result.getPath()); + } + + @Test + public void fromHttpRequestMultipleForwardedHeaderComma() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Forwarded", "host=84.198.58.199 ;proto=https, proto=ftp; host=1.2.3.4"); + request.setScheme("http"); + request.setServerName("example.com"); + request.setRequestURI("/rest/mobile/users/1"); + + HttpRequest httpRequest = new ServletServerHttpRequest(request); + UriComponents result = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); + + assertEquals("https", result.getScheme()); + assertEquals("84.198.58.199", result.getHost()); + assertEquals("/rest/mobile/users/1", result.getPath()); + } + + }