mirror of
https://github.com/square/okhttp.git
synced 2026-01-17 08:42:25 +03:00
Finish implementing RFC 6265 cookies.
This commit is contained in:
@@ -19,7 +19,6 @@ import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import okhttp3.internal.Util;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -27,8 +26,8 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CookieTest {
|
||||
HttpUrl url = HttpUrl.parse("http://example.com/");
|
||||
public final class CookieTest {
|
||||
HttpUrl url = HttpUrl.parse("https://example.com/");
|
||||
|
||||
@Test public void test() throws Exception {
|
||||
Cookie cookie = Cookie.parse(url, "SID=31d4d96e407aad42");
|
||||
@@ -179,7 +178,6 @@ public class CookieTest {
|
||||
Cookie.parse(url, "a=b; Expires=Thu, 01 Jan 1970 00:00:60 GMT").expiresAt());
|
||||
}
|
||||
|
||||
@Ignore("cookie matching isn't implemented")
|
||||
@Test public void domainMatches() throws Exception {
|
||||
Cookie cookie = Cookie.parse(url, "a=b; domain=example.com");
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://example.com")));
|
||||
@@ -188,7 +186,6 @@ public class CookieTest {
|
||||
}
|
||||
|
||||
/** If no domain is present, match only the origin domain. */
|
||||
@Ignore("cookie matching isn't implemented")
|
||||
@Test public void domainMatchesNoDomain() throws Exception {
|
||||
Cookie cookie = Cookie.parse(url, "a=b");
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://example.com")));
|
||||
@@ -197,7 +194,6 @@ public class CookieTest {
|
||||
}
|
||||
|
||||
/** Ignore an optional leading `.` in the domain. */
|
||||
@Ignore("cookie matching isn't implemented")
|
||||
@Test public void domainMatchesIgnoresLeadingDot() throws Exception {
|
||||
Cookie cookie = Cookie.parse(url, "a=b; domain=.example.com");
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://example.com")));
|
||||
@@ -206,7 +202,6 @@ public class CookieTest {
|
||||
}
|
||||
|
||||
/** Ignore the entire attribute if the domain ends with `.`. */
|
||||
@Ignore("cookie matching isn't implemented")
|
||||
@Test public void domainIgnoredWithTrailingDot() throws Exception {
|
||||
Cookie cookie = Cookie.parse(url, "a=b; domain=example.com.");
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://example.com")));
|
||||
@@ -214,6 +209,64 @@ public class CookieTest {
|
||||
assertFalse(cookie.matches(HttpUrl.parse("http://square.com")));
|
||||
}
|
||||
|
||||
@Test public void idnDomainMatches() throws Exception {
|
||||
Cookie cookie = Cookie.parse(HttpUrl.parse("http://☃.net/"), "a=b; domain=☃.net");
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://☃.net/")));
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://xn--n3h.net/")));
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://www.☃.net/")));
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://www.xn--n3h.net/")));
|
||||
}
|
||||
|
||||
@Test public void punycodeDomainMatches() throws Exception {
|
||||
Cookie cookie = Cookie.parse(HttpUrl.parse("http://xn--n3h.net/"), "a=b; domain=xn--n3h.net");
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://☃.net/")));
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://xn--n3h.net/")));
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://www.☃.net/")));
|
||||
assertTrue(cookie.matches(HttpUrl.parse("http://www.xn--n3h.net/")));
|
||||
}
|
||||
|
||||
@Test public void domainMatchesIpAddress() throws Exception {
|
||||
HttpUrl urlWithIp = HttpUrl.parse("http://123.45.234.56/");
|
||||
assertNull(Cookie.parse(urlWithIp, "a=b; domain=234.56"));
|
||||
assertEquals("123.45.234.56", Cookie.parse(urlWithIp, "a=b; domain=123.45.234.56").domain());
|
||||
}
|
||||
|
||||
@Test public void hostOnly() throws Exception {
|
||||
assertTrue(Cookie.parse(url, "a=b").hostOnly());
|
||||
assertFalse(Cookie.parse(url, "a=b; domain=example.com").hostOnly());
|
||||
}
|
||||
|
||||
@Test public void defaultPath() throws Exception {
|
||||
assertEquals("/foo", Cookie.parse(HttpUrl.parse("http://example.com/foo/bar"), "a=b").path());
|
||||
assertEquals("/foo", Cookie.parse(HttpUrl.parse("http://example.com/foo/"), "a=b").path());
|
||||
assertEquals("/", Cookie.parse(HttpUrl.parse("http://example.com/foo"), "a=b").path());
|
||||
assertEquals("/", Cookie.parse(HttpUrl.parse("http://example.com/"), "a=b").path());
|
||||
}
|
||||
|
||||
@Test public void defaultPathIsUsedIfPathDoesntHaveLeadingSlash() throws Exception {
|
||||
assertEquals("/foo", Cookie.parse(HttpUrl.parse("http://example.com/foo/bar"),
|
||||
"a=b; path=quux").path());
|
||||
assertEquals("/foo", Cookie.parse(HttpUrl.parse("http://example.com/foo/bar"),
|
||||
"a=b; path=").path());
|
||||
}
|
||||
|
||||
@Test public void pathAttributeDoesntNeedToMatch() throws Exception {
|
||||
assertEquals("/quux", Cookie.parse(HttpUrl.parse("http://example.com/"),
|
||||
"a=b; path=/quux").path());
|
||||
assertEquals("/quux", Cookie.parse(HttpUrl.parse("http://example.com/foo/bar"),
|
||||
"a=b; path=/quux").path());
|
||||
}
|
||||
|
||||
@Test public void httpOnly() throws Exception {
|
||||
assertFalse(Cookie.parse(url, "a=b").httpOnly());
|
||||
assertTrue(Cookie.parse(url, "a=b; HttpOnly").httpOnly());
|
||||
}
|
||||
|
||||
@Test public void secure() throws Exception {
|
||||
assertFalse(Cookie.parse(url, "a=b").secure());
|
||||
assertTrue(Cookie.parse(url, "a=b; Secure").secure());
|
||||
}
|
||||
|
||||
@Test public void maxAgeTakesPrecedenceOverExpires() throws Exception {
|
||||
// Max-Age = 1, Expires = 2. In either order.
|
||||
assertEquals(1000L, Cookie.parse(
|
||||
|
||||
@@ -517,28 +517,28 @@ public final class HostnameVerifierTest {
|
||||
|
||||
@Test public void verifyAsIpAddress() {
|
||||
// IPv4
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("127.0.0.1"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("1.2.3.4"));
|
||||
assertTrue(Util.verifyAsIpAddress("127.0.0.1"));
|
||||
assertTrue(Util.verifyAsIpAddress("1.2.3.4"));
|
||||
|
||||
// IPv6
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("::1"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("2001:db8::1"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("::192.168.0.1"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("::ffff:192.168.0.1"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("1080:0:0:0:8:800:200C:417A"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("1080::8:800:200C:417A"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("FF01::101"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("0:0:0:0:0:0:13.1.68.3"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("0:0:0:0:0:FFFF:129.144.52.38"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("::13.1.68.3"));
|
||||
assertTrue(OkHostnameVerifier.verifyAsIpAddress("::FFFF:129.144.52.38"));
|
||||
assertTrue(Util.verifyAsIpAddress("::1"));
|
||||
assertTrue(Util.verifyAsIpAddress("2001:db8::1"));
|
||||
assertTrue(Util.verifyAsIpAddress("::192.168.0.1"));
|
||||
assertTrue(Util.verifyAsIpAddress("::ffff:192.168.0.1"));
|
||||
assertTrue(Util.verifyAsIpAddress("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210"));
|
||||
assertTrue(Util.verifyAsIpAddress("1080:0:0:0:8:800:200C:417A"));
|
||||
assertTrue(Util.verifyAsIpAddress("1080::8:800:200C:417A"));
|
||||
assertTrue(Util.verifyAsIpAddress("FF01::101"));
|
||||
assertTrue(Util.verifyAsIpAddress("0:0:0:0:0:0:13.1.68.3"));
|
||||
assertTrue(Util.verifyAsIpAddress("0:0:0:0:0:FFFF:129.144.52.38"));
|
||||
assertTrue(Util.verifyAsIpAddress("::13.1.68.3"));
|
||||
assertTrue(Util.verifyAsIpAddress("::FFFF:129.144.52.38"));
|
||||
|
||||
// Hostnames
|
||||
assertFalse(OkHostnameVerifier.verifyAsIpAddress("go"));
|
||||
assertFalse(OkHostnameVerifier.verifyAsIpAddress("localhost"));
|
||||
assertFalse(OkHostnameVerifier.verifyAsIpAddress("squareup.com"));
|
||||
assertFalse(OkHostnameVerifier.verifyAsIpAddress("www.nintendo.co.jp"));
|
||||
assertFalse(Util.verifyAsIpAddress("go"));
|
||||
assertFalse(Util.verifyAsIpAddress("localhost"));
|
||||
assertFalse(Util.verifyAsIpAddress("squareup.com"));
|
||||
assertFalse(Util.verifyAsIpAddress("www.nintendo.co.jp"));
|
||||
}
|
||||
|
||||
private X509Certificate certificate(String certificate) throws Exception {
|
||||
|
||||
@@ -23,9 +23,17 @@ import java.util.regex.Pattern;
|
||||
|
||||
import static okhttp3.internal.Util.UTC;
|
||||
import static okhttp3.internal.Util.delimiterOffset;
|
||||
import static okhttp3.internal.Util.domainToAscii;
|
||||
import static okhttp3.internal.Util.trimSubstring;
|
||||
import static okhttp3.internal.Util.verifyAsIpAddress;
|
||||
|
||||
/** An <a href="http://tools.ietf.org/html/rfc6265">RFC 6265</a> Cookie. */
|
||||
/**
|
||||
* An <a href="http://tools.ietf.org/html/rfc6265">RFC 6265</a> Cookie.
|
||||
*
|
||||
* <p>This class doesn't support additional attributes on cookies, like <a
|
||||
* href="https://code.google.com/p/chromium/issues/detail?id=232693">Chromium's Priority=HIGH
|
||||
* extension</a>.
|
||||
*/
|
||||
public final class Cookie {
|
||||
private static final Pattern YEAR_PATTERN
|
||||
= Pattern.compile("(\\d{2,4})[^\\d]*");
|
||||
@@ -41,20 +49,20 @@ public final class Cookie {
|
||||
private final long expiresAt;
|
||||
private final String domain;
|
||||
private final String path;
|
||||
private final boolean secureOnly;
|
||||
private final boolean secure;
|
||||
private final boolean httpOnly;
|
||||
|
||||
private final boolean persistent;
|
||||
private final boolean hostOnly; // True if there's no domain attribute
|
||||
private final boolean persistent; // True if 'expires' or 'max-age' is present.
|
||||
private final boolean hostOnly; // True unless 'domain' is present.
|
||||
|
||||
private Cookie(String name, String value, long expiresAt, String domain, String path,
|
||||
boolean secureOnly, boolean httpOnly, boolean hostOnly, boolean persistent) {
|
||||
boolean secure, boolean httpOnly, boolean hostOnly, boolean persistent) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.expiresAt = expiresAt;
|
||||
this.domain = domain;
|
||||
this.path = path;
|
||||
this.secureOnly = secureOnly;
|
||||
this.secure = secure;
|
||||
this.httpOnly = httpOnly;
|
||||
this.hostOnly = hostOnly;
|
||||
this.persistent = persistent;
|
||||
@@ -102,6 +110,84 @@ public final class Cookie {
|
||||
return hostOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cookie's domain. If {@link #hostOnly()} returns true this is the only domain that
|
||||
* matches this cookie; otherwise it matches this domain and all subdomains.
|
||||
*/
|
||||
public String domain() {
|
||||
return domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this cookie's path. This cookie matches URLs prefixed with path segments that match
|
||||
* this path's segments. For example, if this path is {@code /foo} this cookie matches requests to
|
||||
* {@code /foo} and {@code /foo/bar}, but not {@code /} or {@code /football}.
|
||||
*/
|
||||
public String path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this cookie should be limited to only HTTP APIs. In web browsers this prevents
|
||||
* the cookie from being accessible to scripts.
|
||||
*/
|
||||
public boolean httpOnly() {
|
||||
return httpOnly;
|
||||
}
|
||||
|
||||
/** Returns true if this cookie should be limited to only HTTPS requests. */
|
||||
public boolean secure() {
|
||||
return secure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this cookie should be included on a request to {@code url}. In addition to this
|
||||
* check callers should also confirm that this cookie has not expired.
|
||||
*/
|
||||
public boolean matches(HttpUrl url) {
|
||||
boolean domainMatch = hostOnly
|
||||
? url.host().equals(domain)
|
||||
: domainMatch(url, domain);
|
||||
if (!domainMatch) return false;
|
||||
|
||||
if (!pathMatch(url, path)) return false;
|
||||
|
||||
if (secure && !url.isHttps()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean domainMatch(HttpUrl url, String domain) {
|
||||
String urlHost = url.host();
|
||||
|
||||
if (urlHost.equals(domain)) {
|
||||
return true; // As in 'example.com' matching 'example.com'.
|
||||
}
|
||||
|
||||
if (urlHost.endsWith(domain)
|
||||
&& urlHost.charAt(urlHost.length() - domain.length() - 1) == '.'
|
||||
&& !verifyAsIpAddress(urlHost)) {
|
||||
return true; // As in 'example.com' matching 'www.example.com'.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean pathMatch(HttpUrl url, String path) {
|
||||
String urlPath = url.encodedPath();
|
||||
|
||||
if (urlPath.equals(path)) {
|
||||
return true; // As in '/foo' matching '/foo'.
|
||||
}
|
||||
|
||||
if (urlPath.startsWith(path)) {
|
||||
if (path.endsWith("/")) return true; // As in '/' matching '/foo'.
|
||||
if (urlPath.charAt(path.length()) == '/') return true; // As in '/foo' matching '/foo/bar'.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse a {@code Set-Cookie} HTTP header value {@code setCookie} as a cookie. Returns
|
||||
* null if {@code setCookie} is not a well-formed cookie.
|
||||
@@ -157,8 +243,12 @@ public final class Cookie {
|
||||
// Ignore this attribute, it isn't recognizable as a max age.
|
||||
}
|
||||
} else if (attributeName.equalsIgnoreCase("domain")) {
|
||||
domain = parseDomain(attributeValue);
|
||||
hostOnly = false;
|
||||
try {
|
||||
domain = parseDomain(attributeValue);
|
||||
hostOnly = false;
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Ignore this attribute, it isn't recognizable as a domain.
|
||||
}
|
||||
} else if (attributeName.equalsIgnoreCase("path")) {
|
||||
path = attributeValue;
|
||||
} else if (attributeName.equalsIgnoreCase("secure")) {
|
||||
@@ -182,16 +272,21 @@ public final class Cookie {
|
||||
if (expiresAt < currentTimeMillis) expiresAt = Long.MAX_VALUE; // Clamp overflow.
|
||||
}
|
||||
|
||||
// If the domain is present, it must domain match. Otherwise we have a host-only cookie.
|
||||
if (domain == null) {
|
||||
domain = url.host();
|
||||
} else if (!domainMatch(url, domain)) {
|
||||
return null; // No domain match? This is either incompetence or malice!
|
||||
}
|
||||
|
||||
// If the path is absent or didn't start with '/', use the default path. It's a string like
|
||||
// '/foo/bar' for a URL like 'http://example.com/foo/bar/baz'. It always starts with '/'.
|
||||
if (path == null || !path.startsWith("/")) {
|
||||
String encodedPath = url.encodedPath();
|
||||
int lastSlash = encodedPath.lastIndexOf('/');
|
||||
path = lastSlash == 0 ? encodedPath.substring(0, lastSlash) : "/";
|
||||
path = lastSlash != 0 ? encodedPath.substring(0, lastSlash) : "/";
|
||||
}
|
||||
|
||||
// TODO(jwilson): validate that the domain matches.
|
||||
|
||||
return new Cookie(cookieName, cookieValue, expiresAt, domain, path, secureOnly, httpOnly,
|
||||
hostOnly, persistent);
|
||||
}
|
||||
@@ -295,14 +390,17 @@ public final class Cookie {
|
||||
* or {@code .example.com}.
|
||||
*/
|
||||
private static String parseDomain(String s) {
|
||||
if (s.endsWith(".")) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (s.startsWith(".")) {
|
||||
s = s.substring(1);
|
||||
}
|
||||
return s.toLowerCase(Locale.US);
|
||||
}
|
||||
|
||||
public boolean matches(HttpUrl url) {
|
||||
throw new UnsupportedOperationException("TODO");
|
||||
String canonicalDomain = domainToAscii(s);
|
||||
if (canonicalDomain == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return canonicalDomain;
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package okhttp3;
|
||||
|
||||
import java.net.IDN;
|
||||
import java.net.InetAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
@@ -27,11 +26,11 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import okio.Buffer;
|
||||
|
||||
import static okhttp3.internal.Util.delimiterOffset;
|
||||
import static okhttp3.internal.Util.domainToAscii;
|
||||
import static okhttp3.internal.Util.skipLeadingAsciiWhitespace;
|
||||
import static okhttp3.internal.Util.skipTrailingAsciiWhitespace;
|
||||
|
||||
@@ -1389,47 +1388,6 @@ public final class HttpUrl {
|
||||
return true; // Success.
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs IDN ToASCII encoding and canonicalize the result to lowercase. e.g. This converts
|
||||
* {@code ☃.net} to {@code xn--n3h.net}, and {@code WwW.GoOgLe.cOm} to {@code www.google.com}.
|
||||
* {@code null} will be returned if the input cannot be ToASCII encoded or if the result
|
||||
* contains unsupported ASCII characters.
|
||||
*/
|
||||
private static String domainToAscii(String input) {
|
||||
try {
|
||||
String result = IDN.toASCII(input).toLowerCase(Locale.US);
|
||||
if (result.isEmpty()) return null;
|
||||
|
||||
// Confirm that the IDN ToASCII result doesn't contain any illegal characters.
|
||||
if (containsInvalidHostnameAsciiCodes(result)) {
|
||||
return null;
|
||||
}
|
||||
// TODO: implement all label limits.
|
||||
return result;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsInvalidHostnameAsciiCodes(String hostnameAscii) {
|
||||
for (int i = 0; i < hostnameAscii.length(); i++) {
|
||||
char c = hostnameAscii.charAt(i);
|
||||
// The WHATWG Host parsing rules accepts some character codes which are invalid by
|
||||
// definition for OkHttp's host header checks (and the WHATWG Host syntax definition). Here
|
||||
// we rule out characters that would cause problems in host headers.
|
||||
if (c <= '\u001f' || c >= '\u007f') {
|
||||
return true;
|
||||
}
|
||||
// Check for the characters mentioned in the WHATWG Host parsing spec:
|
||||
// U+0000, U+0009, U+000A, U+000D, U+0020, "#", "%", "/", ":", "?", "@", "[", "\", and "]"
|
||||
// (excluding the characters covered above).
|
||||
if (" #%/:?@[\\]".indexOf(c) != -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Array;
|
||||
import java.net.IDN;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.Charset;
|
||||
@@ -30,10 +31,12 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
import okhttp3.HttpUrl;
|
||||
import okio.Buffer;
|
||||
import okio.ByteString;
|
||||
@@ -50,6 +53,19 @@ public final class Util {
|
||||
/** GMT and UTC are equivalent for our purposes. */
|
||||
public static final TimeZone UTC = TimeZone.getTimeZone("GMT");
|
||||
|
||||
/**
|
||||
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
|
||||
* of Android's private InetAddress#isNumeric API.
|
||||
*
|
||||
* <p>This matches IPv6 addresses as a hex string containing at least one colon, and possibly
|
||||
* including dots after the first colon. It matches IPv4 addresses as strings containing only
|
||||
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
|
||||
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
|
||||
* verification).
|
||||
*/
|
||||
private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile(
|
||||
"([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
|
||||
|
||||
private Util() {
|
||||
}
|
||||
|
||||
@@ -371,4 +387,50 @@ public final class Util {
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs IDN ToASCII encoding and canonicalize the result to lowercase. e.g. This converts
|
||||
* {@code ☃.net} to {@code xn--n3h.net}, and {@code WwW.GoOgLe.cOm} to {@code www.google.com}.
|
||||
* {@code null} will be returned if the input cannot be ToASCII encoded or if the result
|
||||
* contains unsupported ASCII characters.
|
||||
*/
|
||||
public static String domainToAscii(String input) {
|
||||
try {
|
||||
String result = IDN.toASCII(input).toLowerCase(Locale.US);
|
||||
if (result.isEmpty()) return null;
|
||||
|
||||
// Confirm that the IDN ToASCII result doesn't contain any illegal characters.
|
||||
if (containsInvalidHostnameAsciiCodes(result)) {
|
||||
return null;
|
||||
}
|
||||
// TODO: implement all label limits.
|
||||
return result;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsInvalidHostnameAsciiCodes(String hostnameAscii) {
|
||||
for (int i = 0; i < hostnameAscii.length(); i++) {
|
||||
char c = hostnameAscii.charAt(i);
|
||||
// The WHATWG Host parsing rules accepts some character codes which are invalid by
|
||||
// definition for OkHttp's host header checks (and the WHATWG Host syntax definition). Here
|
||||
// we rule out characters that would cause problems in host headers.
|
||||
if (c <= '\u001f' || c >= '\u007f') {
|
||||
return true;
|
||||
}
|
||||
// Check for the characters mentioned in the WHATWG Host parsing spec:
|
||||
// U+0000, U+0009, U+000A, U+000D, U+0020, "#", "%", "/", ":", "?", "@", "[", "\", and "]"
|
||||
// (excluding the characters covered above).
|
||||
if (" #%/:?@[\\]".indexOf(c) != -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Returns true if {@code host} is not a host name and might be an IP address. */
|
||||
public static boolean verifyAsIpAddress(String host) {
|
||||
return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,31 +25,19 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import static okhttp3.internal.Util.verifyAsIpAddress;
|
||||
|
||||
/**
|
||||
* A HostnameVerifier consistent with <a href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>.
|
||||
*/
|
||||
public final class OkHostnameVerifier implements HostnameVerifier {
|
||||
public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier();
|
||||
|
||||
/**
|
||||
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
|
||||
* of Android's private InetAddress#isNumeric API.
|
||||
*
|
||||
* <p>This matches IPv6 addresses as a hex string containing at least one colon, and possibly
|
||||
* including dots after the first colon. It matches IPv4 addresses as strings containing only
|
||||
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
|
||||
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
|
||||
* verification).
|
||||
*/
|
||||
private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile(
|
||||
"([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)");
|
||||
|
||||
private static final int ALT_DNS_NAME = 2;
|
||||
private static final int ALT_IPA_NAME = 7;
|
||||
|
||||
@@ -72,13 +60,7 @@ public final class OkHostnameVerifier implements HostnameVerifier {
|
||||
: verifyHostName(host, certificate);
|
||||
}
|
||||
|
||||
static boolean verifyAsIpAddress(String host) {
|
||||
return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@code certificate} matches {@code ipAddress}.
|
||||
*/
|
||||
/** Returns true if {@code certificate} matches {@code ipAddress}. */
|
||||
private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
|
||||
List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
|
||||
for (int i = 0, size = altNames.size(); i < size; i++) {
|
||||
@@ -89,9 +71,7 @@ public final class OkHostnameVerifier implements HostnameVerifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@code certificate} matches {@code hostName}.
|
||||
*/
|
||||
/** Returns true if {@code certificate} matches {@code hostName}. */
|
||||
private boolean verifyHostName(String hostName, X509Certificate certificate) {
|
||||
hostName = hostName.toLowerCase(Locale.US);
|
||||
boolean hasDns = false;
|
||||
|
||||
Reference in New Issue
Block a user