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