1
0
mirror of https://github.com/square/okhttp.git synced 2025-11-29 06:23:09 +03:00

Convert Cookie to Kotlin

This commit is contained in:
Jesse Wilson
2019-03-24 21:22:07 -05:00
parent 44b8c7c25a
commit 0ece6afccc
7 changed files with 740 additions and 755 deletions

View File

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

View File

@@ -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<Cookie> cookies) {
if (cookieHandler != null) {
List<String> cookieStrings = new ArrayList<>();
for (Cookie cookie : cookies) {
cookieStrings.add(cookie.toString(true));
}
Map<String, List<String>> 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<Cookie> loadForRequest(HttpUrl url) {
// The RI passes all headers. We don't have 'em, so we don't pass 'em!
Map<String, List<String>> headers = Collections.emptyMap();
Map<String, List<String>> 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<Cookie> cookies = null;
for (Map.Entry<String, List<String>> 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<Cookie> decodeHeaderAsJavaNetCookies(HttpUrl url, String header) {
List<Cookie> 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;
}
}

View File

@@ -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<Cookie>) {
val cookieStrings = mutableListOf<String>()
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<Cookie> {
val cookieHeaders = try {
// The RI passes all headers. We don't have 'em, so we don't pass 'em!
cookieHandler.get(url.uri(), emptyMap<String, List<String>>())
} catch (e: IOException) {
Platform.get().log(WARN, "Loading cookies failed for " + url.resolve("/...")!!, e)
return emptyList()
}
var cookies: MutableList<Cookie>? = 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<Cookie> {
val result = mutableListOf<Cookie>()
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
}
}

View File

@@ -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)',

View File

@@ -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 <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]*");
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.
*
* <p>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.
*
* <p>For example, suppose the cookie's domain is {@code example.com}. If this flag is true it
* matches <strong>only</strong> {@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<Cookie> parseAll(HttpUrl url, Headers headers) {
List<String> cookieStrings = headers.values("Set-Cookie");
List<Cookie> 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;
}
}

View File

@@ -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<Cookie> {
val cookieStrings = headers.values("Set-Cookie")
var cookies: MutableList<Cookie>? = 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()
}
}
}
}

View File

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