okhttp3.internal.http.HttpHeaders Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.document.library.opener.onedrive.web
Show all versions of com.liferay.document.library.opener.onedrive.web
Liferay Document Library Opener OneDrive Web
/*
* 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.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 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.equal;
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 (!equal(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 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 varyFields(Headers responseHeaders) {
Set 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 varyFields = varyFields(responseHeaders);
if (varyFields.isEmpty()) return new Headers.Builder().build();
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.
*
* For example, the first line has a parameter name/value pair and the second line has a single
* token68:
*
*
{@code
*
* WWW-Authenticate: Digest foo=bar
* WWW-Authenticate: Digest foo=
* }
*
* Similarly, the first line has one challenge and the second line has two challenges:
*
*
{@code
*
* WWW-Authenticate: Digest ,foo=bar
* WWW-Authenticate: Digest ,foo
* }
*/
public static List parseChallenges(Headers responseHeaders, String headerName) {
List 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));
parseChallengeHeader(result, header);
}
}
return result;
}
private static void parseChallengeHeader(List result, Buffer header) {
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(
(String) null, peek + repeat('=', eqCount))));
peek = null;
continue;
}
// It's a series of parameter names and values.
Map 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) {
boolean commaFound = false;
while (!buffer.exhausted()) {
byte b = buffer.getByte(0);
if (b == ',') {
buffer.readByte(); // Consume ','.
commaFound = true;
} else if (b == ' ' || b == '\t') {
buffer.readByte(); // Consume space or tab.
} else {
break;
}
}
return commaFound;
}
private static int skipAll(Buffer buffer, byte b) {
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) {
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);
buffer.readByte(); // Consume '"'.
return result.readUtf8();
}
if (buffer.size() == i + 1L) return null; // Dangling escape.
result.write(buffer, i);
buffer.readByte(); // Consume '\'.
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 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;
}
}
}