From a8949b129df558bc90672606bcbcd3cf6d9d4912 Mon Sep 17 00:00:00 2001 From: jwilson Date: Sat, 1 Aug 2015 21:04:54 -0700 Subject: [PATCH] IPv6 canonical string. Closes https://github.com/square/okhttp/issues/1636 --- .../java/com/squareup/okhttp/HttpUrlTest.java | 58 +++++++++++++------ .../squareup/okhttp/WebPlatformUrlTest.java | 4 +- .../java/com/squareup/okhttp/HttpUrl.java | 38 +++++++++++- 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java index c2184a282..2386aba1b 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java @@ -269,27 +269,26 @@ public final class HttpUrlTest { @Test public void hostIpv6() throws Exception { // Square braces are absent from host()... - String address = "0:0:0:0:0:0:0:1"; - assertEquals(address, HttpUrl.parse("http://[::1]/").host()); + assertEquals("::1", HttpUrl.parse("http://[::1]/").host()); // ... but they're included in toString(). - assertEquals("http://[0:0:0:0:0:0:0:1]/", HttpUrl.parse("http://[::1]/").toString()); + assertEquals("http://[::1]/", HttpUrl.parse("http://[::1]/").toString()); // IPv6 colons don't interfere with port numbers or passwords. assertEquals(8080, HttpUrl.parse("http://[::1]:8080/").port()); assertEquals("password", HttpUrl.parse("http://user:password@[::1]/").password()); - assertEquals(address, HttpUrl.parse("http://user:password@[::1]:8080/").host()); + assertEquals("::1", HttpUrl.parse("http://user:password@[::1]:8080/").host()); // Permit the contents of IPv6 addresses to be percent-encoded... - assertEquals(address, HttpUrl.parse("http://[%3A%3A%31]/").host()); + assertEquals("::1", HttpUrl.parse("http://[%3A%3A%31]/").host()); // Including the Square braces themselves! (This is what Chrome does.) - assertEquals(address, HttpUrl.parse("http://%5B%3A%3A1%5D/").host()); + assertEquals("::1", HttpUrl.parse("http://%5B%3A%3A1%5D/").host()); } @Test public void hostIpv6AddressDifferentFormats() throws Exception { // Multiple representations of the same address; see http://tools.ietf.org/html/rfc5952. - String a3 = "2001:db8:0:0:1:0:0:1"; + String a3 = "2001:db8::1:0:0:1"; assertEquals(a3, HttpUrl.parse("http://[2001:db8:0:0:1:0:0:1]").host()); assertEquals(a3, HttpUrl.parse("http://[2001:0db8:0:0:1:0:0:1]").host()); assertEquals(a3, HttpUrl.parse("http://[2001:db8::1:0:0:1]").host()); @@ -301,19 +300,17 @@ public final class HttpUrlTest { } @Test public void hostIpv6AddressLeadingCompression() throws Exception { - String a1 = "0:0:0:0:0:0:0:1"; - assertEquals(a1, HttpUrl.parse("http://[::0001]").host()); - assertEquals(a1, HttpUrl.parse("http://[0000::0001]").host()); - assertEquals(a1, HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000:0000:0001]").host()); - assertEquals(a1, HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000::0001]").host()); + assertEquals("::1", HttpUrl.parse("http://[::0001]").host()); + assertEquals("::1", HttpUrl.parse("http://[0000::0001]").host()); + assertEquals("::1", HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000:0000:0001]").host()); + assertEquals("::1", HttpUrl.parse("http://[0000:0000:0000:0000:0000:0000::0001]").host()); } @Test public void hostIpv6AddressTrailingCompression() throws Exception { - String a2 = "1:0:0:0:0:0:0:0"; - assertEquals(a2, HttpUrl.parse("http://[0001:0000::]").host()); - assertEquals(a2, HttpUrl.parse("http://[0001::0000]").host()); - assertEquals(a2, HttpUrl.parse("http://[0001::]").host()); - assertEquals(a2, HttpUrl.parse("http://[1::]").host()); + assertEquals("1::", HttpUrl.parse("http://[0001:0000::]").host()); + assertEquals("1::", HttpUrl.parse("http://[0001::0000]").host()); + assertEquals("1::", HttpUrl.parse("http://[0001::]").host()); + assertEquals("1::", HttpUrl.parse("http://[1::]").host()); } @Test public void hostIpv6AddressTooManyDigitsInGroup() throws Exception { @@ -351,8 +348,8 @@ public final class HttpUrlTest { } @Test public void hostIpv6WithIpv4Suffix() throws Exception { - assertEquals("0:0:0:0:0:1:ffff:ffff", HttpUrl.parse("http://[::1:255.255.255.255]/").host()); - assertEquals("0:0:0:0:0:1:0:0", HttpUrl.parse("http://[0:0:0:0:0:1:0.0.0.0]/").host()); + assertEquals("::1:ffff:ffff", HttpUrl.parse("http://[::1:255.255.255.255]/").host()); + assertEquals("::1:0:0", HttpUrl.parse("http://[0:0:0:0:0:1:0.0.0.0]/").host()); } @Test public void hostIpv6WithIpv4SuffixWithOctalPrefix() throws Exception { @@ -389,6 +386,29 @@ public final class HttpUrlTest { assertEquals(null, HttpUrl.parse("http://[0:0:0:0:0:1:255.255.255]/")); } + @Test public void hostIpv6CanonicalForm() throws Exception { + assertEquals("abcd:ef01:2345:6789:abcd:ef01:2345:6789", + HttpUrl.parse("http://[abcd:ef01:2345:6789:abcd:ef01:2345:6789]/").host()); + assertEquals("a::b:0:0:0", HttpUrl.parse("http://[a:0:0:0:b:0:0:0]/").host()); + assertEquals("a:b:0:0:c::", HttpUrl.parse("http://[a:b:0:0:c:0:0:0]/").host()); + assertEquals("a:b::c:0:0", HttpUrl.parse("http://[a:b:0:0:0:c:0:0]/").host()); + assertEquals("a::b:0:0:0", HttpUrl.parse("http://[a:0:0:0:b:0:0:0]/").host()); + assertEquals("::a:b:0:0:0", HttpUrl.parse("http://[0:0:0:a:b:0:0:0]/").host()); + assertEquals("::a:0:0:0:b", HttpUrl.parse("http://[0:0:0:a:0:0:0:b]/").host()); + assertEquals("::a:b:c:d:e:f:1", HttpUrl.parse("http://[0:a:b:c:d:e:f:1]/").host()); + assertEquals("a:b:c:d:e:f:1::", HttpUrl.parse("http://[a:b:c:d:e:f:1:0]/").host()); + assertEquals("ff01::101", HttpUrl.parse("http://[FF01:0:0:0:0:0:0:101]/").host()); + assertEquals("1::", HttpUrl.parse("http://[1:0:0:0:0:0:0:0]/").host()); + assertEquals("::1", HttpUrl.parse("http://[0:0:0:0:0:0:0:1]/").host()); + assertEquals("::", HttpUrl.parse("http://[0:0:0:0:0:0:0:0]/").host()); + } + + @Test public void hostIpv4CanonicalForm() throws Exception { + assertEquals("255.255.255.255", HttpUrl.parse("http://255.255.255.255/").host()); + assertEquals("1.2.3.4", HttpUrl.parse("http://1.2.3.4/").host()); + assertEquals("0.0.0.0", HttpUrl.parse("http://0.0.0.0/").host()); + } + @Ignore("java.net.IDN strips trailing trailing dots on Java 7, but not on Java 8.") @Test public void hostWithTrailingDot() throws Exception { assertEquals("host.", HttpUrl.parse("http://host./").host()); diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java index 2260e8ab8..e45761ce5 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java @@ -64,9 +64,7 @@ public final class WebPlatformUrlTest { "Parsing: against ", "Parsing: against ", "Parsing: against ", - "Parsing: against ", - "Parsing: against ", - "Parsing: against " + "Parsing: against " ); /** Test how {@link HttpUrl} does against the web platform test suite. */ diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java index 2440ea8b9..d56ea8253 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java +++ b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java @@ -1189,7 +1189,10 @@ public final class HttpUrl { // If the input is encased in square braces "[...]", drop 'em. We have an IPv6 address. if (percentDecoded.startsWith("[") && percentDecoded.endsWith("]")) { InetAddress inetAddress = decodeIpv6(percentDecoded, 1, percentDecoded.length() - 1); - return inetAddress != null ? inetAddress.getHostAddress() : null; + if (inetAddress == null) return null; + byte[] address = inetAddress.getAddress(); + if (address.length == 16) return inet6AddressToAscii(address); + throw new AssertionError(); } // Do IDN decoding. This converts {@code ☃.net} to {@code xn--n3h.net}. @@ -1322,6 +1325,39 @@ public final class HttpUrl { } } + private static String inet6AddressToAscii(byte[] address) { + // Go through the address looking for the longest run of 0s. Each group is 2-bytes. + int longestRunOffset = -1; + int longestRunLength = 0; + for (int i = 0; i < address.length; i += 2) { + int currentRunOffset = i; + while (i < 16 && address[i] == 0 && address[i + 1] == 0) { + i += 2; + } + int currentRunLength = i - currentRunOffset; + if (currentRunLength > longestRunLength) { + longestRunOffset = currentRunOffset; + longestRunLength = currentRunLength; + } + } + + // Emit each 2-byte group in hex, separated by ':'. The longest run of zeroes is "::". + Buffer result = new Buffer(); + for (int i = 0; i < address.length; ) { + if (i == longestRunOffset) { + result.writeByte(':'); + i += longestRunLength; + if (i == 16) result.writeByte(':'); + } else { + if (i > 0) result.writeByte(':'); + int group = (address[i] & 0xff) << 8 | address[i + 1] & 0xff; + result.writeHexadecimalUnsignedLong(group); + i += 2; + } + } + return result.readUtf8(); + } + private static int parsePort(String input, int pos, int limit) { try { // Canonicalize the port string to skip '\n' etc.