mirror of
https://github.com/square/okhttp.git
synced 2025-11-26 06:43:09 +03:00
Convert HttpHeaders to Kotlin (#5016)
* Rename HttpHeaders.java to .kt * Convert HttpHeaders to Kotlin
This commit is contained in:
@@ -19,7 +19,7 @@ import okhttp3.Headers
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.internal.http.HttpHeaders
|
import okhttp3.internal.http.promisesBody
|
||||||
import okhttp3.internal.platform.Platform
|
import okhttp3.internal.platform.Platform
|
||||||
import okhttp3.internal.platform.Platform.Companion.INFO
|
import okhttp3.internal.platform.Platform.Companion.INFO
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
@@ -235,7 +235,7 @@ class HttpLoggingInterceptor @JvmOverloads constructor(
|
|||||||
logHeader(headers, i)
|
logHeader(headers, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!logBody || !HttpHeaders.hasBody(response)) {
|
if (!logBody || !response.promisesBody()) {
|
||||||
logger.log("<-- END HTTP")
|
logger.log("<-- END HTTP")
|
||||||
} else if (bodyHasUnknownEncoding(response.headers())) {
|
} else if (bodyHasUnknownEncoding(response.headers())) {
|
||||||
logger.log("<-- END HTTP (encoded body omitted)")
|
logger.log("<-- END HTTP (encoded body omitted)")
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package okhttp3
|
package okhttp3
|
||||||
|
|
||||||
|
import okhttp3.internal.Util
|
||||||
import okhttp3.internal.Util.closeQuietly
|
import okhttp3.internal.Util.closeQuietly
|
||||||
import okhttp3.internal.addHeaderLenient
|
import okhttp3.internal.addHeaderLenient
|
||||||
import okhttp3.internal.cache.CacheRequest
|
import okhttp3.internal.cache.CacheRequest
|
||||||
import okhttp3.internal.cache.CacheStrategy
|
import okhttp3.internal.cache.CacheStrategy
|
||||||
import okhttp3.internal.cache.DiskLruCache
|
import okhttp3.internal.cache.DiskLruCache
|
||||||
import okhttp3.internal.cache.InternalCache
|
import okhttp3.internal.cache.InternalCache
|
||||||
import okhttp3.internal.http.HttpHeaders
|
|
||||||
import okhttp3.internal.http.HttpMethod
|
import okhttp3.internal.http.HttpMethod
|
||||||
import okhttp3.internal.http.StatusLine
|
import okhttp3.internal.http.StatusLine
|
||||||
import okhttp3.internal.io.FileSystem
|
import okhttp3.internal.io.FileSystem
|
||||||
@@ -47,6 +47,7 @@ import java.security.cert.CertificateException
|
|||||||
import java.security.cert.CertificateFactory
|
import java.security.cert.CertificateFactory
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import java.util.NoSuchElementException
|
import java.util.NoSuchElementException
|
||||||
|
import java.util.TreeSet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and
|
* Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and
|
||||||
@@ -228,7 +229,7 @@ class Cache internal constructor(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HttpHeaders.hasVaryAll(response)) {
|
if (response.hasVaryAll()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,7 +560,7 @@ class Cache internal constructor(
|
|||||||
|
|
||||||
internal constructor(response: Response) {
|
internal constructor(response: Response) {
|
||||||
this.url = response.request().url().toString()
|
this.url = response.request().url().toString()
|
||||||
this.varyHeaders = HttpHeaders.varyHeaders(response)
|
this.varyHeaders = response.varyHeaders()
|
||||||
this.requestMethod = response.request().method()
|
this.requestMethod = response.request().method()
|
||||||
this.protocol = response.protocol()
|
this.protocol = response.protocol()
|
||||||
this.code = response.code()
|
this.code = response.code()
|
||||||
@@ -647,7 +648,7 @@ class Cache internal constructor(
|
|||||||
fun matches(request: Request, response: Response): Boolean {
|
fun matches(request: Request, response: Response): Boolean {
|
||||||
return url == request.url().toString() &&
|
return url == request.url().toString() &&
|
||||||
requestMethod == request.method() &&
|
requestMethod == request.method() &&
|
||||||
HttpHeaders.varyMatches(response, varyHeaders, request)
|
varyMatches(response, varyHeaders, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun response(snapshot: DiskLruCache.Snapshot): Response {
|
fun response(snapshot: DiskLruCache.Snapshot): Response {
|
||||||
@@ -736,5 +737,76 @@ class Cache internal constructor(
|
|||||||
throw IOException(e.message)
|
throw IOException(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if none of the Vary headers have changed between [cachedRequest] and
|
||||||
|
* [newRequest].
|
||||||
|
*/
|
||||||
|
fun varyMatches(
|
||||||
|
cachedResponse: Response,
|
||||||
|
cachedRequest: Headers,
|
||||||
|
newRequest: Request
|
||||||
|
): Boolean {
|
||||||
|
return cachedResponse.headers().varyFields().none {
|
||||||
|
cachedRequest.values(it) != newRequest.headers(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if a Vary header contains an asterisk. Such responses cannot be cached. */
|
||||||
|
fun Response.hasVaryAll(): Boolean {
|
||||||
|
val responseHeaders = headers()
|
||||||
|
val varyFields = responseHeaders.varyFields()
|
||||||
|
return varyFields.contains("*")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the names of the request headers that need to be checked for equality when caching.
|
||||||
|
*/
|
||||||
|
private fun Headers.varyFields(): Set<String> {
|
||||||
|
var result: MutableSet<String>? = null
|
||||||
|
for (i in 0 until size()) {
|
||||||
|
if (!"Vary".equals(name(i), ignoreCase = true)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val value = value(i)
|
||||||
|
if (result == null) {
|
||||||
|
result = TreeSet(String.CASE_INSENSITIVE_ORDER)
|
||||||
|
}
|
||||||
|
for (varyField in value.split(',')) {
|
||||||
|
result.add(varyField.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result ?: emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the subset of the headers in this's request that impact the content of this's body.
|
||||||
|
*/
|
||||||
|
fun Response.varyHeaders(): Headers {
|
||||||
|
// Use the request headers sent over the network, since that's what the response varies on.
|
||||||
|
// Otherwise OkHttp-supplied headers like "Accept-Encoding: gzip" may be lost.
|
||||||
|
val requestHeaders = networkResponse()!!.request().headers()
|
||||||
|
val responseHeaders = headers()
|
||||||
|
return varyHeaders(requestHeaders, responseHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the subset of the headers in [requestHeaders] that impact the content of the
|
||||||
|
* response's body.
|
||||||
|
*/
|
||||||
|
private fun varyHeaders(requestHeaders: Headers, responseHeaders: Headers): Headers {
|
||||||
|
val varyFields = responseHeaders.varyFields()
|
||||||
|
if (varyFields.isEmpty()) return Util.EMPTY_HEADERS
|
||||||
|
|
||||||
|
val result = Headers.Builder()
|
||||||
|
for (i in 0 until requestHeaders.size()) {
|
||||||
|
val fieldName = requestHeaders.name(i)
|
||||||
|
if (varyFields.contains(fieldName)) {
|
||||||
|
result.add(fieldName, requestHeaders.value(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package okhttp3
|
package okhttp3
|
||||||
|
|
||||||
import okhttp3.internal.http.HttpHeaders
|
import okhttp3.internal.indexOfNonWhitespace
|
||||||
|
import okhttp3.internal.toNonNegativeInt
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -261,7 +262,7 @@ class CacheControl private constructor(
|
|||||||
var pos = 0
|
var pos = 0
|
||||||
while (pos < value.length) {
|
while (pos < value.length) {
|
||||||
val tokenStart = pos
|
val tokenStart = pos
|
||||||
pos = HttpHeaders.skipUntil(value, pos, "=,;")
|
pos = value.indexOfElement("=,;", pos)
|
||||||
val directive = value.substring(tokenStart, pos).trim { it <= ' ' }
|
val directive = value.substring(tokenStart, pos).trim { it <= ' ' }
|
||||||
val parameter: String?
|
val parameter: String?
|
||||||
|
|
||||||
@@ -270,19 +271,19 @@ class CacheControl private constructor(
|
|||||||
parameter = null
|
parameter = null
|
||||||
} else {
|
} else {
|
||||||
pos++ // Consume '='.
|
pos++ // Consume '='.
|
||||||
pos = HttpHeaders.skipWhitespace(value, pos)
|
pos = value.indexOfNonWhitespace(pos)
|
||||||
|
|
||||||
if (pos < value.length && value[pos] == '\"') {
|
if (pos < value.length && value[pos] == '\"') {
|
||||||
// Quoted string.
|
// Quoted string.
|
||||||
pos++ // Consume '"' open quote.
|
pos++ // Consume '"' open quote.
|
||||||
val parameterStart = pos
|
val parameterStart = pos
|
||||||
pos = HttpHeaders.skipUntil(value, pos, "\"")
|
pos = value.indexOfElement("\"", pos)
|
||||||
parameter = value.substring(parameterStart, pos)
|
parameter = value.substring(parameterStart, pos)
|
||||||
pos++ // Consume '"' close quote (if necessary).
|
pos++ // Consume '"' close quote (if necessary).
|
||||||
} else {
|
} else {
|
||||||
// Unquoted string.
|
// Unquoted string.
|
||||||
val parameterStart = pos
|
val parameterStart = pos
|
||||||
pos = HttpHeaders.skipUntil(value, pos, ",;")
|
pos = value.indexOfElement(",;", pos)
|
||||||
parameter = value.substring(parameterStart, pos).trim { it <= ' ' }
|
parameter = value.substring(parameterStart, pos).trim { it <= ' ' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,10 +296,10 @@ class CacheControl private constructor(
|
|||||||
noStore = true
|
noStore = true
|
||||||
}
|
}
|
||||||
"max-age".equals(directive, ignoreCase = true) -> {
|
"max-age".equals(directive, ignoreCase = true) -> {
|
||||||
maxAgeSeconds = HttpHeaders.parseSeconds(parameter, -1)
|
maxAgeSeconds = parameter.toNonNegativeInt(-1)
|
||||||
}
|
}
|
||||||
"s-maxage".equals(directive, ignoreCase = true) -> {
|
"s-maxage".equals(directive, ignoreCase = true) -> {
|
||||||
sMaxAgeSeconds = HttpHeaders.parseSeconds(parameter, -1)
|
sMaxAgeSeconds = parameter.toNonNegativeInt(-1)
|
||||||
}
|
}
|
||||||
"private".equals(directive, ignoreCase = true) -> {
|
"private".equals(directive, ignoreCase = true) -> {
|
||||||
isPrivate = true
|
isPrivate = true
|
||||||
@@ -310,10 +311,10 @@ class CacheControl private constructor(
|
|||||||
mustRevalidate = true
|
mustRevalidate = true
|
||||||
}
|
}
|
||||||
"max-stale".equals(directive, ignoreCase = true) -> {
|
"max-stale".equals(directive, ignoreCase = true) -> {
|
||||||
maxStaleSeconds = HttpHeaders.parseSeconds(parameter, Integer.MAX_VALUE)
|
maxStaleSeconds = parameter.toNonNegativeInt(Integer.MAX_VALUE)
|
||||||
}
|
}
|
||||||
"min-fresh".equals(directive, ignoreCase = true) -> {
|
"min-fresh".equals(directive, ignoreCase = true) -> {
|
||||||
minFreshSeconds = HttpHeaders.parseSeconds(parameter, -1)
|
minFreshSeconds = parameter.toNonNegativeInt(-1)
|
||||||
}
|
}
|
||||||
"only-if-cached".equals(directive, ignoreCase = true) -> {
|
"only-if-cached".equals(directive, ignoreCase = true) -> {
|
||||||
onlyIfCached = true
|
onlyIfCached = true
|
||||||
@@ -336,5 +337,18 @@ class CacheControl private constructor(
|
|||||||
mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform, immutable,
|
mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform, immutable,
|
||||||
headerValue)
|
headerValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next index in this at or after [startIndex] that is a character from
|
||||||
|
* [characters]. Returns the input length if none of the requested characters can be found.
|
||||||
|
*/
|
||||||
|
private fun String.indexOfElement(characters: String, startIndex: Int = 0): Int {
|
||||||
|
for (i in startIndex until length) {
|
||||||
|
if (characters.contains(this[i])) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return length
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package okhttp3
|
package okhttp3
|
||||||
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.IOException
|
|
||||||
import okhttp3.internal.connection.Exchange
|
import okhttp3.internal.connection.Exchange
|
||||||
import okhttp3.internal.http.HttpHeaders
|
|
||||||
import okhttp3.internal.http.StatusLine.Companion.HTTP_PERM_REDIRECT
|
import okhttp3.internal.http.StatusLine.Companion.HTTP_PERM_REDIRECT
|
||||||
import okhttp3.internal.http.StatusLine.Companion.HTTP_TEMP_REDIRECT
|
import okhttp3.internal.http.StatusLine.Companion.HTTP_TEMP_REDIRECT
|
||||||
|
import okhttp3.internal.http.parseChallenges
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.IOException
|
||||||
import java.net.HttpURLConnection.HTTP_MOVED_PERM
|
import java.net.HttpURLConnection.HTTP_MOVED_PERM
|
||||||
import java.net.HttpURLConnection.HTTP_MOVED_TEMP
|
import java.net.HttpURLConnection.HTTP_MOVED_TEMP
|
||||||
import java.net.HttpURLConnection.HTTP_MULT_CHOICE
|
import java.net.HttpURLConnection.HTTP_MULT_CHOICE
|
||||||
@@ -177,8 +176,7 @@ class Response internal constructor(
|
|||||||
* auth param, this is up to the caller that interprets these challenges.
|
* auth param, this is up to the caller that interprets these challenges.
|
||||||
*/
|
*/
|
||||||
fun challenges(): List<Challenge> {
|
fun challenges(): List<Challenge> {
|
||||||
return HttpHeaders.parseChallenges(
|
return headers().parseChallenges(
|
||||||
headers(),
|
|
||||||
when (code) {
|
when (code) {
|
||||||
HTTP_UNAUTHORIZED -> "WWW-Authenticate"
|
HTTP_UNAUTHORIZED -> "WWW-Authenticate"
|
||||||
HTTP_PROXY_AUTH -> "Proxy-Authenticate"
|
HTTP_PROXY_AUTH -> "Proxy-Authenticate"
|
||||||
|
|||||||
@@ -13,8 +13,11 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
@file:JvmName("UtilKt")
|
||||||
package okhttp3.internal
|
package okhttp3.internal
|
||||||
|
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.Buffer
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -70,3 +73,56 @@ inline fun Executor.tryExecute(name: String, crossinline block: () -> Unit) {
|
|||||||
} catch (_: RejectedExecutionException) {
|
} catch (_: RejectedExecutionException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Buffer.skipAll(b: Byte): Int {
|
||||||
|
var count = 0
|
||||||
|
while (!exhausted() && this[0] == b) {
|
||||||
|
count++
|
||||||
|
readByte()
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the next non-whitespace character in this. Result is undefined if input
|
||||||
|
* contains newline characters.
|
||||||
|
*/
|
||||||
|
fun String.indexOfNonWhitespace(startIndex: Int = 0): Int {
|
||||||
|
for (i in startIndex until length) {
|
||||||
|
val c = this[i]
|
||||||
|
if (c != ' ' && c != '\t') {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the Content-Length as reported by the response headers. */
|
||||||
|
fun Response.headersContentLength(): Long {
|
||||||
|
return headers()["Content-Length"]?.toLongOrDefault(-1L) ?: -1L
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toLongOrDefault(defaultValue: Long): Long {
|
||||||
|
return try {
|
||||||
|
toLong()
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this as a non-negative integer, or 0 if it is negative, or [Int.MAX_VALUE] if it is too
|
||||||
|
* large, or [defaultValue] if it cannot be parsed.
|
||||||
|
*/
|
||||||
|
fun String?.toNonNegativeInt(defaultValue: Int): Int {
|
||||||
|
try {
|
||||||
|
val value = this?.toLong() ?: return defaultValue
|
||||||
|
return when {
|
||||||
|
value > Int.MAX_VALUE -> Int.MAX_VALUE
|
||||||
|
value < 0 -> 0
|
||||||
|
else -> value.toInt()
|
||||||
|
}
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,26 +16,26 @@
|
|||||||
*/
|
*/
|
||||||
package okhttp3.internal.cache
|
package okhttp3.internal.cache
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.internal.Util
|
import okhttp3.internal.Util
|
||||||
import okhttp3.internal.http.ExchangeCodec
|
|
||||||
import okhttp3.internal.http.HttpHeaders
|
|
||||||
import okhttp3.internal.http.HttpMethod
|
|
||||||
import okhttp3.internal.http.RealResponseBody
|
|
||||||
import okio.Buffer
|
|
||||||
import okio.Source
|
|
||||||
import okio.Timeout
|
|
||||||
import java.net.HttpURLConnection.HTTP_NOT_MODIFIED
|
|
||||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
|
||||||
import okhttp3.internal.Util.closeQuietly
|
import okhttp3.internal.Util.closeQuietly
|
||||||
import okhttp3.internal.Util.discard
|
import okhttp3.internal.Util.discard
|
||||||
import okhttp3.internal.addHeaderLenient
|
import okhttp3.internal.addHeaderLenient
|
||||||
|
import okhttp3.internal.http.ExchangeCodec
|
||||||
|
import okhttp3.internal.http.HttpMethod
|
||||||
|
import okhttp3.internal.http.RealResponseBody
|
||||||
|
import okhttp3.internal.http.promisesBody
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.Source
|
||||||
|
import okio.Timeout
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
import java.io.IOException
|
||||||
import java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT
|
import java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT
|
||||||
|
import java.net.HttpURLConnection.HTTP_NOT_MODIFIED
|
||||||
|
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||||
|
|
||||||
/** Serves requests from the cache and writes responses to the cache. */
|
/** Serves requests from the cache and writes responses to the cache. */
|
||||||
class CacheInterceptor(internal val cache: InternalCache?) : Interceptor {
|
class CacheInterceptor(internal val cache: InternalCache?) : Interceptor {
|
||||||
@@ -118,7 +118,7 @@ class CacheInterceptor(internal val cache: InternalCache?) : Interceptor {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
if (cache != null) {
|
if (cache != null) {
|
||||||
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
|
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
|
||||||
// Offer this request to the cache.
|
// Offer this request to the cache.
|
||||||
val cacheRequest = cache.put(response)
|
val cacheRequest = cache.put(response)
|
||||||
return cacheWritingResponse(cacheRequest, response)
|
return cacheWritingResponse(cacheRequest, response)
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import okhttp3.CacheControl;
|
|||||||
import okhttp3.Headers;
|
import okhttp3.Headers;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
import okhttp3.internal.UtilKt;
|
||||||
import okhttp3.internal.http.HttpDate;
|
import okhttp3.internal.http.HttpDate;
|
||||||
import okhttp3.internal.http.HttpHeaders;
|
|
||||||
import okhttp3.internal.http.StatusLine;
|
import okhttp3.internal.http.StatusLine;
|
||||||
|
|
||||||
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
|
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
|
||||||
@@ -160,7 +160,7 @@ public final class CacheStrategy {
|
|||||||
} else if ("ETag".equalsIgnoreCase(fieldName)) {
|
} else if ("ETag".equalsIgnoreCase(fieldName)) {
|
||||||
etag = value;
|
etag = value;
|
||||||
} else if ("Age".equalsIgnoreCase(fieldName)) {
|
} else if ("Age".equalsIgnoreCase(fieldName)) {
|
||||||
ageSeconds = HttpHeaders.parseSeconds(value, -1);
|
ageSeconds = UtilKt.toNonNegativeInt(value, -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,14 +83,14 @@ class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {
|
|||||||
|
|
||||||
val networkResponse = chain.proceed(requestBuilder.build())
|
val networkResponse = chain.proceed(requestBuilder.build())
|
||||||
|
|
||||||
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers())
|
cookieJar.receiveHeaders(userRequest.url(), networkResponse.headers())
|
||||||
|
|
||||||
val responseBuilder = networkResponse.newBuilder()
|
val responseBuilder = networkResponse.newBuilder()
|
||||||
.request(userRequest)
|
.request(userRequest)
|
||||||
|
|
||||||
if (transparentGzip &&
|
if (transparentGzip &&
|
||||||
"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
|
"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
|
||||||
HttpHeaders.hasBody(networkResponse)) {
|
networkResponse.promisesBody()) {
|
||||||
val responseBody = networkResponse.body()
|
val responseBody = networkResponse.body()
|
||||||
if (responseBody != null) {
|
if (responseBody != null) {
|
||||||
val gzipSource = GzipSource(responseBody.source())
|
val gzipSource = GzipSource(responseBody.source())
|
||||||
|
|||||||
@@ -1,401 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* 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.http;
|
|
||||||
|
|
||||||
import java.io.EOFException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
import okhttp3.Challenge;
|
|
||||||
import okhttp3.Cookie;
|
|
||||||
import okhttp3.CookieJar;
|
|
||||||
import okhttp3.Headers;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import okhttp3.internal.platform.Platform;
|
|
||||||
import okio.Buffer;
|
|
||||||
import okio.ByteString;
|
|
||||||
|
|
||||||
import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
|
|
||||||
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
|
|
||||||
import static okhttp3.internal.Util.EMPTY_HEADERS;
|
|
||||||
import static okhttp3.internal.http.StatusLine.HTTP_CONTINUE;
|
|
||||||
|
|
||||||
/** Headers and utilities for internal use by OkHttp. */
|
|
||||||
public final class HttpHeaders {
|
|
||||||
private static final ByteString QUOTED_STRING_DELIMITERS = ByteString.encodeUtf8("\"\\");
|
|
||||||
private static final ByteString TOKEN_DELIMITERS = ByteString.encodeUtf8("\t ,=");
|
|
||||||
|
|
||||||
private HttpHeaders() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long contentLength(Response response) {
|
|
||||||
return contentLength(response.headers());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long contentLength(Headers headers) {
|
|
||||||
return stringToLong(headers.get("Content-Length"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long stringToLong(String s) {
|
|
||||||
if (s == null) return -1;
|
|
||||||
try {
|
|
||||||
return Long.parseLong(s);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if none of the Vary headers have changed between {@code cachedRequest} and {@code
|
|
||||||
* newRequest}.
|
|
||||||
*/
|
|
||||||
public static boolean varyMatches(
|
|
||||||
Response cachedResponse, Headers cachedRequest, Request newRequest) {
|
|
||||||
for (String field : varyFields(cachedResponse)) {
|
|
||||||
if (!Objects.equals(cachedRequest.values(field), newRequest.headers(field))) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if a Vary header contains an asterisk. Such responses cannot be cached.
|
|
||||||
*/
|
|
||||||
public static boolean hasVaryAll(Response response) {
|
|
||||||
return hasVaryAll(response.headers());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if a Vary header contains an asterisk. Such responses cannot be cached.
|
|
||||||
*/
|
|
||||||
public static boolean hasVaryAll(Headers responseHeaders) {
|
|
||||||
return varyFields(responseHeaders).contains("*");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> varyFields(Response response) {
|
|
||||||
return varyFields(response.headers());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the names of the request headers that need to be checked for equality when caching.
|
|
||||||
*/
|
|
||||||
public static Set<String> varyFields(Headers responseHeaders) {
|
|
||||||
Set<String> result = Collections.emptySet();
|
|
||||||
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
|
|
||||||
if (!"Vary".equalsIgnoreCase(responseHeaders.name(i))) continue;
|
|
||||||
|
|
||||||
String value = responseHeaders.value(i);
|
|
||||||
if (result.isEmpty()) {
|
|
||||||
result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
|
||||||
}
|
|
||||||
for (String varyField : value.split(",")) {
|
|
||||||
result.add(varyField.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the subset of the headers in {@code response}'s request that impact the content of
|
|
||||||
* response's body.
|
|
||||||
*/
|
|
||||||
public static Headers varyHeaders(Response response) {
|
|
||||||
// Use the request headers sent over the network, since that's what the
|
|
||||||
// response varies on. Otherwise OkHttp-supplied headers like
|
|
||||||
// "Accept-Encoding: gzip" may be lost.
|
|
||||||
Headers requestHeaders = response.networkResponse().request().headers();
|
|
||||||
Headers responseHeaders = response.headers();
|
|
||||||
return varyHeaders(requestHeaders, responseHeaders);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the subset of the headers in {@code requestHeaders} that impact the content of
|
|
||||||
* response's body.
|
|
||||||
*/
|
|
||||||
public static Headers varyHeaders(Headers requestHeaders, Headers responseHeaders) {
|
|
||||||
Set<String> varyFields = varyFields(responseHeaders);
|
|
||||||
if (varyFields.isEmpty()) return EMPTY_HEADERS;
|
|
||||||
|
|
||||||
Headers.Builder result = new Headers.Builder();
|
|
||||||
for (int i = 0, size = requestHeaders.size(); i < size; i++) {
|
|
||||||
String fieldName = requestHeaders.name(i);
|
|
||||||
if (varyFields.contains(fieldName)) {
|
|
||||||
result.add(fieldName, requestHeaders.value(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse RFC 7235 challenges. This is awkward because we need to look ahead to know how to
|
|
||||||
* interpret a token.
|
|
||||||
*
|
|
||||||
* <p>For example, the first line has a parameter name/value pair and the second line has a single
|
|
||||||
* token68:
|
|
||||||
*
|
|
||||||
* <pre> {@code
|
|
||||||
*
|
|
||||||
* WWW-Authenticate: Digest foo=bar
|
|
||||||
* WWW-Authenticate: Digest foo=
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* <p>Similarly, the first line has one challenge and the second line has two challenges:
|
|
||||||
*
|
|
||||||
* <pre> {@code
|
|
||||||
*
|
|
||||||
* WWW-Authenticate: Digest ,foo=bar
|
|
||||||
* WWW-Authenticate: Digest ,foo
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
public static List<Challenge> parseChallenges(Headers responseHeaders, String headerName) {
|
|
||||||
List<Challenge> result = new ArrayList<>();
|
|
||||||
for (int h = 0; h < responseHeaders.size(); h++) {
|
|
||||||
if (headerName.equalsIgnoreCase(responseHeaders.name(h))) {
|
|
||||||
Buffer header = new Buffer().writeUtf8(responseHeaders.value(h));
|
|
||||||
try {
|
|
||||||
parseChallengeHeader(result, header);
|
|
||||||
} catch (EOFException e) {
|
|
||||||
Platform.get().log(Platform.WARN, "Unable to parse challenge", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void parseChallengeHeader(List<Challenge> result, Buffer header)
|
|
||||||
throws EOFException {
|
|
||||||
String peek = null;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
// Read a scheme name for this challenge if we don't have one already.
|
|
||||||
if (peek == null) {
|
|
||||||
skipWhitespaceAndCommas(header);
|
|
||||||
peek = readToken(header);
|
|
||||||
if (peek == null) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String schemeName = peek;
|
|
||||||
|
|
||||||
// Read a token68, a sequence of parameters, or nothing.
|
|
||||||
boolean commaPrefixed = skipWhitespaceAndCommas(header);
|
|
||||||
peek = readToken(header);
|
|
||||||
if (peek == null) {
|
|
||||||
if (!header.exhausted()) return; // Expected a token; got something else.
|
|
||||||
result.add(new Challenge(schemeName, Collections.emptyMap()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int eqCount = skipAll(header, (byte) '=');
|
|
||||||
boolean commaSuffixed = skipWhitespaceAndCommas(header);
|
|
||||||
|
|
||||||
// It's a token68 because there isn't a value after it.
|
|
||||||
if (!commaPrefixed && (commaSuffixed || header.exhausted())) {
|
|
||||||
result.add(new Challenge(schemeName, Collections.singletonMap(
|
|
||||||
null, peek + repeat('=', eqCount))));
|
|
||||||
peek = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's a series of parameter names and values.
|
|
||||||
Map<String, String> parameters = new LinkedHashMap<>();
|
|
||||||
eqCount += skipAll(header, (byte) '=');
|
|
||||||
while (true) {
|
|
||||||
if (peek == null) {
|
|
||||||
peek = readToken(header);
|
|
||||||
if (skipWhitespaceAndCommas(header)) break; // We peeked a scheme name followed by ','.
|
|
||||||
eqCount = skipAll(header, (byte) '=');
|
|
||||||
}
|
|
||||||
if (eqCount == 0) break; // We peeked a scheme name.
|
|
||||||
if (eqCount > 1) return; // Unexpected '=' characters.
|
|
||||||
if (skipWhitespaceAndCommas(header)) return; // Unexpected ','.
|
|
||||||
|
|
||||||
String parameterValue = !header.exhausted() && header.getByte(0) == '"'
|
|
||||||
? readQuotedString(header)
|
|
||||||
: readToken(header);
|
|
||||||
if (parameterValue == null) return; // Expected a value.
|
|
||||||
String replaced = parameters.put(peek, parameterValue);
|
|
||||||
peek = null;
|
|
||||||
if (replaced != null) return; // Unexpected duplicate parameter.
|
|
||||||
if (!skipWhitespaceAndCommas(header) && !header.exhausted()) return; // Expected ',' or EOF.
|
|
||||||
}
|
|
||||||
result.add(new Challenge(schemeName, parameters));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns true if any commas were skipped. */
|
|
||||||
private static boolean skipWhitespaceAndCommas(Buffer buffer) throws EOFException {
|
|
||||||
boolean commaFound = false;
|
|
||||||
while (!buffer.exhausted()) {
|
|
||||||
byte b = buffer.getByte(0);
|
|
||||||
if (b == ',') {
|
|
||||||
// Consume ','.
|
|
||||||
buffer.readByte();
|
|
||||||
commaFound = true;
|
|
||||||
} else if (b == ' ' || b == '\t') {
|
|
||||||
// Consume space or tab.
|
|
||||||
buffer.readByte();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return commaFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int skipAll(Buffer buffer, byte b) throws EOFException {
|
|
||||||
int count = 0;
|
|
||||||
while (!buffer.exhausted() && buffer.getByte(0) == b) {
|
|
||||||
count++;
|
|
||||||
buffer.readByte();
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a double-quoted string, unescaping quoted pairs like {@code \"} to the 2nd character in
|
|
||||||
* each sequence. Returns the unescaped string, or null if the buffer isn't prefixed with a
|
|
||||||
* double-quoted string.
|
|
||||||
*/
|
|
||||||
private static String readQuotedString(Buffer buffer) throws EOFException {
|
|
||||||
if (buffer.readByte() != '\"') throw new IllegalArgumentException();
|
|
||||||
Buffer result = new Buffer();
|
|
||||||
while (true) {
|
|
||||||
long i = buffer.indexOfElement(QUOTED_STRING_DELIMITERS);
|
|
||||||
if (i == -1L) return null; // Unterminated quoted string.
|
|
||||||
|
|
||||||
if (buffer.getByte(i) == '"') {
|
|
||||||
result.write(buffer, i);
|
|
||||||
// Consume '"'.
|
|
||||||
buffer.readByte();
|
|
||||||
return result.readUtf8();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buffer.size() == i + 1L) return null; // Dangling escape.
|
|
||||||
result.write(buffer, i);
|
|
||||||
// Consume '\'.
|
|
||||||
buffer.readByte();
|
|
||||||
result.write(buffer, 1L); // The escaped character.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consumes and returns a non-empty token, terminating at special characters in {@link
|
|
||||||
* #TOKEN_DELIMITERS}. Returns null if the buffer is empty or prefixed with a delimiter.
|
|
||||||
*/
|
|
||||||
private static String readToken(Buffer buffer) {
|
|
||||||
try {
|
|
||||||
long tokenSize = buffer.indexOfElement(TOKEN_DELIMITERS);
|
|
||||||
if (tokenSize == -1L) tokenSize = buffer.size();
|
|
||||||
|
|
||||||
return tokenSize != 0L
|
|
||||||
? buffer.readUtf8(tokenSize)
|
|
||||||
: null;
|
|
||||||
} catch (EOFException e) {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String repeat(char c, int count) {
|
|
||||||
char[] array = new char[count];
|
|
||||||
Arrays.fill(array, c);
|
|
||||||
return new String(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void receiveHeaders(CookieJar cookieJar, HttpUrl url, Headers headers) {
|
|
||||||
if (cookieJar == CookieJar.NO_COOKIES) return;
|
|
||||||
|
|
||||||
List<Cookie> cookies = Cookie.parseAll(url, headers);
|
|
||||||
if (cookies.isEmpty()) return;
|
|
||||||
|
|
||||||
cookieJar.saveFromResponse(url, cookies);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns true if the response must have a (possibly 0-length) body. See RFC 7231. */
|
|
||||||
public static boolean hasBody(Response response) {
|
|
||||||
// HEAD requests never yield a body regardless of the response headers.
|
|
||||||
if (response.request().method().equals("HEAD")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = response.code();
|
|
||||||
if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
|
|
||||||
&& responseCode != HTTP_NO_CONTENT
|
|
||||||
&& responseCode != HTTP_NOT_MODIFIED) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the Content-Length or Transfer-Encoding headers disagree with the response code, the
|
|
||||||
// response is malformed. For best compatibility, we honor the headers.
|
|
||||||
if (contentLength(response) != -1
|
|
||||||
|| "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next index in {@code input} at or after {@code pos} that contains a character from
|
|
||||||
* {@code characters}. Returns the input length if none of the requested characters can be found.
|
|
||||||
*/
|
|
||||||
public static int skipUntil(String input, int pos, String characters) {
|
|
||||||
for (; pos < input.length(); pos++) {
|
|
||||||
if (characters.indexOf(input.charAt(pos)) != -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next non-whitespace character in {@code input} that is white space. Result is
|
|
||||||
* undefined if input contains newline characters.
|
|
||||||
*/
|
|
||||||
public static int skipWhitespace(String input, int pos) {
|
|
||||||
for (; pos < input.length(); pos++) {
|
|
||||||
char c = input.charAt(pos);
|
|
||||||
if (c != ' ' && c != '\t') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code value} as a positive integer, or 0 if it is negative, or {@code defaultValue} if
|
|
||||||
* it cannot be parsed.
|
|
||||||
*/
|
|
||||||
public static int parseSeconds(String value, int defaultValue) {
|
|
||||||
try {
|
|
||||||
long seconds = Long.parseLong(value);
|
|
||||||
if (seconds > Integer.MAX_VALUE) {
|
|
||||||
return Integer.MAX_VALUE;
|
|
||||||
} else if (seconds < 0) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return (int) seconds;
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
234
okhttp/src/main/java/okhttp3/internal/http/HttpHeaders.kt
Normal file
234
okhttp/src/main/java/okhttp3/internal/http/HttpHeaders.kt
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@file:JvmName("HttpHeaders")
|
||||||
|
package okhttp3.internal.http
|
||||||
|
|
||||||
|
import okhttp3.Challenge
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.headersContentLength
|
||||||
|
import okhttp3.internal.http.StatusLine.Companion.HTTP_CONTINUE
|
||||||
|
import okhttp3.internal.platform.Platform
|
||||||
|
import okhttp3.internal.skipAll
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.ByteString.Companion.encodeUtf8
|
||||||
|
import java.io.EOFException
|
||||||
|
import java.net.HttpURLConnection.HTTP_NOT_MODIFIED
|
||||||
|
import java.net.HttpURLConnection.HTTP_NO_CONTENT
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
|
private val QUOTED_STRING_DELIMITERS = "\"\\".encodeUtf8()
|
||||||
|
private val TOKEN_DELIMITERS = "\t ,=".encodeUtf8()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse RFC 7235 challenges. This is awkward because we need to look ahead to know how to
|
||||||
|
* interpret a token.
|
||||||
|
*
|
||||||
|
* For example, the first line has a parameter name/value pair and the second line has a single
|
||||||
|
* token68:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* WWW-Authenticate: Digest foo=bar
|
||||||
|
* WWW-Authenticate: Digest foo=
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Similarly, the first line has one challenge and the second line has two challenges:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* WWW-Authenticate: Digest ,foo=bar
|
||||||
|
* WWW-Authenticate: Digest ,foo
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
fun Headers.parseChallenges(headerName: String): List<Challenge> {
|
||||||
|
val result = mutableListOf<Challenge>()
|
||||||
|
for (h in 0 until size()) {
|
||||||
|
if (headerName.equals(name(h), ignoreCase = true)) {
|
||||||
|
val header = Buffer().writeUtf8(value(h))
|
||||||
|
try {
|
||||||
|
header.readChallengeHeader(result)
|
||||||
|
} catch (e: EOFException) {
|
||||||
|
Platform.get().log(Platform.WARN, "Unable to parse challenge", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(EOFException::class)
|
||||||
|
private fun Buffer.readChallengeHeader(result: MutableList<Challenge>) {
|
||||||
|
var peek: String? = null
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Read a scheme name for this challenge if we don't have one already.
|
||||||
|
if (peek == null) {
|
||||||
|
skipCommasAndWhitespace()
|
||||||
|
peek = readToken()
|
||||||
|
if (peek == null) return
|
||||||
|
}
|
||||||
|
|
||||||
|
val schemeName = peek
|
||||||
|
|
||||||
|
// Read a token68, a sequence of parameters, or nothing.
|
||||||
|
val commaPrefixed = skipCommasAndWhitespace()
|
||||||
|
peek = readToken()
|
||||||
|
if (peek == null) {
|
||||||
|
if (!exhausted()) return // Expected a token; got something else.
|
||||||
|
result.add(Challenge(schemeName, emptyMap()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var eqCount = skipAll('='.toByte())
|
||||||
|
val commaSuffixed = skipCommasAndWhitespace()
|
||||||
|
|
||||||
|
// It's a token68 because there isn't a value after it.
|
||||||
|
if (!commaPrefixed && (commaSuffixed || exhausted())) {
|
||||||
|
result.add(Challenge(schemeName,
|
||||||
|
Collections.singletonMap<String, String>(null, peek + "=".repeat(eqCount))))
|
||||||
|
peek = null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's a series of parameter names and values.
|
||||||
|
val parameters = mutableMapOf<String?, String>()
|
||||||
|
eqCount += skipAll('='.toByte())
|
||||||
|
while (true) {
|
||||||
|
if (peek == null) {
|
||||||
|
peek = readToken()
|
||||||
|
if (skipCommasAndWhitespace()) break // We peeked a scheme name followed by ','.
|
||||||
|
eqCount = skipAll('='.toByte())
|
||||||
|
}
|
||||||
|
if (eqCount == 0) break // We peeked a scheme name.
|
||||||
|
if (eqCount > 1) return // Unexpected '=' characters.
|
||||||
|
if (skipCommasAndWhitespace()) return // Unexpected ','.
|
||||||
|
|
||||||
|
val parameterValue = when {
|
||||||
|
startsWith('"'.toByte()) -> readQuotedString()
|
||||||
|
else -> readToken()
|
||||||
|
} ?: return // Expected a value.
|
||||||
|
|
||||||
|
val replaced = parameters.put(peek, parameterValue)
|
||||||
|
peek = null
|
||||||
|
if (replaced != null) return // Unexpected duplicate parameter.
|
||||||
|
if (!skipCommasAndWhitespace() && !exhausted()) return // Expected ',' or EOF.
|
||||||
|
}
|
||||||
|
result.add(Challenge(schemeName, parameters))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if any commas were skipped. */
|
||||||
|
private fun Buffer.skipCommasAndWhitespace(): Boolean {
|
||||||
|
var commaFound = false
|
||||||
|
loop@ while (!exhausted()) {
|
||||||
|
when (this[0]) {
|
||||||
|
','.toByte() -> {
|
||||||
|
// Consume ','.
|
||||||
|
readByte()
|
||||||
|
commaFound = true
|
||||||
|
}
|
||||||
|
|
||||||
|
' '.toByte(), '\t'.toByte() -> {
|
||||||
|
readByte()
|
||||||
|
// Consume space or tab.
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> break@loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commaFound
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Buffer.startsWith(prefix: Byte) = !exhausted() && this[0] == prefix
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a double-quoted string, unescaping quoted pairs like `\"` to the 2nd character in each
|
||||||
|
* sequence. Returns the unescaped string, or null if the buffer isn't prefixed with a
|
||||||
|
* double-quoted string.
|
||||||
|
*/
|
||||||
|
@Throws(EOFException::class)
|
||||||
|
private fun Buffer.readQuotedString(): String? {
|
||||||
|
require(readByte() == '\"'.toByte())
|
||||||
|
val result = Buffer()
|
||||||
|
while (true) {
|
||||||
|
val i = indexOfElement(QUOTED_STRING_DELIMITERS)
|
||||||
|
if (i == -1L) return null // Unterminated quoted string.
|
||||||
|
|
||||||
|
if (this[i] == '"'.toByte()) {
|
||||||
|
result.write(this, i)
|
||||||
|
// Consume '"'.
|
||||||
|
readByte()
|
||||||
|
return result.readUtf8()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size == i + 1L) return null // Dangling escape.
|
||||||
|
result.write(this, i)
|
||||||
|
// Consume '\'.
|
||||||
|
readByte()
|
||||||
|
result.write(this, 1L) // The escaped character.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes and returns a non-empty token, terminating at special characters in
|
||||||
|
* [TOKEN_DELIMITERS]. Returns null if the buffer is empty or prefixed with a delimiter.
|
||||||
|
*/
|
||||||
|
private fun Buffer.readToken(): String? {
|
||||||
|
var tokenSize = indexOfElement(TOKEN_DELIMITERS)
|
||||||
|
if (tokenSize == -1L) tokenSize = size
|
||||||
|
|
||||||
|
return when {
|
||||||
|
tokenSize != 0L -> readUtf8(tokenSize)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CookieJar.receiveHeaders(url: HttpUrl, headers: Headers) {
|
||||||
|
if (this === CookieJar.NO_COOKIES) return
|
||||||
|
|
||||||
|
val cookies = Cookie.parseAll(url, headers)
|
||||||
|
if (cookies.isEmpty()) return
|
||||||
|
|
||||||
|
saveFromResponse(url, cookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the response headers and status indicate that this response has a (possibly
|
||||||
|
* 0-length) body. See RFC 7231.
|
||||||
|
*/
|
||||||
|
fun Response.promisesBody(): Boolean {
|
||||||
|
// HEAD requests never yield a body regardless of the response headers.
|
||||||
|
if (request().method() == "HEAD") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseCode = code()
|
||||||
|
if ((responseCode < HTTP_CONTINUE || responseCode >= 200) &&
|
||||||
|
responseCode != HTTP_NO_CONTENT &&
|
||||||
|
responseCode != HTTP_NOT_MODIFIED) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the Content-Length or Transfer-Encoding headers disagree with the response code, the
|
||||||
|
// response is malformed. For best compatibility, we honor the headers.
|
||||||
|
if (headersContentLength() != -1L ||
|
||||||
|
"chunked".equals(header("Transfer-Encoding"), ignoreCase = true)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -24,11 +24,13 @@ import okhttp3.internal.Util
|
|||||||
import okhttp3.internal.Util.checkOffsetAndCount
|
import okhttp3.internal.Util.checkOffsetAndCount
|
||||||
import okhttp3.internal.addHeaderLenient
|
import okhttp3.internal.addHeaderLenient
|
||||||
import okhttp3.internal.connection.RealConnection
|
import okhttp3.internal.connection.RealConnection
|
||||||
|
import okhttp3.internal.headersContentLength
|
||||||
import okhttp3.internal.http.ExchangeCodec
|
import okhttp3.internal.http.ExchangeCodec
|
||||||
import okhttp3.internal.http.HttpHeaders
|
|
||||||
import okhttp3.internal.http.RequestLine
|
import okhttp3.internal.http.RequestLine
|
||||||
import okhttp3.internal.http.StatusLine
|
import okhttp3.internal.http.StatusLine
|
||||||
import okhttp3.internal.http.StatusLine.Companion.HTTP_CONTINUE
|
import okhttp3.internal.http.StatusLine.Companion.HTTP_CONTINUE
|
||||||
|
import okhttp3.internal.http.promisesBody
|
||||||
|
import okhttp3.internal.http.receiveHeaders
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
@@ -123,18 +125,18 @@ class Http1ExchangeCodec(
|
|||||||
|
|
||||||
override fun reportedContentLength(response: Response): Long {
|
override fun reportedContentLength(response: Response): Long {
|
||||||
return when {
|
return when {
|
||||||
!HttpHeaders.hasBody(response) -> 0L
|
!response.promisesBody() -> 0L
|
||||||
response.isChunked() -> -1L
|
response.isChunked() -> -1L
|
||||||
else -> HttpHeaders.contentLength(response)
|
else -> response.headersContentLength()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openResponseBodySource(response: Response): Source {
|
override fun openResponseBodySource(response: Response): Source {
|
||||||
return when {
|
return when {
|
||||||
!HttpHeaders.hasBody(response) -> newFixedLengthSource(0)
|
!response.promisesBody() -> newFixedLengthSource(0)
|
||||||
response.isChunked() -> newChunkedSource(response.request().url())
|
response.isChunked() -> newChunkedSource(response.request().url())
|
||||||
else -> {
|
else -> {
|
||||||
val contentLength = HttpHeaders.contentLength(response)
|
val contentLength = response.headersContentLength()
|
||||||
if (contentLength != -1L) {
|
if (contentLength != -1L) {
|
||||||
newFixedLengthSource(contentLength)
|
newFixedLengthSource(contentLength)
|
||||||
} else {
|
} else {
|
||||||
@@ -267,7 +269,7 @@ class Http1ExchangeCodec(
|
|||||||
* before proceeding.
|
* before proceeding.
|
||||||
*/
|
*/
|
||||||
fun skipConnectBody(response: Response) {
|
fun skipConnectBody(response: Response) {
|
||||||
val contentLength = HttpHeaders.contentLength(response)
|
val contentLength = response.headersContentLength()
|
||||||
if (contentLength == -1L) return
|
if (contentLength == -1L) return
|
||||||
val body = newFixedLengthSource(contentLength)
|
val body = newFixedLengthSource(contentLength)
|
||||||
Util.skipAll(body, Int.MAX_VALUE, MILLISECONDS)
|
Util.skipAll(body, Int.MAX_VALUE, MILLISECONDS)
|
||||||
@@ -455,7 +457,7 @@ class Http1ExchangeCodec(
|
|||||||
if (bytesRemainingInChunk == 0L) {
|
if (bytesRemainingInChunk == 0L) {
|
||||||
hasMoreChunks = false
|
hasMoreChunks = false
|
||||||
trailers = readHeaders()
|
trailers = readHeaders()
|
||||||
HttpHeaders.receiveHeaders(client!!.cookieJar(), url, trailers)
|
client!!.cookieJar().receiveHeaders(url, trailers!!)
|
||||||
responseBodyComplete()
|
responseBodyComplete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import okhttp3.internal.Internal
|
|||||||
import okhttp3.internal.Util
|
import okhttp3.internal.Util
|
||||||
import okhttp3.internal.addHeaderLenient
|
import okhttp3.internal.addHeaderLenient
|
||||||
import okhttp3.internal.connection.RealConnection
|
import okhttp3.internal.connection.RealConnection
|
||||||
|
import okhttp3.internal.headersContentLength
|
||||||
import okhttp3.internal.http.ExchangeCodec
|
import okhttp3.internal.http.ExchangeCodec
|
||||||
import okhttp3.internal.http.HttpHeaders
|
|
||||||
import okhttp3.internal.http.RequestLine
|
import okhttp3.internal.http.RequestLine
|
||||||
import okhttp3.internal.http.StatusLine
|
import okhttp3.internal.http.StatusLine
|
||||||
import okhttp3.internal.http.StatusLine.Companion.HTTP_CONTINUE
|
import okhttp3.internal.http.StatusLine.Companion.HTTP_CONTINUE
|
||||||
@@ -108,7 +108,7 @@ class Http2ExchangeCodec(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun reportedContentLength(response: Response): Long {
|
override fun reportedContentLength(response: Response): Long {
|
||||||
return HttpHeaders.contentLength(response)
|
return response.headersContentLength()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openResponseBodySource(response: Response): Source {
|
override fun openResponseBodySource(response: Response): Source {
|
||||||
|
|||||||
Reference in New Issue
Block a user