1
0
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:
jwilson
2015-12-28 12:20:37 -05:00
parent 937bedf919
commit 5c4e76237f
6 changed files with 259 additions and 108 deletions

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;