blob: c381c4756274fe3fc29765f9ec7a467002dd8f68 [file] [log] [blame]
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Authenticator;
import com.squareup.okhttp.Challenge;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.Platform;
import java.io.IOException;
import java.net.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import static com.squareup.okhttp.internal.Util.equal;
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
/** Headers and utilities for internal use by OkHttp. */
public final class OkHeaders {
private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
// @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
@Override public int compare(String a, String b) {
if (a == b) {
return 0;
} else if (a == null) {
return -1;
} else if (b == null) {
return 1;
} else {
return String.CASE_INSENSITIVE_ORDER.compare(a, b);
}
}
};
static final String PREFIX = Platform.get().getPrefix();
/**
* Synthetic response header: the local time when the request was sent.
*/
public static final String SENT_MILLIS = PREFIX + "-Sent-Millis";
/**
* Synthetic response header: the local time when the response was received.
*/
public static final String RECEIVED_MILLIS = PREFIX + "-Received-Millis";
/**
* Synthetic response header: the selected
* {@link com.squareup.okhttp.Protocol protocol} ("spdy/3.1", "http/1.1", etc).
*/
public static final String SELECTED_PROTOCOL = PREFIX + "-Selected-Protocol";
private OkHeaders() {
}
public static long contentLength(Request request) {
return contentLength(request.headers());
}
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 an immutable map containing each field to its list of values.
*
* @param valueForNullKey the request line for requests, or the status line
* for responses. If non-null, this value is mapped to the null key.
*/
public static Map<String, List<String>> toMultimap(Headers headers, String valueForNullKey) {
Map<String, List<String>> result = new TreeMap<>(FIELD_NAME_COMPARATOR);
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
List<String> allValues = new ArrayList<>();
List<String> otherValues = result.get(fieldName);
if (otherValues != null) {
allValues.addAll(otherValues);
}
allValues.add(value);
result.put(fieldName, Collections.unmodifiableList(allValues));
}
if (valueForNullKey != null) {
result.put(null, Collections.unmodifiableList(Collections.singletonList(valueForNullKey)));
}
return Collections.unmodifiableMap(result);
}
public static void addCookies(Request.Builder builder, Map<String, List<String>> cookieHeaders) {
for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
String key = entry.getKey();
if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
&& !entry.getValue().isEmpty()) {
builder.addHeader(key, buildCookieHeader(entry.getValue()));
}
}
}
/**
* Send all cookies in one big header, as recommended by
* <a href="http://tools.ietf.org/html/rfc6265#section-4.2.1">RFC 6265</a>.
*/
private static String buildCookieHeader(List<String> cookies) {
if (cookies.size() == 1) return cookies.get(0);
StringBuilder sb = new StringBuilder();
for (int i = 0, size = cookies.size(); i < size; i++) {
if (i > 0) sb.append("; ");
sb.append(cookies.get(i));
}
return sb.toString();
}
/**
* 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<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 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();
}
/**
* Returns true if {@code fieldName} is an end-to-end HTTP header, as
* defined by RFC 2616, 13.5.1.
*/
static boolean isEndToEnd(String fieldName) {
return !"Connection".equalsIgnoreCase(fieldName)
&& !"Keep-Alive".equalsIgnoreCase(fieldName)
&& !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
&& !"Proxy-Authorization".equalsIgnoreCase(fieldName)
&& !"TE".equalsIgnoreCase(fieldName)
&& !"Trailers".equalsIgnoreCase(fieldName)
&& !"Transfer-Encoding".equalsIgnoreCase(fieldName)
&& !"Upgrade".equalsIgnoreCase(fieldName);
}
/**
* Parse RFC 2617 challenges. This API is only interested in the scheme
* name and realm.
*/
public static List<Challenge> parseChallenges(Headers responseHeaders, String challengeHeader) {
// auth-scheme = token
// auth-param = token "=" ( token | quoted-string )
// challenge = auth-scheme 1*SP 1#auth-param
// realm = "realm" "=" realm-value
// realm-value = quoted-string
List<Challenge> result = new ArrayList<>();
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
if (!challengeHeader.equalsIgnoreCase(responseHeaders.name(i))) {
continue;
}
String value = responseHeaders.value(i);
int pos = 0;
while (pos < value.length()) {
int tokenStart = pos;
pos = HeaderParser.skipUntil(value, pos, " ");
String scheme = value.substring(tokenStart, pos).trim();
pos = HeaderParser.skipWhitespace(value, pos);
// TODO: This currently only handles schemes with a 'realm' parameter;
// It needs to be fixed to handle any scheme and any parameters
// http://code.google.com/p/android/issues/detail?id=11140
if (!value.regionMatches(true, pos, "realm=\"", 0, "realm=\"".length())) {
break; // Unexpected challenge parameter; give up!
}
pos += "realm=\"".length();
int realmStart = pos;
pos = HeaderParser.skipUntil(value, pos, "\"");
String realm = value.substring(realmStart, pos);
pos++; // Consume '"' close quote.
pos = HeaderParser.skipUntil(value, pos, ",");
pos++; // Consume ',' comma.
pos = HeaderParser.skipWhitespace(value, pos);
result.add(new Challenge(scheme, realm));
}
}
return result;
}
/**
* React to a failed authorization response by looking up new credentials.
* Returns a request for a subsequent attempt, or null if no further attempts
* should be made.
*/
public static Request processAuthHeader(Authenticator authenticator, Response response,
Proxy proxy) throws IOException {
return response.code() == HTTP_PROXY_AUTH
? authenticator.authenticateProxy(proxy, response)
: authenticator.authenticate(proxy, response);
}
}