diff --git a/okhttp-tests/src/test/java/okhttp3/CookieTest.java b/okhttp-tests/src/test/java/okhttp3/CookieTest.java index 284fa1e5b..a1f591360 100644 --- a/okhttp-tests/src/test/java/okhttp3/CookieTest.java +++ b/okhttp-tests/src/test/java/okhttp3/CookieTest.java @@ -17,7 +17,6 @@ package okhttp3; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Objects; @@ -26,6 +25,7 @@ import okhttp3.internal.http.HttpDate; import org.junit.Test; import static java.util.Arrays.asList; +import static okhttp3.internal.InternalKtKt.parseCookie; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; @@ -93,31 +93,31 @@ public final class CookieTest { } @Test public void maxAge() throws Exception { - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=1").expiresAt()).isEqualTo(51000L); - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=9223372036854724").expiresAt()).isEqualTo( + assertThat(parseCookie(50000L, url, "a=b; Max-Age=1").expiresAt()).isEqualTo(51000L); + assertThat(parseCookie(50000L, url, "a=b; Max-Age=9223372036854724").expiresAt()).isEqualTo( HttpDate.MAX_DATE); - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=9223372036854725").expiresAt()).isEqualTo( + assertThat(parseCookie(50000L, url, "a=b; Max-Age=9223372036854725").expiresAt()).isEqualTo( HttpDate.MAX_DATE); - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=9223372036854726").expiresAt()).isEqualTo( + assertThat(parseCookie(50000L, url, "a=b; Max-Age=9223372036854726").expiresAt()).isEqualTo( HttpDate.MAX_DATE); - assertThat(Cookie.parse(9223372036854773807L, url, "a=b; Max-Age=1").expiresAt()).isEqualTo( + assertThat(parseCookie(9223372036854773807L, url, "a=b; Max-Age=1").expiresAt()).isEqualTo( HttpDate.MAX_DATE); - assertThat(Cookie.parse(9223372036854773807L, url, "a=b; Max-Age=2").expiresAt()).isEqualTo( + assertThat(parseCookie(9223372036854773807L, url, "a=b; Max-Age=2").expiresAt()).isEqualTo( HttpDate.MAX_DATE); - assertThat(Cookie.parse(9223372036854773807L, url, "a=b; Max-Age=3").expiresAt()).isEqualTo( + assertThat(parseCookie(9223372036854773807L, url, "a=b; Max-Age=3").expiresAt()).isEqualTo( HttpDate.MAX_DATE); - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=10000000000000000000").expiresAt()).isEqualTo( + assertThat(parseCookie(50000L, url, "a=b; Max-Age=10000000000000000000").expiresAt()).isEqualTo( HttpDate.MAX_DATE); } @Test public void maxAgeNonPositive() throws Exception { - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=-1").expiresAt()).isEqualTo(Long.MIN_VALUE); - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=0").expiresAt()).isEqualTo(Long.MIN_VALUE); - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=-9223372036854775808").expiresAt()).isEqualTo( + assertThat(parseCookie(50000L, url, "a=b; Max-Age=-1").expiresAt()).isEqualTo(Long.MIN_VALUE); + assertThat(parseCookie(50000L, url, "a=b; Max-Age=0").expiresAt()).isEqualTo(Long.MIN_VALUE); + assertThat(parseCookie(50000L, url, "a=b; Max-Age=-9223372036854775808").expiresAt()).isEqualTo( Long.MIN_VALUE); - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=-9223372036854775809").expiresAt()).isEqualTo( + assertThat(parseCookie(50000L, url, "a=b; Max-Age=-9223372036854775809").expiresAt()).isEqualTo( Long.MIN_VALUE); - assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=-10000000000000000000").expiresAt()).isEqualTo( + assertThat(parseCookie(50000L, url, "a=b; Max-Age=-10000000000000000000").expiresAt()).isEqualTo( Long.MIN_VALUE); } @@ -356,30 +356,30 @@ public final class CookieTest { @Test public void maxAgeTakesPrecedenceOverExpires() throws Exception { // Max-Age = 1, Expires = 2. In either order. - assertThat(Cookie.parse( + assertThat(parseCookie( 0L, url, "a=b; Max-Age=1; Expires=Thu, 01 Jan 1970 00:00:02 GMT").expiresAt()).isEqualTo( 1000L); - assertThat(Cookie.parse( + assertThat(parseCookie( 0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:02 GMT; Max-Age=1").expiresAt()).isEqualTo( 1000L); // Max-Age = 2, Expires = 1. In either order. - assertThat(Cookie.parse( + assertThat(parseCookie( 0L, url, "a=b; Max-Age=2; Expires=Thu, 01 Jan 1970 00:00:01 GMT").expiresAt()).isEqualTo( 2000L); - assertThat(Cookie.parse( + assertThat(parseCookie( 0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=2").expiresAt()).isEqualTo( 2000L); } /** If a cookie incorrectly defines multiple 'Max-Age' attributes, the last one defined wins. */ @Test public void lastMaxAgeWins() throws Exception { - assertThat(Cookie.parse( + assertThat(parseCookie( 0L, url, "a=b; Max-Age=2; Max-Age=4; Max-Age=1; Max-Age=3").expiresAt()).isEqualTo(3000L); } /** If a cookie incorrectly defines multiple 'Expires' attributes, the last one defined wins. */ @Test public void lastExpiresAtWins() throws Exception { - assertThat(Cookie.parse(0L, url, "a=b; " + assertThat(parseCookie(0L, url, "a=b; " + "Expires=Thu, 01 Jan 1970 00:00:02 GMT; " + "Expires=Thu, 01 Jan 1970 00:00:04 GMT; " + "Expires=Thu, 01 Jan 1970 00:00:01 GMT; " @@ -387,9 +387,9 @@ public final class CookieTest { } @Test public void maxAgeOrExpiresMakesCookiePersistent() throws Exception { - assertThat(Cookie.parse(0L, url, "a=b").persistent()).isFalse(); - assertThat(Cookie.parse(0L, url, "a=b; Max-Age=1").persistent()).isTrue(); - assertThat(Cookie.parse(0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:01 GMT").persistent()).isTrue(); + assertThat(parseCookie(0L, url, "a=b").persistent()).isFalse(); + assertThat(parseCookie(0L, url, "a=b; Max-Age=1").persistent()).isTrue(); + assertThat(parseCookie(0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:01 GMT").persistent()).isTrue(); } @Test public void parseAll() throws Exception { @@ -424,7 +424,7 @@ public final class CookieTest { try { new Cookie.Builder().name(null); fail(); - } catch (NullPointerException expected) { + } catch (IllegalArgumentException expected) { } try { new Cookie.Builder().name(" a "); @@ -437,7 +437,7 @@ public final class CookieTest { try { new Cookie.Builder().value(null); fail(); - } catch (NullPointerException expected) { + } catch (IllegalArgumentException expected) { } try { new Cookie.Builder().value(" b "); @@ -482,7 +482,7 @@ public final class CookieTest { try { new Cookie.Builder().hostOnlyDomain(null); fail(); - } catch (NullPointerException expected) { + } catch (IllegalArgumentException expected) { } try { new Cookie.Builder().hostOnlyDomain("a/b"); @@ -515,7 +515,7 @@ public final class CookieTest { try { new Cookie.Builder().path(null); fail(); - } catch (NullPointerException expected) { + } catch (IllegalArgumentException expected) { } try { new Cookie.Builder().path("foo"); @@ -564,9 +564,9 @@ public final class CookieTest { "a=b; Path=/c; Domain=example.com; Max-Age=5; Secure; " ); for (String stringA : cookieStrings) { - Cookie cookieA = Cookie.parse(0, url, stringA); + Cookie cookieA = parseCookie(0, url, stringA); for (String stringB : cookieStrings) { - Cookie cookieB = Cookie.parse(0, url, stringB); + Cookie cookieB = parseCookie(0, url, stringB); if (Objects.equals(stringA, stringB)) { assertThat(cookieB.hashCode()).isEqualTo(cookieA.hashCode()); assertThat(cookieB).isEqualTo(cookieA); diff --git a/okhttp-urlconnection/src/main/java/okhttp3/JavaNetCookieJar.java b/okhttp-urlconnection/src/main/java/okhttp3/JavaNetCookieJar.java deleted file mode 100644 index 6faf288de..000000000 --- a/okhttp-urlconnection/src/main/java/okhttp3/JavaNetCookieJar.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2015 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okhttp3; - -import java.io.IOException; -import java.net.CookieHandler; -import java.net.HttpCookie; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import okhttp3.internal.annotations.EverythingIsNonNull; -import okhttp3.internal.platform.Platform; - -import static okhttp3.internal.Util.delimiterOffset; -import static okhttp3.internal.Util.trimSubstring; -import static okhttp3.internal.platform.Platform.WARN; - -/** A cookie jar that delegates to a {@link java.net.CookieHandler}. */ -@EverythingIsNonNull -public final class JavaNetCookieJar implements CookieJar { - private final CookieHandler cookieHandler; - - public JavaNetCookieJar(CookieHandler cookieHandler) { - this.cookieHandler = cookieHandler; - } - - @Override public void saveFromResponse(HttpUrl url, List cookies) { - if (cookieHandler != null) { - List cookieStrings = new ArrayList<>(); - for (Cookie cookie : cookies) { - cookieStrings.add(cookie.toString(true)); - } - Map> multimap = Collections.singletonMap("Set-Cookie", cookieStrings); - try { - cookieHandler.put(url.uri(), multimap); - } catch (IOException e) { - Platform.get().log(WARN, "Saving cookies failed for " + url.resolve("/..."), e); - } - } - } - - @Override public List loadForRequest(HttpUrl url) { - // The RI passes all headers. We don't have 'em, so we don't pass 'em! - Map> headers = Collections.emptyMap(); - Map> cookieHeaders; - try { - cookieHeaders = cookieHandler.get(url.uri(), headers); - } catch (IOException e) { - Platform.get().log(WARN, "Loading cookies failed for " + url.resolve("/..."), e); - return Collections.emptyList(); - } - - List cookies = null; - for (Map.Entry> entry : cookieHeaders.entrySet()) { - String key = entry.getKey(); - if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) - && !entry.getValue().isEmpty()) { - for (String header : entry.getValue()) { - if (cookies == null) cookies = new ArrayList<>(); - cookies.addAll(decodeHeaderAsJavaNetCookies(url, header)); - } - } - } - - return cookies != null - ? Collections.unmodifiableList(cookies) - : Collections.emptyList(); - } - - /** - * Convert a request header to OkHttp's cookies via {@link HttpCookie}. That extra step handles - * multiple cookies in a single request header, which {@link Cookie#parse} doesn't support. - */ - private List decodeHeaderAsJavaNetCookies(HttpUrl url, String header) { - List result = new ArrayList<>(); - for (int pos = 0, limit = header.length(), pairEnd; pos < limit; pos = pairEnd + 1) { - pairEnd = delimiterOffset(header, pos, limit, ";,"); - int equalsSign = delimiterOffset(header, pos, pairEnd, '='); - String name = trimSubstring(header, pos, equalsSign); - if (name.startsWith("$")) continue; - - // We have either name=value or just a name. - String value = equalsSign < pairEnd - ? trimSubstring(header, equalsSign + 1, pairEnd) - : ""; - - // If the value is "quoted", drop the quotes. - if (value.startsWith("\"") && value.endsWith("\"")) { - value = value.substring(1, value.length() - 1); - } - - result.add(new Cookie.Builder() - .name(name) - .value(value) - .domain(url.host()) - .build()); - } - return result; - } -} diff --git a/okhttp-urlconnection/src/main/java/okhttp3/JavaNetCookieJar.kt b/okhttp-urlconnection/src/main/java/okhttp3/JavaNetCookieJar.kt new file mode 100644 index 000000000..c9b2cd50f --- /dev/null +++ b/okhttp-urlconnection/src/main/java/okhttp3/JavaNetCookieJar.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3 + +import okhttp3.internal.Util.delimiterOffset +import okhttp3.internal.Util.trimSubstring +import okhttp3.internal.cookieToString +import okhttp3.internal.platform.Platform +import okhttp3.internal.platform.Platform.Companion.WARN +import java.io.IOException +import java.net.CookieHandler +import java.net.HttpCookie +import java.util.ArrayList +import java.util.Collections + +/** A cookie jar that delegates to a [java.net.CookieHandler]. */ +class JavaNetCookieJar(private val cookieHandler: CookieHandler) : CookieJar { + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + val cookieStrings = mutableListOf() + for (cookie in cookies) { + cookieStrings.add(cookieToString(cookie, true)) + } + val multimap = mapOf("Set-Cookie" to cookieStrings) + try { + cookieHandler.put(url.uri(), multimap) + } catch (e: IOException) { + Platform.get().log(WARN, "Saving cookies failed for " + url.resolve("/...")!!, e) + } + } + + override fun loadForRequest(url: HttpUrl): List { + val cookieHeaders = try { + // The RI passes all headers. We don't have 'em, so we don't pass 'em! + cookieHandler.get(url.uri(), emptyMap>()) + } catch (e: IOException) { + Platform.get().log(WARN, "Loading cookies failed for " + url.resolve("/...")!!, e) + return emptyList() + } + + var cookies: MutableList? = null + for ((key, value) in cookieHeaders) { + if (("Cookie".equals(key, ignoreCase = true) || "Cookie2".equals(key, ignoreCase = true)) + && value.isNotEmpty()) { + for (header in value) { + if (cookies == null) cookies = ArrayList() + cookies.addAll(decodeHeaderAsJavaNetCookies(url, header)) + } + } + } + + return if (cookies != null) { + Collections.unmodifiableList(cookies) + } else { + emptyList() + } + } + + /** + * Convert a request header to OkHttp's cookies via [HttpCookie]. That extra step handles + * multiple cookies in a single request header, which [Cookie.parse] doesn't support. + */ + private fun decodeHeaderAsJavaNetCookies(url: HttpUrl, header: String): List { + val result = mutableListOf() + var pos = 0 + val limit = header.length + var pairEnd: Int + while (pos < limit) { + pairEnd = delimiterOffset(header, pos, limit, ";,") + val equalsSign = delimiterOffset(header, pos, pairEnd, '=') + val name = trimSubstring(header, pos, equalsSign) + if (name.startsWith("$")) { + pos = pairEnd + 1 + continue + } + + // We have either name=value or just a name. + var value = if (equalsSign < pairEnd) { + trimSubstring(header, equalsSign + 1, pairEnd) + } else { + "" + } + + // If the value is "quoted", drop the quotes. + if (value.startsWith("\"") && value.endsWith("\"")) { + value = value.substring(1, value.length - 1) + } + + result.add(Cookie.Builder() + .name(name) + .value(value) + .domain(url.host()) + .build()) + pos = pairEnd + 1 + } + return result + } +} diff --git a/okhttp/build.gradle b/okhttp/build.gradle index 447b90c38..7f7204cdb 100644 --- a/okhttp/build.gradle +++ b/okhttp/build.gradle @@ -71,6 +71,25 @@ task japicmp(type: me.champeau.gradle.japicmp.JapicmpTask, dependsOn: 'jar') { 'okhttp3.ConnectionPool#connectionCount()', 'okhttp3.ConnectionPool#evictAll()', 'okhttp3.ConnectionPool#idleConnectionCount()', + 'okhttp3.Cookie#domain()', + 'okhttp3.Cookie#expiresAt()', + 'okhttp3.Cookie#hostOnly()', + 'okhttp3.Cookie#httpOnly()', + 'okhttp3.Cookie#matches(okhttp3.HttpUrl)', + 'okhttp3.Cookie#name()', + 'okhttp3.Cookie#path()', + 'okhttp3.Cookie#persistent()', + 'okhttp3.Cookie#secure()', + 'okhttp3.Cookie#value()', + 'okhttp3.Cookie$Builder#build()', + 'okhttp3.Cookie$Builder#domain(java.lang.String)', + 'okhttp3.Cookie$Builder#expiresAt(long)', + 'okhttp3.Cookie$Builder#hostOnlyDomain(java.lang.String)', + 'okhttp3.Cookie$Builder#httpOnly()', + 'okhttp3.Cookie$Builder#name(java.lang.String)', + 'okhttp3.Cookie$Builder#path(java.lang.String)', + 'okhttp3.Cookie$Builder#secure()', + 'okhttp3.Cookie$Builder#value(java.lang.String)', 'okhttp3.FormBody#encodedName(int)', 'okhttp3.FormBody#encodedValue(int)', 'okhttp3.FormBody#name(int)', diff --git a/okhttp/src/main/java/okhttp3/Cookie.java b/okhttp/src/main/java/okhttp3/Cookie.java deleted file mode 100644 index 867451e38..000000000 --- a/okhttp/src/main/java/okhttp3/Cookie.java +++ /dev/null @@ -1,612 +0,0 @@ -/* - * Copyright (C) 2015 Square, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okhttp3; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nullable; -import okhttp3.internal.Util; -import okhttp3.internal.http.HttpDate; -import okhttp3.internal.publicsuffix.PublicSuffixDatabase; - -import static okhttp3.internal.Util.UTC; -import static okhttp3.internal.Util.canonicalizeHost; -import static okhttp3.internal.Util.delimiterOffset; -import static okhttp3.internal.Util.indexOfControlOrNonAscii; -import static okhttp3.internal.Util.trimSubstring; -import static okhttp3.internal.Util.verifyAsIpAddress; - -/** - * An RFC 6265 Cookie. - * - *

This class doesn't support additional attributes on cookies, like Chromium's Priority=HIGH - * extension. - */ -public final class Cookie { - private static final Pattern YEAR_PATTERN - = Pattern.compile("(\\d{2,4})[^\\d]*"); - private static final Pattern MONTH_PATTERN - = Pattern.compile("(?i)(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec).*"); - private static final Pattern DAY_OF_MONTH_PATTERN - = Pattern.compile("(\\d{1,2})[^\\d]*"); - private static final Pattern TIME_PATTERN - = Pattern.compile("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})[^\\d]*"); - - private final String name; - private final String value; - private final long expiresAt; - private final String domain; - private final String path; - private final boolean secure; - private final boolean httpOnly; - - 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 secure, boolean httpOnly, boolean hostOnly, boolean persistent) { - this.name = name; - this.value = value; - this.expiresAt = expiresAt; - this.domain = domain; - this.path = path; - this.secure = secure; - this.httpOnly = httpOnly; - this.hostOnly = hostOnly; - this.persistent = persistent; - } - - Cookie(Builder builder) { - if (builder.name == null) throw new NullPointerException("builder.name == null"); - if (builder.value == null) throw new NullPointerException("builder.value == null"); - if (builder.domain == null) throw new NullPointerException("builder.domain == null"); - - this.name = builder.name; - this.value = builder.value; - this.expiresAt = builder.expiresAt; - this.domain = builder.domain; - this.path = builder.path; - this.secure = builder.secure; - this.httpOnly = builder.httpOnly; - this.persistent = builder.persistent; - this.hostOnly = builder.hostOnly; - } - - /** Returns a non-empty string with this cookie's name. */ - public String name() { - return name; - } - - /** Returns a possibly-empty string with this cookie's value. */ - public String value() { - return value; - } - - /** Returns true if this cookie does not expire at the end of the current session. */ - public boolean persistent() { - return persistent; - } - - /** - * Returns the time that this cookie expires, in the same format as {@link - * System#currentTimeMillis()}. This is December 31, 9999 if the cookie is {@linkplain - * #persistent() not persistent}, in which case it will expire at the end of the current session. - * - *

This may return a value less than the current time, in which case the cookie is already - * expired. Webservers may return expired cookies as a mechanism to delete previously set cookies - * that may or may not themselves be expired. - */ - public long expiresAt() { - return expiresAt; - } - - /** - * Returns true if this cookie's domain should be interpreted as a single host name, or false if - * it should be interpreted as a pattern. This flag will be false if its {@code Set-Cookie} header - * included a {@code domain} attribute. - * - *

For example, suppose the cookie's domain is {@code example.com}. If this flag is true it - * matches only {@code example.com}. If this flag is false it matches {@code - * example.com} and all subdomains including {@code api.example.com}, {@code www.example.com}, and - * {@code beta.api.example.com}. - */ - public boolean hostOnly() { - 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.host(), domain); - if (!domainMatch) return false; - - if (!pathMatch(url, path)) return false; - - if (secure && !url.isHttps()) return false; - - return true; - } - - private static boolean domainMatch(String urlHost, String domain) { - 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. - */ - public static @Nullable Cookie parse(HttpUrl url, String setCookie) { - return parse(System.currentTimeMillis(), url, setCookie); - } - - static @Nullable Cookie parse(long currentTimeMillis, HttpUrl url, String setCookie) { - int pos = 0; - int limit = setCookie.length(); - int cookiePairEnd = delimiterOffset(setCookie, pos, limit, ';'); - - int pairEqualsSign = delimiterOffset(setCookie, pos, cookiePairEnd, '='); - if (pairEqualsSign == cookiePairEnd) return null; - - String cookieName = trimSubstring(setCookie, pos, pairEqualsSign); - if (cookieName.isEmpty() || indexOfControlOrNonAscii(cookieName) != -1) return null; - - String cookieValue = trimSubstring(setCookie, pairEqualsSign + 1, cookiePairEnd); - if (indexOfControlOrNonAscii(cookieValue) != -1) return null; - - long expiresAt = HttpDate.MAX_DATE; - long deltaSeconds = -1L; - String domain = null; - String path = null; - boolean secureOnly = false; - boolean httpOnly = false; - boolean hostOnly = true; - boolean persistent = false; - - pos = cookiePairEnd + 1; - while (pos < limit) { - int attributePairEnd = delimiterOffset(setCookie, pos, limit, ';'); - - int attributeEqualsSign = delimiterOffset(setCookie, pos, attributePairEnd, '='); - String attributeName = trimSubstring(setCookie, pos, attributeEqualsSign); - String attributeValue = attributeEqualsSign < attributePairEnd - ? trimSubstring(setCookie, attributeEqualsSign + 1, attributePairEnd) - : ""; - - if (attributeName.equalsIgnoreCase("expires")) { - try { - expiresAt = parseExpires(attributeValue, 0, attributeValue.length()); - persistent = true; - } catch (IllegalArgumentException e) { - // Ignore this attribute, it isn't recognizable as a date. - } - } else if (attributeName.equalsIgnoreCase("max-age")) { - try { - deltaSeconds = parseMaxAge(attributeValue); - persistent = true; - } catch (NumberFormatException e) { - // Ignore this attribute, it isn't recognizable as a max age. - } - } else if (attributeName.equalsIgnoreCase("domain")) { - 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")) { - secureOnly = true; - } else if (attributeName.equalsIgnoreCase("httponly")) { - httpOnly = true; - } - - pos = attributePairEnd + 1; - } - - // If 'Max-Age' is present, it takes precedence over 'Expires', regardless of the order the two - // attributes are declared in the cookie string. - if (deltaSeconds == Long.MIN_VALUE) { - expiresAt = Long.MIN_VALUE; - } else if (deltaSeconds != -1L) { - long deltaMilliseconds = deltaSeconds <= (Long.MAX_VALUE / 1000) - ? deltaSeconds * 1000 - : Long.MAX_VALUE; - expiresAt = currentTimeMillis + deltaMilliseconds; - if (expiresAt < currentTimeMillis || expiresAt > HttpDate.MAX_DATE) { - expiresAt = HttpDate.MAX_DATE; // Handle overflow & limit the date range. - } - } - - // If the domain is present, it must domain match. Otherwise we have a host-only cookie. - String urlHost = url.host(); - if (domain == null) { - domain = urlHost; - } else if (!domainMatch(urlHost, domain)) { - return null; // No domain match? This is either incompetence or malice! - } - - // If the domain is a suffix of the url host, it must not be a public suffix. - if (urlHost.length() != domain.length() - && PublicSuffixDatabase.get().getEffectiveTldPlusOne(domain) == null) { - return null; - } - - // 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) : "/"; - } - - return new Cookie(cookieName, cookieValue, expiresAt, domain, path, secureOnly, httpOnly, - hostOnly, persistent); - } - - /** Parse a date as specified in RFC 6265, section 5.1.1. */ - private static long parseExpires(String s, int pos, int limit) { - pos = dateCharacterOffset(s, pos, limit, false); - - int hour = -1; - int minute = -1; - int second = -1; - int dayOfMonth = -1; - int month = -1; - int year = -1; - Matcher matcher = TIME_PATTERN.matcher(s); - - while (pos < limit) { - int end = dateCharacterOffset(s, pos + 1, limit, true); - matcher.region(pos, end); - - if (hour == -1 && matcher.usePattern(TIME_PATTERN).matches()) { - hour = Integer.parseInt(matcher.group(1)); - minute = Integer.parseInt(matcher.group(2)); - second = Integer.parseInt(matcher.group(3)); - } else if (dayOfMonth == -1 && matcher.usePattern(DAY_OF_MONTH_PATTERN).matches()) { - dayOfMonth = Integer.parseInt(matcher.group(1)); - } else if (month == -1 && matcher.usePattern(MONTH_PATTERN).matches()) { - String monthString = matcher.group(1).toLowerCase(Locale.US); - month = MONTH_PATTERN.pattern().indexOf(monthString) / 4; // Sneaky! jan=1, dec=12. - } else if (year == -1 && matcher.usePattern(YEAR_PATTERN).matches()) { - year = Integer.parseInt(matcher.group(1)); - } - - pos = dateCharacterOffset(s, end + 1, limit, false); - } - - // Convert two-digit years into four-digit years. 99 becomes 1999, 15 becomes 2015. - if (year >= 70 && year <= 99) year += 1900; - if (year >= 0 && year <= 69) year += 2000; - - // If any partial is omitted or out of range, return -1. The date is impossible. Note that leap - // seconds are not supported by this syntax. - if (year < 1601) throw new IllegalArgumentException(); - if (month == -1) throw new IllegalArgumentException(); - if (dayOfMonth < 1 || dayOfMonth > 31) throw new IllegalArgumentException(); - if (hour < 0 || hour > 23) throw new IllegalArgumentException(); - if (minute < 0 || minute > 59) throw new IllegalArgumentException(); - if (second < 0 || second > 59) throw new IllegalArgumentException(); - - Calendar calendar = new GregorianCalendar(UTC); - calendar.setLenient(false); - calendar.set(Calendar.YEAR, year); - calendar.set(Calendar.MONTH, month - 1); - calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, minute); - calendar.set(Calendar.SECOND, second); - calendar.set(Calendar.MILLISECOND, 0); - return calendar.getTimeInMillis(); - } - - /** - * Returns the index of the next date character in {@code input}, or if {@code invert} the index - * of the next non-date character in {@code input}. - */ - private static int dateCharacterOffset(String input, int pos, int limit, boolean invert) { - for (int i = pos; i < limit; i++) { - int c = input.charAt(i); - boolean dateCharacter = (c < ' ' && c != '\t') || (c >= '\u007f') - || (c >= '0' && c <= '9') - || (c >= 'a' && c <= 'z') - || (c >= 'A' && c <= 'Z') - || (c == ':'); - if (dateCharacter == !invert) return i; - } - return limit; - } - - /** - * Returns the positive value if {@code attributeValue} is positive, or {@link Long#MIN_VALUE} if - * it is either 0 or negative. If the value is positive but out of range, this returns {@link - * Long#MAX_VALUE}. - * - * @throws NumberFormatException if {@code s} is not an integer of any precision. - */ - private static long parseMaxAge(String s) { - try { - long parsed = Long.parseLong(s); - return parsed <= 0L ? Long.MIN_VALUE : parsed; - } catch (NumberFormatException e) { - // Check if the value is an integer (positive or negative) that's too big for a long. - if (s.matches("-?\\d+")) { - return s.startsWith("-") ? Long.MIN_VALUE : Long.MAX_VALUE; - } - throw e; - } - } - - /** - * Returns a domain string like {@code example.com} for an input domain like {@code EXAMPLE.COM} - * or {@code .example.com}. - */ - private static String parseDomain(String s) { - if (s.endsWith(".")) { - throw new IllegalArgumentException(); - } - if (s.startsWith(".")) { - s = s.substring(1); - } - String canonicalDomain = canonicalizeHost(s); - if (canonicalDomain == null) { - throw new IllegalArgumentException(); - } - return canonicalDomain; - } - - /** Returns all of the cookies from a set of HTTP response headers. */ - public static List parseAll(HttpUrl url, Headers headers) { - List cookieStrings = headers.values("Set-Cookie"); - List cookies = null; - - for (int i = 0, size = cookieStrings.size(); i < size; i++) { - Cookie cookie = Cookie.parse(url, cookieStrings.get(i)); - if (cookie == null) continue; - if (cookies == null) cookies = new ArrayList<>(); - cookies.add(cookie); - } - - return cookies != null - ? Collections.unmodifiableList(cookies) - : Collections.emptyList(); - } - - /** - * Builds a cookie. The {@linkplain #name() name}, {@linkplain #value() value}, and {@linkplain - * #domain() domain} values must all be set before calling {@link #build}. - */ - public static final class Builder { - @Nullable String name; - @Nullable String value; - long expiresAt = HttpDate.MAX_DATE; - @Nullable String domain; - String path = "/"; - boolean secure; - boolean httpOnly; - boolean persistent; - boolean hostOnly; - - public Builder name(String name) { - if (name == null) throw new NullPointerException("name == null"); - if (!name.trim().equals(name)) throw new IllegalArgumentException("name is not trimmed"); - this.name = name; - return this; - } - - public Builder value(String value) { - if (value == null) throw new NullPointerException("value == null"); - if (!value.trim().equals(value)) throw new IllegalArgumentException("value is not trimmed"); - this.value = value; - return this; - } - - public Builder expiresAt(long expiresAt) { - if (expiresAt <= 0) expiresAt = Long.MIN_VALUE; - if (expiresAt > HttpDate.MAX_DATE) expiresAt = HttpDate.MAX_DATE; - this.expiresAt = expiresAt; - this.persistent = true; - return this; - } - - /** - * Set the domain pattern for this cookie. The cookie will match {@code domain} and all of its - * subdomains. - */ - public Builder domain(String domain) { - return domain(domain, false); - } - - /** - * Set the host-only domain for this cookie. The cookie will match {@code domain} but none of - * its subdomains. - */ - public Builder hostOnlyDomain(String domain) { - return domain(domain, true); - } - - private Builder domain(String domain, boolean hostOnly) { - if (domain == null) throw new NullPointerException("domain == null"); - String canonicalDomain = Util.canonicalizeHost(domain); - if (canonicalDomain == null) { - throw new IllegalArgumentException("unexpected domain: " + domain); - } - this.domain = canonicalDomain; - this.hostOnly = hostOnly; - return this; - } - - public Builder path(String path) { - if (!path.startsWith("/")) throw new IllegalArgumentException("path must start with '/'"); - this.path = path; - return this; - } - - public Builder secure() { - this.secure = true; - return this; - } - - public Builder httpOnly() { - this.httpOnly = true; - return this; - } - - public Cookie build() { - return new Cookie(this); - } - } - - @Override public String toString() { - return toString(false); - } - - /** - * @param forObsoleteRfc2965 true to include a leading {@code .} on the domain pattern. This is - * necessary for {@code example.com} to match {@code www.example.com} under RFC 2965. This - * extra dot is ignored by more recent specifications. - */ - String toString(boolean forObsoleteRfc2965) { - StringBuilder result = new StringBuilder(); - result.append(name); - result.append('='); - result.append(value); - - if (persistent) { - if (expiresAt == Long.MIN_VALUE) { - result.append("; max-age=0"); - } else { - result.append("; expires=").append(HttpDate.format(new Date(expiresAt))); - } - } - - if (!hostOnly) { - result.append("; domain="); - if (forObsoleteRfc2965) { - result.append("."); - } - result.append(domain); - } - - result.append("; path=").append(path); - - if (secure) { - result.append("; secure"); - } - - if (httpOnly) { - result.append("; httponly"); - } - - return result.toString(); - } - - @Override public boolean equals(@Nullable Object other) { - if (!(other instanceof Cookie)) return false; - Cookie that = (Cookie) other; - return that.name.equals(name) - && that.value.equals(value) - && that.domain.equals(domain) - && that.path.equals(path) - && that.expiresAt == expiresAt - && that.secure == secure - && that.httpOnly == httpOnly - && that.persistent == persistent - && that.hostOnly == hostOnly; - } - - @Override public int hashCode() { - int hash = 17; - hash = 31 * hash + name.hashCode(); - hash = 31 * hash + value.hashCode(); - hash = 31 * hash + domain.hashCode(); - hash = 31 * hash + path.hashCode(); - hash = 31 * hash + (int) (expiresAt ^ (expiresAt >>> 32)); - hash = 31 * hash + (secure ? 0 : 1); - hash = 31 * hash + (httpOnly ? 0 : 1); - hash = 31 * hash + (persistent ? 0 : 1); - hash = 31 * hash + (hostOnly ? 0 : 1); - return hash; - } -} diff --git a/okhttp/src/main/java/okhttp3/Cookie.kt b/okhttp/src/main/java/okhttp3/Cookie.kt new file mode 100644 index 000000000..a8bcf2e00 --- /dev/null +++ b/okhttp/src/main/java/okhttp3/Cookie.kt @@ -0,0 +1,556 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3 + +import okhttp3.internal.Util +import okhttp3.internal.Util.UTC +import okhttp3.internal.Util.canonicalizeHost +import okhttp3.internal.Util.delimiterOffset +import okhttp3.internal.Util.indexOfControlOrNonAscii +import okhttp3.internal.Util.trimSubstring +import okhttp3.internal.Util.verifyAsIpAddress +import okhttp3.internal.http.HttpDate +import okhttp3.internal.publicsuffix.PublicSuffixDatabase +import java.util.Calendar +import java.util.Collections +import java.util.Date +import java.util.GregorianCalendar +import java.util.Locale +import java.util.regex.Pattern + +/** + * An [RFC 6265](http://tools.ietf.org/html/rfc6265) Cookie. + * + * This class doesn't support additional attributes on cookies, like [Chromium's Priority=HIGH + * extension](https://code.google.com/p/chromium/issues/detail?id=232693). + */ +class Cookie private constructor( + private val name: String, + private val value: String, + private val expiresAt: Long, + private val domain: String, + private val path: String, + private val secure: Boolean, + private val httpOnly: Boolean, + private val persistent: Boolean, // True if 'expires' or 'max-age' is present. + private val hostOnly: Boolean // True unless 'domain' is present. +) { + /** Returns a non-empty string with this cookie's name. */ + fun name(): String = name + + /** Returns a possibly-empty string with this cookie's value. */ + fun value(): String = value + + /** Returns true if this cookie does not expire at the end of the current session. */ + fun persistent(): Boolean = persistent + + /** + * Returns the time that this cookie expires, in the same format as [System.currentTimeMillis]. + * This is December 31, 9999 if the cookie is [persistent], in which case it will expire at the + * end of the current session. + * + * This may return a value less than the current time, in which case the cookie is already + * expired. Webservers may return expired cookies as a mechanism to delete previously set cookies + * that may or may not themselves be expired. + */ + fun expiresAt(): Long = expiresAt + + /** + * Returns true if this cookie's domain should be interpreted as a single host name, or false if + * it should be interpreted as a pattern. This flag will be false if its `Set-Cookie` header + * included a `domain` attribute. + * + * For example, suppose the cookie's domain is `example.com`. If this flag is true it matches + * **only** `example.com`. If this flag is false it matches `example.com` and all subdomains + * including `api.example.com`, `www.example.com`, and `beta.api.example.com`. + */ + fun hostOnly(): Boolean = hostOnly + + /** + * Returns the cookie's domain. If [.hostOnly] returns true this is the only domain that + * matches this cookie; otherwise it matches this domain and all subdomains. + */ + fun domain(): String = 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 `/foo` this cookie matches requests to + * `/foo` and `/foo/bar`, but not `/` or `/football`. + */ + fun path(): String = 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. + */ + fun httpOnly(): Boolean = httpOnly + + /** Returns true if this cookie should be limited to only HTTPS requests. */ + fun secure(): Boolean = secure + + /** + * Returns true if this cookie should be included on a request to `url`. In addition to this + * check callers should also confirm that this cookie has not expired. + */ + fun matches(url: HttpUrl): Boolean { + val domainMatch = if (hostOnly) { + url.host() == domain + } else { + domainMatch(url.host(), domain) + } + if (!domainMatch) return false + + if (!pathMatch(url, path)) return false + + return !secure || url.isHttps + } + + /** + * Builds a cookie. The [name], [value], and [domain] values must all be set before calling + * [build]. + */ + class Builder { + private var name: String? = null + private var value: String? = null + private var expiresAt = HttpDate.MAX_DATE + private var domain: String? = null + private var path = "/" + private var secure: Boolean = false + private var httpOnly: Boolean = false + private var persistent: Boolean = false + private var hostOnly: Boolean = false + + fun name(name: String) = apply { + require(name.trim { it <= ' ' } == name) { "name is not trimmed" } + this.name = name + } + + fun value(value: String) = apply { + require(value.trim { it <= ' ' } == value) { "value is not trimmed" } + this.value = value + } + + fun expiresAt(expiresAt: Long) = apply { + var expiresAt = expiresAt + if (expiresAt <= 0) expiresAt = Long.MIN_VALUE + if (expiresAt > HttpDate.MAX_DATE) expiresAt = HttpDate.MAX_DATE + this.expiresAt = expiresAt + this.persistent = true + } + + /** + * Set the domain pattern for this cookie. The cookie will match `domain` and all of its + * subdomains. + */ + fun domain(domain: String): Builder = domain(domain, false) + + /** + * Set the host-only domain for this cookie. The cookie will match `domain` but none of + * its subdomains. + */ + fun hostOnlyDomain(domain: String): Builder = domain(domain, true) + + private fun domain(domain: String, hostOnly: Boolean) = apply { + val canonicalDomain = Util.canonicalizeHost(domain) + ?: throw IllegalArgumentException("unexpected domain: $domain") + this.domain = canonicalDomain + this.hostOnly = hostOnly + } + + fun path(path: String) = apply { + require(path.startsWith("/")) { "path must start with '/'" } + this.path = path + } + + fun secure() = apply { + this.secure = true + } + + fun httpOnly() = apply { + this.httpOnly = true + } + + fun build(): Cookie { + return Cookie( + name ?: throw NullPointerException("builder.name == null"), + value ?: throw NullPointerException("builder.value == null"), + expiresAt, + domain ?: throw NullPointerException("builder.domain == null"), + path, + secure, + httpOnly, + persistent, + hostOnly) + } + } + + override fun toString(): String = toString(false) + + /** + * @param forObsoleteRfc2965 true to include a leading `.` on the domain pattern. This is + * necessary for `example.com` to match `www.example.com` under RFC 2965. This extra dot is + * ignored by more recent specifications. + */ + internal fun toString(forObsoleteRfc2965: Boolean): String { + return buildString { + append(name) + append('=') + append(value) + + if (persistent) { + if (expiresAt == Long.MIN_VALUE) { + append("; max-age=0") + } else { + append("; expires=").append(HttpDate.format(Date(expiresAt))) + } + } + + if (!hostOnly) { + append("; domain=") + if (forObsoleteRfc2965) { + append(".") + } + append(domain) + } + + append("; path=").append(path) + + if (secure) { + append("; secure") + } + + if (httpOnly) { + append("; httponly") + } + + return toString() + } + } + + override fun equals(other: Any?): Boolean { + if (other !is Cookie) return false + val that = other as Cookie? + return (that!!.name == name + && that.value == value + && that.domain == domain + && that.path == path + && that.expiresAt == expiresAt + && that.secure == secure + && that.httpOnly == httpOnly + && that.persistent == persistent + && that.hostOnly == hostOnly) + } + + override fun hashCode(): Int { + var hash = 17 + hash = 31 * hash + name.hashCode() + hash = 31 * hash + value.hashCode() + hash = 31 * hash + domain.hashCode() + hash = 31 * hash + path.hashCode() + hash = 31 * hash + (expiresAt xor expiresAt.ushr(32)).toInt() + hash = 31 * hash + if (secure) 0 else 1 + hash = 31 * hash + if (httpOnly) 0 else 1 + hash = 31 * hash + if (persistent) 0 else 1 + hash = 31 * hash + if (hostOnly) 0 else 1 + return hash + } + + companion object { + private val YEAR_PATTERN = Pattern.compile("(\\d{2,4})[^\\d]*") + private val MONTH_PATTERN = + Pattern.compile("(?i)(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec).*") + private val DAY_OF_MONTH_PATTERN = Pattern.compile("(\\d{1,2})[^\\d]*") + private val TIME_PATTERN = Pattern.compile("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})[^\\d]*") + + private fun domainMatch(urlHost: String, domain: String): Boolean { + if (urlHost == domain) { + return true // As in 'example.com' matching 'example.com'. + } + + return urlHost.endsWith(domain) + && urlHost[urlHost.length - domain.length - 1] == '.' + && !verifyAsIpAddress(urlHost) + } + + private fun pathMatch(url: HttpUrl, path: String): Boolean { + val urlPath = url.encodedPath() + + if (urlPath == path) { + return true // As in '/foo' matching '/foo'. + } + + if (urlPath.startsWith(path)) { + if (path.endsWith("/")) return true // As in '/' matching '/foo'. + if (urlPath[path.length] == '/') return true // As in '/foo' matching '/foo/bar'. + } + + return false + } + + /** + * Attempt to parse a `Set-Cookie` HTTP header value `setCookie` as a cookie. Returns null if + * `setCookie` is not a well-formed cookie. + */ + @JvmStatic + fun parse(url: HttpUrl, setCookie: String): Cookie? = + parse(System.currentTimeMillis(), url, setCookie) + + internal fun parse(currentTimeMillis: Long, url: HttpUrl, setCookie: String): Cookie? { + var pos = 0 + val limit = setCookie.length + val cookiePairEnd = delimiterOffset(setCookie, pos, limit, ';') + + val pairEqualsSign = delimiterOffset(setCookie, pos, cookiePairEnd, '=') + if (pairEqualsSign == cookiePairEnd) return null + + val cookieName = trimSubstring(setCookie, pos, pairEqualsSign) + if (cookieName.isEmpty() || indexOfControlOrNonAscii(cookieName) != -1) return null + + val cookieValue = trimSubstring(setCookie, pairEqualsSign + 1, cookiePairEnd) + if (indexOfControlOrNonAscii(cookieValue) != -1) return null + + var expiresAt = HttpDate.MAX_DATE + var deltaSeconds = -1L + var domain: String? = null + var path: String? = null + var secureOnly = false + var httpOnly = false + var hostOnly = true + var persistent = false + + pos = cookiePairEnd + 1 + while (pos < limit) { + val attributePairEnd = delimiterOffset(setCookie, pos, limit, ';') + + val attributeEqualsSign = delimiterOffset(setCookie, pos, attributePairEnd, '=') + val attributeName = trimSubstring(setCookie, pos, attributeEqualsSign) + val attributeValue = if (attributeEqualsSign < attributePairEnd) { + trimSubstring(setCookie, attributeEqualsSign + 1, attributePairEnd) + } else { + "" + } + + when { + attributeName.equals("expires", ignoreCase = true) -> { + try { + expiresAt = parseExpires(attributeValue, 0, attributeValue.length) + persistent = true + } catch (e: IllegalArgumentException) { + // Ignore this attribute, it isn't recognizable as a date. + } + } + attributeName.equals("max-age", ignoreCase = true) -> { + try { + deltaSeconds = parseMaxAge(attributeValue) + persistent = true + } catch (e: NumberFormatException) { + // Ignore this attribute, it isn't recognizable as a max age. + } + } + attributeName.equals("domain", ignoreCase = true) -> { + try { + domain = parseDomain(attributeValue) + hostOnly = false + } catch (e: IllegalArgumentException) { + // Ignore this attribute, it isn't recognizable as a domain. + } + } + attributeName.equals("path", ignoreCase = true) -> { + path = attributeValue + } + attributeName.equals("secure", ignoreCase = true) -> { + secureOnly = true + } + attributeName.equals("httponly", ignoreCase = true) -> { + httpOnly = true + } + } + + pos = attributePairEnd + 1 + } + + // If 'Max-Age' is present, it takes precedence over 'Expires', regardless of the order the two + // attributes are declared in the cookie string. + if (deltaSeconds == Long.MIN_VALUE) { + expiresAt = Long.MIN_VALUE + } else if (deltaSeconds != -1L) { + val deltaMilliseconds = if (deltaSeconds <= Long.MAX_VALUE / 1000) { + deltaSeconds * 1000 + } else { + Long.MAX_VALUE + } + expiresAt = currentTimeMillis + deltaMilliseconds + if (expiresAt < currentTimeMillis || expiresAt > HttpDate.MAX_DATE) { + expiresAt = HttpDate.MAX_DATE // Handle overflow & limit the date range. + } + } + + // If the domain is present, it must domain match. Otherwise we have a host-only cookie. + val urlHost = url.host() + if (domain == null) { + domain = urlHost + } else if (!domainMatch(urlHost, domain)) { + return null // No domain match? This is either incompetence or malice! + } + + // If the domain is a suffix of the url host, it must not be a public suffix. + if (urlHost.length != domain.length + && PublicSuffixDatabase.get().getEffectiveTldPlusOne(domain) == null) { + return null + } + + // 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("/")) { + val encodedPath = url.encodedPath() + val lastSlash = encodedPath.lastIndexOf('/') + path = if (lastSlash != 0) encodedPath.substring(0, lastSlash) else "/" + } + + return Cookie(cookieName, cookieValue, expiresAt, domain, path, secureOnly, httpOnly, + persistent, hostOnly) + } + + /** Parse a date as specified in RFC 6265, section 5.1.1. */ + private fun parseExpires(s: String, pos: Int, limit: Int): Long { + var pos = pos + pos = dateCharacterOffset(s, pos, limit, false) + + var hour = -1 + var minute = -1 + var second = -1 + var dayOfMonth = -1 + var month = -1 + var year = -1 + val matcher = TIME_PATTERN.matcher(s) + + while (pos < limit) { + val end = dateCharacterOffset(s, pos + 1, limit, true) + matcher.region(pos, end) + + when { + hour == -1 && matcher.usePattern(TIME_PATTERN).matches() -> { + hour = Integer.parseInt(matcher.group(1)) + minute = Integer.parseInt(matcher.group(2)) + second = Integer.parseInt(matcher.group(3)) + } + dayOfMonth == -1 && matcher.usePattern(DAY_OF_MONTH_PATTERN).matches() -> { + dayOfMonth = Integer.parseInt(matcher.group(1)) + } + month == -1 && matcher.usePattern(MONTH_PATTERN).matches() -> { + val monthString = matcher.group(1).toLowerCase(Locale.US) + month = MONTH_PATTERN.pattern().indexOf(monthString) / 4 // Sneaky! jan=1, dec=12. + } + year == -1 && matcher.usePattern(YEAR_PATTERN).matches() -> { + year = Integer.parseInt(matcher.group(1)) + } + } + + pos = dateCharacterOffset(s, end + 1, limit, false) + } + + // Convert two-digit years into four-digit years. 99 becomes 1999, 15 becomes 2015. + if (year in 70..99) year += 1900 + if (year in 0..69) year += 2000 + + // If any partial is omitted or out of range, return -1. The date is impossible. Note that leap + // seconds are not supported by this syntax. + require(year >= 1601) + require(month != -1) + require(dayOfMonth in 1..31) + require(hour in 0..23) + require(minute in 0..59) + require(second in 0..59) + + GregorianCalendar(UTC).apply { + isLenient = false + set(Calendar.YEAR, year) + set(Calendar.MONTH, month - 1) + set(Calendar.DAY_OF_MONTH, dayOfMonth) + set(Calendar.HOUR_OF_DAY, hour) + set(Calendar.MINUTE, minute) + set(Calendar.SECOND, second) + set(Calendar.MILLISECOND, 0) + return timeInMillis + } + } + + /** + * Returns the index of the next date character in `input`, or if `invert` the index + * of the next non-date character in `input`. + */ + private fun dateCharacterOffset(input: String, pos: Int, limit: Int, invert: Boolean): Int { + for (i in pos until limit) { + val c = input[i].toInt() + val dateCharacter = (c < ' '.toInt() && c != '\t'.toInt() || c >= '\u007f'.toInt() + || c in '0'.toInt()..'9'.toInt() + || c in 'a'.toInt()..'z'.toInt() + || c in 'A'.toInt()..'Z'.toInt() + || c == ':'.toInt()) + if (dateCharacter == !invert) return i + } + return limit + } + + /** + * Returns the positive value if `attributeValue` is positive, or [Long.MIN_VALUE] if it is + * either 0 or negative. If the value is positive but out of range, this returns + * [Long.MAX_VALUE]. + * + * @throws NumberFormatException if `s` is not an integer of any precision. + */ + private fun parseMaxAge(s: String): Long { + try { + val parsed = s.toLong() + return if (parsed <= 0L) Long.MIN_VALUE else parsed + } catch (e: NumberFormatException) { + // Check if the value is an integer (positive or negative) that's too big for a long. + if (s.matches("-?\\d+".toRegex())) { + return if (s.startsWith("-")) Long.MIN_VALUE else Long.MAX_VALUE + } + throw e + } + } + + /** + * Returns a domain string like `example.com` for an input domain like `EXAMPLE.COM` + * or `.example.com`. + */ + private fun parseDomain(s: String): String { + require(!s.endsWith(".")) + return canonicalizeHost(s.removePrefix(".")) ?: throw IllegalArgumentException() + } + + /** Returns all of the cookies from a set of HTTP response headers. */ + @JvmStatic + fun parseAll(url: HttpUrl, headers: Headers): List { + val cookieStrings = headers.values("Set-Cookie") + var cookies: MutableList? = null + + for (i in 0 until cookieStrings.size) { + val cookie = Cookie.parse(url, cookieStrings[i]) ?: continue + if (cookies == null) cookies = mutableListOf() + cookies.add(cookie) + } + + return if (cookies != null) { + Collections.unmodifiableList(cookies) + } else { + emptyList() + } + } + } +} diff --git a/okhttp/src/main/java/okhttp3/internal/InternalKt.kt b/okhttp/src/main/java/okhttp3/internal/InternalKt.kt new file mode 100644 index 000000000..cf41f9ef4 --- /dev/null +++ b/okhttp/src/main/java/okhttp3/internal/InternalKt.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.internal + +import okhttp3.Cookie +import okhttp3.HttpUrl + +fun parseCookie(currentTimeMillis: Long, url: HttpUrl, setCookie: String): Cookie? = + Cookie.parse(currentTimeMillis, url, setCookie) + +fun cookieToString(cookie: Cookie, forObsoleteRfc2965: Boolean) = + cookie.toString(forObsoleteRfc2965)