Skip to content

Commit a73c217

Browse files
authored
Fix IPv6 handling for cookies (square#3564)
1 parent e5f50b5 commit a73c217

File tree

5 files changed

+217
-177
lines changed

5 files changed

+217
-177
lines changed

okhttp-tests/src/test/java/okhttp3/CookieTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,35 @@ public final class CookieTest {
267267
assertEquals("123.45.234.56", Cookie.parse(urlWithIp, "a=b; domain=123.45.234.56").domain());
268268
}
269269

270+
@Test public void domainMatchesIpv6Address() throws Exception {
271+
Cookie cookie = Cookie.parse(HttpUrl.parse("http://[::1]/"), "a=b; domain=::1");
272+
assertEquals("::1", cookie.domain());
273+
assertTrue(cookie.matches(HttpUrl.parse("http://[::1]/")));
274+
}
275+
276+
@Test public void domainMatchesIpv6AddressWithCompression() throws Exception {
277+
Cookie cookie = Cookie.parse(HttpUrl.parse("http://[0001:0000::]/"), "a=b; domain=0001:0000::");
278+
assertEquals("1::", cookie.domain());
279+
assertTrue(cookie.matches(HttpUrl.parse("http://[1::]/")));
280+
}
281+
282+
@Test public void domainMatchesIpv6AddressWithIpv4Suffix() throws Exception {
283+
Cookie cookie = Cookie.parse(
284+
HttpUrl.parse("http://[::1:ffff:ffff]/"), "a=b; domain=::1:255.255.255.255");
285+
assertEquals("::1:ffff:ffff", cookie.domain());
286+
assertTrue(cookie.matches(HttpUrl.parse("http://[::1:ffff:ffff]/")));
287+
}
288+
289+
@Test public void ipv6AddressDoesntMatch() throws Exception {
290+
Cookie cookie = Cookie.parse(HttpUrl.parse("http://[::1]/"), "a=b; domain=::2");
291+
assertNull(cookie);
292+
}
293+
294+
@Test public void ipv6AddressMalformed() throws Exception {
295+
Cookie cookie = Cookie.parse(HttpUrl.parse("http://[::1]/"), "a=b; domain=::2::2");
296+
assertEquals("::1", cookie.domain());
297+
}
298+
270299
/**
271300
* These public suffixes were selected by inspecting the publicsuffix.org list. It's possible they
272301
* may change in the future. If this test begins to fail, please double check they are still
@@ -507,6 +536,15 @@ public final class CookieTest {
507536
assertEquals(true, cookie.httpOnly());
508537
}
509538

539+
@Test public void builderIpv6() throws Exception {
540+
Cookie cookie = new Cookie.Builder()
541+
.name("a")
542+
.value("b")
543+
.domain("0:0:0:0:0:0:0:1")
544+
.build();
545+
assertEquals("::1", cookie.domain());
546+
}
547+
510548
@Test public void equalsAndHashCode() throws Exception {
511549
List<String> cookieStrings = Arrays.asList(
512550
"a=b; Path=/c; Domain=example.com; Max-Age=5; Secure; HttpOnly",

okhttp-tests/src/test/java/okhttp3/internal/publicsuffix/PublicSuffixDatabaseTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import java.io.IOException;
1919
import java.io.InputStream;
20-
import java.io.InterruptedIOException;
2120
import okhttp3.internal.Util;
2221
import okio.Buffer;
2322
import okio.BufferedSource;
@@ -272,14 +271,14 @@ private void checkPublicSuffix(String domain, String registrablePart) {
272271
return;
273272
}
274273

275-
String canonicalDomain = Util.domainToAscii(domain);
274+
String canonicalDomain = Util.canonicalizeHost(domain);
276275
if (canonicalDomain == null) return;
277276

278277
String result = publicSuffixDatabase.getEffectiveTldPlusOne(canonicalDomain);
279278
if (registrablePart == null) {
280279
assertNull(result);
281280
} else {
282-
assertEquals(Util.domainToAscii(registrablePart), result);
281+
assertEquals(Util.canonicalizeHost(registrablePart), result);
283282
}
284283
}
285284
}

okhttp/src/main/java/okhttp3/Cookie.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
import okhttp3.internal.publicsuffix.PublicSuffixDatabase;
3131

3232
import static okhttp3.internal.Util.UTC;
33+
import static okhttp3.internal.Util.canonicalizeHost;
3334
import static okhttp3.internal.Util.delimiterOffset;
34-
import static okhttp3.internal.Util.domainToAscii;
3535
import static okhttp3.internal.Util.indexOfControlOrNonAscii;
3636
import static okhttp3.internal.Util.trimSubstring;
3737
import static okhttp3.internal.Util.verifyAsIpAddress;
@@ -429,7 +429,7 @@ private static String parseDomain(String s) {
429429
if (s.startsWith(".")) {
430430
s = s.substring(1);
431431
}
432-
String canonicalDomain = domainToAscii(s);
432+
String canonicalDomain = canonicalizeHost(s);
433433
if (canonicalDomain == null) {
434434
throw new IllegalArgumentException();
435435
}
@@ -508,7 +508,7 @@ public Builder hostOnlyDomain(String domain) {
508508

509509
private Builder domain(String domain, boolean hostOnly) {
510510
if (domain == null) throw new NullPointerException("domain == null");
511-
String canonicalDomain = Util.domainToAscii(domain);
511+
String canonicalDomain = Util.canonicalizeHost(domain);
512512
if (canonicalDomain == null) {
513513
throw new IllegalArgumentException("unexpected domain: " + domain);
514514
}

okhttp/src/main/java/okhttp3/HttpUrl.java

Lines changed: 2 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.net.UnknownHostException;
2424
import java.nio.charset.Charset;
2525
import java.util.ArrayList;
26-
import java.util.Arrays;
2726
import java.util.Collections;
2827
import java.util.LinkedHashSet;
2928
import java.util.List;
@@ -33,8 +32,8 @@
3332
import okhttp3.internal.publicsuffix.PublicSuffixDatabase;
3433
import okio.Buffer;
3534

35+
import static okhttp3.internal.Util.decodeHexDigit;
3636
import static okhttp3.internal.Util.delimiterOffset;
37-
import static okhttp3.internal.Util.domainToAscii;
3837
import static okhttp3.internal.Util.skipLeadingAsciiWhitespace;
3938
import static okhttp3.internal.Util.skipTrailingAsciiWhitespace;
4039
import static okhttp3.internal.Util.verifyAsIpAddress;
@@ -1589,162 +1588,7 @@ private static String canonicalizeHost(String input, int pos, int limit) {
15891588
// Start by percent decoding the host. The WHATWG spec suggests doing this only after we've
15901589
// checked for IPv6 square braces. But Chrome does it first, and that's more lenient.
15911590
String percentDecoded = percentDecode(input, pos, limit, false);
1592-
1593-
// If the input contains a :, it’s an IPv6 address.
1594-
if (percentDecoded.contains(":")) {
1595-
// If the input is encased in square braces "[...]", drop 'em.
1596-
InetAddress inetAddress = percentDecoded.startsWith("[") && percentDecoded.endsWith("]")
1597-
? decodeIpv6(percentDecoded, 1, percentDecoded.length() - 1)
1598-
: decodeIpv6(percentDecoded, 0, percentDecoded.length());
1599-
if (inetAddress == null) return null;
1600-
byte[] address = inetAddress.getAddress();
1601-
if (address.length == 16) return inet6AddressToAscii(address);
1602-
throw new AssertionError("Invalid IPv6 address: '" + percentDecoded + "'");
1603-
}
1604-
1605-
return domainToAscii(percentDecoded);
1606-
}
1607-
1608-
/** Decodes an IPv6 address like 1111:2222:3333:4444:5555:6666:7777:8888 or ::1. */
1609-
private static @Nullable InetAddress decodeIpv6(String input, int pos, int limit) {
1610-
byte[] address = new byte[16];
1611-
int b = 0;
1612-
int compress = -1;
1613-
int groupOffset = -1;
1614-
1615-
for (int i = pos; i < limit; ) {
1616-
if (b == address.length) return null; // Too many groups.
1617-
1618-
// Read a delimiter.
1619-
if (i + 2 <= limit && input.regionMatches(i, "::", 0, 2)) {
1620-
// Compression "::" delimiter, which is anywhere in the input, including its prefix.
1621-
if (compress != -1) return null; // Multiple "::" delimiters.
1622-
i += 2;
1623-
b += 2;
1624-
compress = b;
1625-
if (i == limit) break;
1626-
} else if (b != 0) {
1627-
// Group separator ":" delimiter.
1628-
if (input.regionMatches(i, ":", 0, 1)) {
1629-
i++;
1630-
} else if (input.regionMatches(i, ".", 0, 1)) {
1631-
// If we see a '.', rewind to the beginning of the previous group and parse as IPv4.
1632-
if (!decodeIpv4Suffix(input, groupOffset, limit, address, b - 2)) return null;
1633-
b += 2; // We rewound two bytes and then added four.
1634-
break;
1635-
} else {
1636-
return null; // Wrong delimiter.
1637-
}
1638-
}
1639-
1640-
// Read a group, one to four hex digits.
1641-
int value = 0;
1642-
groupOffset = i;
1643-
for (; i < limit; i++) {
1644-
char c = input.charAt(i);
1645-
int hexDigit = decodeHexDigit(c);
1646-
if (hexDigit == -1) break;
1647-
value = (value << 4) + hexDigit;
1648-
}
1649-
int groupLength = i - groupOffset;
1650-
if (groupLength == 0 || groupLength > 4) return null; // Group is the wrong size.
1651-
1652-
// We've successfully read a group. Assign its value to our byte array.
1653-
address[b++] = (byte) ((value >>> 8) & 0xff);
1654-
address[b++] = (byte) (value & 0xff);
1655-
}
1656-
1657-
// All done. If compression happened, we need to move bytes to the right place in the
1658-
// address. Here's a sample:
1659-
//
1660-
// input: "1111:2222:3333::7777:8888"
1661-
// before: { 11, 11, 22, 22, 33, 33, 00, 00, 77, 77, 88, 88, 00, 00, 00, 00 }
1662-
// compress: 6
1663-
// b: 10
1664-
// after: { 11, 11, 22, 22, 33, 33, 00, 00, 00, 00, 00, 00, 77, 77, 88, 88 }
1665-
//
1666-
if (b != address.length) {
1667-
if (compress == -1) return null; // Address didn't have compression or enough groups.
1668-
System.arraycopy(address, compress, address, address.length - (b - compress), b - compress);
1669-
Arrays.fill(address, compress, compress + (address.length - b), (byte) 0);
1670-
}
1671-
1672-
try {
1673-
return InetAddress.getByAddress(address);
1674-
} catch (UnknownHostException e) {
1675-
throw new AssertionError();
1676-
}
1677-
}
1678-
1679-
/** Decodes an IPv4 address suffix of an IPv6 address, like 1111::5555:6666:192.168.0.1. */
1680-
private static boolean decodeIpv4Suffix(
1681-
String input, int pos, int limit, byte[] address, int addressOffset) {
1682-
int b = addressOffset;
1683-
1684-
for (int i = pos; i < limit; ) {
1685-
if (b == address.length) return false; // Too many groups.
1686-
1687-
// Read a delimiter.
1688-
if (b != addressOffset) {
1689-
if (input.charAt(i) != '.') return false; // Wrong delimiter.
1690-
i++;
1691-
}
1692-
1693-
// Read 1 or more decimal digits for a value in 0..255.
1694-
int value = 0;
1695-
int groupOffset = i;
1696-
for (; i < limit; i++) {
1697-
char c = input.charAt(i);
1698-
if (c < '0' || c > '9') break;
1699-
if (value == 0 && groupOffset != i) return false; // Reject unnecessary leading '0's.
1700-
value = (value * 10) + c - '0';
1701-
if (value > 255) return false; // Value out of range.
1702-
}
1703-
int groupLength = i - groupOffset;
1704-
if (groupLength == 0) return false; // No digits.
1705-
1706-
// We've successfully read a byte.
1707-
address[b++] = (byte) value;
1708-
}
1709-
1710-
if (b != addressOffset + 4) return false; // Too few groups. We wanted exactly four.
1711-
return true; // Success.
1712-
}
1713-
1714-
/** Encodes an IPv6 address in canonical form according to RFC 5952. */
1715-
private static String inet6AddressToAscii(byte[] address) {
1716-
// Go through the address looking for the longest run of 0s. Each group is 2-bytes.
1717-
// A run must be longer than one group (section 4.2.2).
1718-
// If there are multiple equal runs, the first one must be used (section 4.2.3).
1719-
int longestRunOffset = -1;
1720-
int longestRunLength = 0;
1721-
for (int i = 0; i < address.length; i += 2) {
1722-
int currentRunOffset = i;
1723-
while (i < 16 && address[i] == 0 && address[i + 1] == 0) {
1724-
i += 2;
1725-
}
1726-
int currentRunLength = i - currentRunOffset;
1727-
if (currentRunLength > longestRunLength && currentRunLength >= 4) {
1728-
longestRunOffset = currentRunOffset;
1729-
longestRunLength = currentRunLength;
1730-
}
1731-
}
1732-
1733-
// Emit each 2-byte group in hex, separated by ':'. The longest run of zeroes is "::".
1734-
Buffer result = new Buffer();
1735-
for (int i = 0; i < address.length; ) {
1736-
if (i == longestRunOffset) {
1737-
result.writeByte(':');
1738-
i += longestRunLength;
1739-
if (i == 16) result.writeByte(':');
1740-
} else {
1741-
if (i > 0) result.writeByte(':');
1742-
int group = (address[i] & 0xff) << 8 | address[i + 1] & 0xff;
1743-
result.writeHexadecimalUnsignedLong(group);
1744-
i += 2;
1745-
}
1746-
}
1747-
return result.readUtf8();
1591+
return Util.canonicalizeHost(percentDecoded);
17481592
}
17491593

17501594
private static int parsePort(String input, int pos, int limit) {
@@ -1817,13 +1661,6 @@ && decodeHexDigit(encoded.charAt(pos + 1)) != -1
18171661
&& decodeHexDigit(encoded.charAt(pos + 2)) != -1;
18181662
}
18191663

1820-
static int decodeHexDigit(char c) {
1821-
if (c >= '0' && c <= '9') return c - '0';
1822-
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
1823-
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
1824-
return -1;
1825-
}
1826-
18271664
/**
18281665
* Returns a substring of {@code input} on the range {@code [pos..limit)} with the following
18291666
* transformations:

0 commit comments

Comments
 (0)