From 2dd4480103716fd1f68c56efd519cbc20210740d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 24 Sep 2013 12:03:53 +0200 Subject: [PATCH 1/2] Add IPv6 support in RestTemplate Prior to this commit, RestTemplate would not would not accept IPv6 raw addresses in URLs because UriComponentsBuilder would not parse/encode the Host part correctly. The UriComponentsBuilder now parses and encode raw IPv6 addresses in the "[1abc:2abc:3abc::5ABC:6abc]" format and also supports the use of IPv6 scope_ids (see JDK8 java.net.Inet6Address), like "[1abc:2abc:3abc::5ABC:6abc%eth0]". Issue: SPR-10539 --- .../web/util/HierarchicalUriComponents.java | 14 ++++++++++- .../web/util/UriComponentsBuilder.java | 12 +++++++-- .../web/util/UriComponentsBuilderTests.java | 25 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index 60ac593247..1bddd7a4e6 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -182,7 +182,13 @@ final class HierarchicalUriComponents extends UriComponents { } String encodedScheme = encodeUriComponent(this.getScheme(), encoding, Type.SCHEME); String encodedUserInfo = encodeUriComponent(this.userInfo, encoding, Type.USER_INFO); - String encodedHost = encodeUriComponent(this.host, encoding, Type.HOST); + String encodedHost; + if(StringUtils.hasLength(this.host) && this.host.startsWith("[")) { + encodedHost = encodeUriComponent(this.host, encoding, Type.HOST_IPV6); + } else { + encodedHost = encodeUriComponent(this.host, encoding, Type.HOST); + } + PathComponent encodedPath = this.path.encode(encoding); MultiValueMap encodedQueryParams = new LinkedMultiValueMap(this.queryParams.size()); @@ -468,6 +474,12 @@ final class HierarchicalUriComponents extends UriComponents { return isUnreserved(c) || isSubDelimiter(c); } }, + HOST_IPV6 { + @Override + public boolean isAllowed(int c) { + return isUnreserved(c) || isSubDelimiter(c) || '[' == c || ']' == c || ':' == c; + } + }, PORT { @Override public boolean isAllowed(int c) { 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 006abc3c4b..f9d20ebf1b 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 @@ -64,7 +64,11 @@ public class UriComponentsBuilder { private static final String USERINFO_PATTERN = "([^@/]*)"; - private static final String HOST_PATTERN = "([^/?#:]*)"; + private static final String HOST_IPv4_PATTERN = "[^\\[/?#:]*"; + + private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}\\:\\.]*[%\\p{Alnum}]*\\]"; + + private static final String HOST_PATTERN = "("+HOST_IPV6_PATTERN + "|" + HOST_IPv4_PATTERN + ")"; private static final String PORT_PATTERN = "(\\d*)"; @@ -226,7 +230,11 @@ public class UriComponentsBuilder { String scheme = m.group(1); builder.scheme((scheme != null) ? scheme.toLowerCase() : scheme); builder.userInfo(m.group(4)); - builder.host(m.group(5)); + String host = m.group(5); + if(StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) { + throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL"); + } + builder.host(host); String port = m.group(7); if (StringUtils.hasLength(port)) { builder.port(Integer.parseInt(port)); 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 beb276fc3c..a62e593b21 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 @@ -169,6 +169,31 @@ public class UriComponentsBuilderTests { assertEquals("https", UriComponentsBuilder.fromHttpUrl("HTTPS://www.google.com").build().getScheme()); } + // SPR-10539 + + @Test(expected = IllegalArgumentException.class) + public void fromHttpUrlStringInvalidIPv6Host() throws URISyntaxException { + UriComponents result = UriComponentsBuilder + .fromHttpUrl("http://[1abc:2abc:3abc::5ABC:6abc:8080/resource").build().encode(); + } + + // SPR-10539 + + @Test + public void fromUriStringIPv6Host() throws URISyntaxException { + UriComponents result = UriComponentsBuilder + .fromUriString("http://[1abc:2abc:3abc::5ABC:6abc]:8080/resource").build().encode(); + assertEquals("[1abc:2abc:3abc::5ABC:6abc]",result.getHost()); + + UriComponents resultWithScopeId = UriComponentsBuilder + .fromUriString("http://[1abc:2abc:3abc::5ABC:6abc%eth0]:8080/resource").build().encode(); + assertEquals("[1abc:2abc:3abc::5ABC:6abc%25eth0]",resultWithScopeId.getHost()); + + UriComponents resultIPv4compatible = UriComponentsBuilder + .fromUriString("http://[::192.168.1.1]:8080/resource").build().encode(); + assertEquals("[::192.168.1.1]",resultIPv4compatible.getHost()); + } + @Test public void path() throws URISyntaxException { UriComponentsBuilder builder = UriComponentsBuilder.fromPath("/foo/bar"); From 92795f463a2cebeb4159ecff53e57b038ced016c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 25 Sep 2013 12:47:50 -0400 Subject: [PATCH 2/2] Polish Issue: SPR-10539 --- .../web/util/HierarchicalUriComponents.java | 14 ++++---- .../web/util/UriComponentsBuilder.java | 4 +-- .../springframework/web/util/UriUtils.java | 2 +- .../web/util/UriComponentsBuilderTests.java | 9 +++--- .../web/util/UriComponentsTests.java | 32 ++++++++++++++----- 5 files changed, 37 insertions(+), 24 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index 1bddd7a4e6..25c5781387 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -182,12 +182,7 @@ final class HierarchicalUriComponents extends UriComponents { } String encodedScheme = encodeUriComponent(this.getScheme(), encoding, Type.SCHEME); String encodedUserInfo = encodeUriComponent(this.userInfo, encoding, Type.USER_INFO); - String encodedHost; - if(StringUtils.hasLength(this.host) && this.host.startsWith("[")) { - encodedHost = encodeUriComponent(this.host, encoding, Type.HOST_IPV6); - } else { - encodedHost = encodeUriComponent(this.host, encoding, Type.HOST); - } + String encodedHost = encodeUriComponent(this.host, encoding, getHostType()); PathComponent encodedPath = this.path.encode(encoding); MultiValueMap encodedQueryParams = @@ -246,6 +241,9 @@ final class HierarchicalUriComponents extends UriComponents { return bos.toByteArray(); } + private Type getHostType() { + return ((this.host != null) && this.host.startsWith("[")) ? Type.HOST_IPV6 : Type.HOST_IPV4; + } // verifying @@ -260,7 +258,7 @@ final class HierarchicalUriComponents extends UriComponents { } verifyUriComponent(getScheme(), Type.SCHEME); verifyUriComponent(userInfo, Type.USER_INFO); - verifyUriComponent(host, Type.HOST); + verifyUriComponent(host, getHostType()); this.path.verify(); for (Map.Entry> entry : queryParams.entrySet()) { verifyUriComponent(entry.getKey(), Type.QUERY_PARAM); @@ -468,7 +466,7 @@ final class HierarchicalUriComponents extends UriComponents { return isUnreserved(c) || isSubDelimiter(c) || ':' == c; } }, - HOST { + HOST_IPV4 { @Override public boolean isAllowed(int c) { return isUnreserved(c) || isSubDelimiter(c); 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 f9d20ebf1b..fa5e7aeba0 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 @@ -64,11 +64,11 @@ public class UriComponentsBuilder { private static final String USERINFO_PATTERN = "([^@/]*)"; - private static final String HOST_IPv4_PATTERN = "[^\\[/?#:]*"; + private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*"; private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}\\:\\.]*[%\\p{Alnum}]*\\]"; - private static final String HOST_PATTERN = "("+HOST_IPV6_PATTERN + "|" + HOST_IPv4_PATTERN + ")"; + private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; private static final String PORT_PATTERN = "(\\d*)"; diff --git a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java index 3a3088e7e6..6abdb392f4 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java @@ -254,7 +254,7 @@ public abstract class UriUtils { */ public static String encodeHost(String host, String encoding) throws UnsupportedEncodingException { return HierarchicalUriComponents - .encodeUriComponent(host, encoding, HierarchicalUriComponents.Type.HOST); + .encodeUriComponent(host, encoding, HierarchicalUriComponents.Type.HOST_IPV4); } /** 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 a62e593b21..2fb397a1aa 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 @@ -173,8 +173,7 @@ public class UriComponentsBuilderTests { @Test(expected = IllegalArgumentException.class) public void fromHttpUrlStringInvalidIPv6Host() throws URISyntaxException { - UriComponents result = UriComponentsBuilder - .fromHttpUrl("http://[1abc:2abc:3abc::5ABC:6abc:8080/resource").build().encode(); + UriComponentsBuilder.fromHttpUrl("http://[1abc:2abc:3abc::5ABC:6abc:8080/resource").build().encode(); } // SPR-10539 @@ -183,15 +182,15 @@ public class UriComponentsBuilderTests { public void fromUriStringIPv6Host() throws URISyntaxException { UriComponents result = UriComponentsBuilder .fromUriString("http://[1abc:2abc:3abc::5ABC:6abc]:8080/resource").build().encode(); - assertEquals("[1abc:2abc:3abc::5ABC:6abc]",result.getHost()); + assertEquals("[1abc:2abc:3abc::5ABC:6abc]", result.getHost()); UriComponents resultWithScopeId = UriComponentsBuilder .fromUriString("http://[1abc:2abc:3abc::5ABC:6abc%eth0]:8080/resource").build().encode(); - assertEquals("[1abc:2abc:3abc::5ABC:6abc%25eth0]",resultWithScopeId.getHost()); + assertEquals("[1abc:2abc:3abc::5ABC:6abc%25eth0]", resultWithScopeId.getHost()); UriComponents resultIPv4compatible = UriComponentsBuilder .fromUriString("http://[::192.168.1.1]:8080/resource").build().encode(); - assertEquals("[::192.168.1.1]",resultIPv4compatible.getHost()); + assertEquals("[::192.168.1.1]", resultIPv4compatible.getHost()); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java index 2013e6df65..458a08a593 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java @@ -25,9 +25,7 @@ import java.net.URISyntaxException; import org.junit.Test; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; /** @@ -45,20 +43,38 @@ public class UriComponentsTests { @Test public void toUriEncoded() throws URISyntaxException { - UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://example.com/hotel list/Z\u00fcrich").build(); - UriComponents encoded = uriComponents.encode(); - assertEquals(new URI("http://example.com/hotel%20list/Z%C3%BCrich"), encoded.toUri()); + UriComponents uriComponents = UriComponentsBuilder.fromUriString( + "http://example.com/hotel list/Z\u00fcrich").build(); + assertEquals(new URI("http://example.com/hotel%20list/Z%C3%BCrich"), uriComponents.encode().toUri()); } @Test public void toUriNotEncoded() throws URISyntaxException { - UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://example.com/hotel list/Z\u00fcrich").build(); + UriComponents uriComponents = UriComponentsBuilder.fromUriString( + "http://example.com/hotel list/Z\u00fcrich").build(); assertEquals(new URI("http://example.com/hotel%20list/Z\u00fcrich"), uriComponents.toUri()); } + @Test + public void toUriAlreadyEncoded() throws URISyntaxException { + UriComponents uriComponents = UriComponentsBuilder.fromUriString( + "http://example.com/hotel%20list/Z%C3%BCrich").build(true); + UriComponents encoded = uriComponents.encode(); + assertEquals(new URI("http://example.com/hotel%20list/Z%C3%BCrich"), encoded.toUri()); + } + + @Test + public void toUriWithIpv6HostAlreadyEncoded() throws URISyntaxException { + UriComponents uriComponents = UriComponentsBuilder.fromUriString( + "http://[1abc:2abc:3abc::5ABC:6abc]:8080/hotel%20list/Z%C3%BCrich").build(true); + UriComponents encoded = uriComponents.encode(); + assertEquals(new URI("http://[1abc:2abc:3abc::5ABC:6abc]:8080/hotel%20list/Z%C3%BCrich"), encoded.toUri()); + } + @Test public void expand() { - UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://example.com").path("/{foo} {bar}").build(); + UriComponents uriComponents = UriComponentsBuilder.fromUriString( + "http://example.com").path("/{foo} {bar}").build(); uriComponents = uriComponents.expand("1 2", "3 4"); assertEquals("/1 2 3 4", uriComponents.getPath()); assertEquals("http://example.com/1 2 3 4", uriComponents.toUriString());