1
0
mirror of https://github.com/square/okhttp.git synced 2025-11-24 18:41:06 +03:00

Convert HttpHeaders to Kotlin (#5016)

* Rename HttpHeaders.java to .kt

* Convert HttpHeaders to Kotlin
This commit is contained in:
Jesse Wilson
2019-05-06 20:50:19 -04:00
committed by GitHub
parent 981639ac55
commit b46f891a52
12 changed files with 421 additions and 446 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
}

View File

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

View File

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