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:
@@ -17,7 +17,6 @@ package okhttp3;
|
|||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -26,6 +25,7 @@ import okhttp3.internal.http.HttpDate;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
|
import static okhttp3.internal.InternalKtKt.parseCookie;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
@@ -93,31 +93,31 @@ public final class CookieTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test public void maxAge() throws Exception {
|
@Test public void maxAge() throws Exception {
|
||||||
assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=1").expiresAt()).isEqualTo(51000L);
|
assertThat(parseCookie(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=9223372036854724").expiresAt()).isEqualTo(
|
||||||
HttpDate.MAX_DATE);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
HttpDate.MAX_DATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void maxAgeNonPositive() throws Exception {
|
@Test public void maxAgeNonPositive() throws Exception {
|
||||||
assertThat(Cookie.parse(50000L, url, "a=b; Max-Age=-1").expiresAt()).isEqualTo(Long.MIN_VALUE);
|
assertThat(parseCookie(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(parseCookie(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=-9223372036854775808").expiresAt()).isEqualTo(
|
||||||
Long.MIN_VALUE);
|
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);
|
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);
|
Long.MIN_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,30 +356,30 @@ public final class CookieTest {
|
|||||||
|
|
||||||
@Test public void maxAgeTakesPrecedenceOverExpires() throws Exception {
|
@Test public void maxAgeTakesPrecedenceOverExpires() throws Exception {
|
||||||
// Max-Age = 1, Expires = 2. In either order.
|
// 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(
|
0L, url, "a=b; Max-Age=1; Expires=Thu, 01 Jan 1970 00:00:02 GMT").expiresAt()).isEqualTo(
|
||||||
1000L);
|
1000L);
|
||||||
assertThat(Cookie.parse(
|
assertThat(parseCookie(
|
||||||
0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:02 GMT; Max-Age=1").expiresAt()).isEqualTo(
|
0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:02 GMT; Max-Age=1").expiresAt()).isEqualTo(
|
||||||
1000L);
|
1000L);
|
||||||
// Max-Age = 2, Expires = 1. In either order.
|
// 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(
|
0L, url, "a=b; Max-Age=2; Expires=Thu, 01 Jan 1970 00:00:01 GMT").expiresAt()).isEqualTo(
|
||||||
2000L);
|
2000L);
|
||||||
assertThat(Cookie.parse(
|
assertThat(parseCookie(
|
||||||
0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=2").expiresAt()).isEqualTo(
|
0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=2").expiresAt()).isEqualTo(
|
||||||
2000L);
|
2000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** If a cookie incorrectly defines multiple 'Max-Age' attributes, the last one defined wins. */
|
/** If a cookie incorrectly defines multiple 'Max-Age' attributes, the last one defined wins. */
|
||||||
@Test public void lastMaxAgeWins() throws Exception {
|
@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);
|
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. */
|
/** If a cookie incorrectly defines multiple 'Expires' attributes, the last one defined wins. */
|
||||||
@Test public void lastExpiresAtWins() throws Exception {
|
@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:02 GMT; "
|
||||||
+ "Expires=Thu, 01 Jan 1970 00:00:04 GMT; "
|
+ "Expires=Thu, 01 Jan 1970 00:00:04 GMT; "
|
||||||
+ "Expires=Thu, 01 Jan 1970 00:00:01 GMT; "
|
+ "Expires=Thu, 01 Jan 1970 00:00:01 GMT; "
|
||||||
@@ -387,9 +387,9 @@ public final class CookieTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test public void maxAgeOrExpiresMakesCookiePersistent() throws Exception {
|
@Test public void maxAgeOrExpiresMakesCookiePersistent() throws Exception {
|
||||||
assertThat(Cookie.parse(0L, url, "a=b").persistent()).isFalse();
|
assertThat(parseCookie(0L, url, "a=b").persistent()).isFalse();
|
||||||
assertThat(Cookie.parse(0L, url, "a=b; Max-Age=1").persistent()).isTrue();
|
assertThat(parseCookie(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; Expires=Thu, 01 Jan 1970 00:00:01 GMT").persistent()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void parseAll() throws Exception {
|
@Test public void parseAll() throws Exception {
|
||||||
@@ -424,7 +424,7 @@ public final class CookieTest {
|
|||||||
try {
|
try {
|
||||||
new Cookie.Builder().name(null);
|
new Cookie.Builder().name(null);
|
||||||
fail();
|
fail();
|
||||||
} catch (NullPointerException expected) {
|
} catch (IllegalArgumentException expected) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
new Cookie.Builder().name(" a ");
|
new Cookie.Builder().name(" a ");
|
||||||
@@ -437,7 +437,7 @@ public final class CookieTest {
|
|||||||
try {
|
try {
|
||||||
new Cookie.Builder().value(null);
|
new Cookie.Builder().value(null);
|
||||||
fail();
|
fail();
|
||||||
} catch (NullPointerException expected) {
|
} catch (IllegalArgumentException expected) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
new Cookie.Builder().value(" b ");
|
new Cookie.Builder().value(" b ");
|
||||||
@@ -482,7 +482,7 @@ public final class CookieTest {
|
|||||||
try {
|
try {
|
||||||
new Cookie.Builder().hostOnlyDomain(null);
|
new Cookie.Builder().hostOnlyDomain(null);
|
||||||
fail();
|
fail();
|
||||||
} catch (NullPointerException expected) {
|
} catch (IllegalArgumentException expected) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
new Cookie.Builder().hostOnlyDomain("a/b");
|
new Cookie.Builder().hostOnlyDomain("a/b");
|
||||||
@@ -515,7 +515,7 @@ public final class CookieTest {
|
|||||||
try {
|
try {
|
||||||
new Cookie.Builder().path(null);
|
new Cookie.Builder().path(null);
|
||||||
fail();
|
fail();
|
||||||
} catch (NullPointerException expected) {
|
} catch (IllegalArgumentException expected) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
new Cookie.Builder().path("foo");
|
new Cookie.Builder().path("foo");
|
||||||
@@ -564,9 +564,9 @@ public final class CookieTest {
|
|||||||
"a=b; Path=/c; Domain=example.com; Max-Age=5; Secure; "
|
"a=b; Path=/c; Domain=example.com; Max-Age=5; Secure; "
|
||||||
);
|
);
|
||||||
for (String stringA : cookieStrings) {
|
for (String stringA : cookieStrings) {
|
||||||
Cookie cookieA = Cookie.parse(0, url, stringA);
|
Cookie cookieA = parseCookie(0, url, stringA);
|
||||||
for (String stringB : cookieStrings) {
|
for (String stringB : cookieStrings) {
|
||||||
Cookie cookieB = Cookie.parse(0, url, stringB);
|
Cookie cookieB = parseCookie(0, url, stringB);
|
||||||
if (Objects.equals(stringA, stringB)) {
|
if (Objects.equals(stringA, stringB)) {
|
||||||
assertThat(cookieB.hashCode()).isEqualTo(cookieA.hashCode());
|
assertThat(cookieB.hashCode()).isEqualTo(cookieA.hashCode());
|
||||||
assertThat(cookieB).isEqualTo(cookieA);
|
assertThat(cookieB).isEqualTo(cookieA);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
111
okhttp-urlconnection/src/main/java/okhttp3/JavaNetCookieJar.kt
Normal file
111
okhttp-urlconnection/src/main/java/okhttp3/JavaNetCookieJar.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,25 @@ task japicmp(type: me.champeau.gradle.japicmp.JapicmpTask, dependsOn: 'jar') {
|
|||||||
'okhttp3.ConnectionPool#connectionCount()',
|
'okhttp3.ConnectionPool#connectionCount()',
|
||||||
'okhttp3.ConnectionPool#evictAll()',
|
'okhttp3.ConnectionPool#evictAll()',
|
||||||
'okhttp3.ConnectionPool#idleConnectionCount()',
|
'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#encodedName(int)',
|
||||||
'okhttp3.FormBody#encodedValue(int)',
|
'okhttp3.FormBody#encodedValue(int)',
|
||||||
'okhttp3.FormBody#name(int)',
|
'okhttp3.FormBody#name(int)',
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
556
okhttp/src/main/java/okhttp3/Cookie.kt
Normal file
556
okhttp/src/main/java/okhttp3/Cookie.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
okhttp/src/main/java/okhttp3/internal/InternalKt.kt
Normal file
25
okhttp/src/main/java/okhttp3/internal/InternalKt.kt
Normal 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)
|
||||||
Reference in New Issue
Block a user