Home | History | Annotate | Download | only in http
      1 package com.squareup.okhttp.internal.http;
      2 
      3 import com.squareup.okhttp.Authenticator;
      4 import com.squareup.okhttp.Challenge;
      5 import com.squareup.okhttp.Headers;
      6 import com.squareup.okhttp.Request;
      7 import com.squareup.okhttp.Response;
      8 import com.squareup.okhttp.internal.Platform;
      9 import java.io.IOException;
     10 import java.net.Proxy;
     11 import java.util.ArrayList;
     12 import java.util.Collections;
     13 import java.util.Comparator;
     14 import java.util.List;
     15 import java.util.Map;
     16 import java.util.Set;
     17 import java.util.TreeMap;
     18 import java.util.TreeSet;
     19 
     20 import static com.squareup.okhttp.internal.Util.equal;
     21 import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
     22 
     23 /** Headers and utilities for internal use by OkHttp. */
     24 public final class OkHeaders {
     25   private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
     26     // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
     27     @Override public int compare(String a, String b) {
     28       if (a == b) {
     29         return 0;
     30       } else if (a == null) {
     31         return -1;
     32       } else if (b == null) {
     33         return 1;
     34       } else {
     35         return String.CASE_INSENSITIVE_ORDER.compare(a, b);
     36       }
     37     }
     38   };
     39 
     40   static final String PREFIX = Platform.get().getPrefix();
     41 
     42   /**
     43    * Synthetic response header: the local time when the request was sent.
     44    */
     45   public static final String SENT_MILLIS = PREFIX + "-Sent-Millis";
     46 
     47   /**
     48    * Synthetic response header: the local time when the response was received.
     49    */
     50   public static final String RECEIVED_MILLIS = PREFIX + "-Received-Millis";
     51 
     52   /**
     53    * Synthetic response header: the selected
     54    * {@link com.squareup.okhttp.Protocol protocol} ("spdy/3.1", "http/1.1", etc).
     55    */
     56   public static final String SELECTED_PROTOCOL = PREFIX + "-Selected-Protocol";
     57 
     58   private OkHeaders() {
     59   }
     60 
     61   public static long contentLength(Request request) {
     62     return contentLength(request.headers());
     63   }
     64 
     65   public static long contentLength(Response response) {
     66     return contentLength(response.headers());
     67   }
     68 
     69   public static long contentLength(Headers headers) {
     70     return stringToLong(headers.get("Content-Length"));
     71   }
     72 
     73   private static long stringToLong(String s) {
     74     if (s == null) return -1;
     75     try {
     76       return Long.parseLong(s);
     77     } catch (NumberFormatException e) {
     78       return -1;
     79     }
     80   }
     81 
     82   /**
     83    * Returns an immutable map containing each field to its list of values.
     84    *
     85    * @param valueForNullKey the request line for requests, or the status line
     86    *     for responses. If non-null, this value is mapped to the null key.
     87    */
     88   public static Map<String, List<String>> toMultimap(Headers headers, String valueForNullKey) {
     89     Map<String, List<String>> result = new TreeMap<>(FIELD_NAME_COMPARATOR);
     90     for (int i = 0, size = headers.size(); i < size; i++) {
     91       String fieldName = headers.name(i);
     92       String value = headers.value(i);
     93 
     94       List<String> allValues = new ArrayList<>();
     95       List<String> otherValues = result.get(fieldName);
     96       if (otherValues != null) {
     97         allValues.addAll(otherValues);
     98       }
     99       allValues.add(value);
    100       result.put(fieldName, Collections.unmodifiableList(allValues));
    101     }
    102     if (valueForNullKey != null) {
    103       result.put(null, Collections.unmodifiableList(Collections.singletonList(valueForNullKey)));
    104     }
    105     return Collections.unmodifiableMap(result);
    106   }
    107 
    108   public static void addCookies(Request.Builder builder, Map<String, List<String>> cookieHeaders) {
    109     for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
    110       String key = entry.getKey();
    111       if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
    112           && !entry.getValue().isEmpty()) {
    113         builder.addHeader(key, buildCookieHeader(entry.getValue()));
    114       }
    115     }
    116   }
    117 
    118   /**
    119    * Send all cookies in one big header, as recommended by
    120    * <a href="http://tools.ietf.org/html/rfc6265#section-4.2.1">RFC 6265</a>.
    121    */
    122   private static String buildCookieHeader(List<String> cookies) {
    123     if (cookies.size() == 1) return cookies.get(0);
    124     StringBuilder sb = new StringBuilder();
    125     for (int i = 0, size = cookies.size(); i < size; i++) {
    126       if (i > 0) sb.append("; ");
    127       sb.append(cookies.get(i));
    128     }
    129     return sb.toString();
    130   }
    131 
    132   /**
    133    * Returns true if none of the Vary headers have changed between {@code
    134    * cachedRequest} and {@code newRequest}.
    135    */
    136   public static boolean varyMatches(
    137       Response cachedResponse, Headers cachedRequest, Request newRequest) {
    138     for (String field : varyFields(cachedResponse)) {
    139       if (!equal(cachedRequest.values(field), newRequest.headers(field))) return false;
    140     }
    141     return true;
    142   }
    143 
    144   /**
    145    * Returns true if a Vary header contains an asterisk. Such responses cannot
    146    * be cached.
    147    */
    148   public static boolean hasVaryAll(Response response) {
    149     return hasVaryAll(response.headers());
    150   }
    151 
    152   /**
    153    * Returns true if a Vary header contains an asterisk. Such responses cannot
    154    * be cached.
    155    */
    156   public static boolean hasVaryAll(Headers responseHeaders) {
    157     return varyFields(responseHeaders).contains("*");
    158   }
    159 
    160   private static Set<String> varyFields(Response response) {
    161     return varyFields(response.headers());
    162   }
    163 
    164   /**
    165    * Returns the names of the request headers that need to be checked for
    166    * equality when caching.
    167    */
    168   public static Set<String> varyFields(Headers responseHeaders) {
    169     Set<String> result = Collections.emptySet();
    170     for (int i = 0, size = responseHeaders.size(); i < size; i++) {
    171       if (!"Vary".equalsIgnoreCase(responseHeaders.name(i))) continue;
    172 
    173       String value = responseHeaders.value(i);
    174       if (result.isEmpty()) {
    175         result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
    176       }
    177       for (String varyField : value.split(",")) {
    178         result.add(varyField.trim());
    179       }
    180     }
    181     return result;
    182   }
    183 
    184   /**
    185    * Returns the subset of the headers in {@code response}'s request that
    186    * impact the content of response's body.
    187    */
    188   public static Headers varyHeaders(Response response) {
    189     // Use the request headers sent over the network, since that's what the
    190     // response varies on. Otherwise OkHttp-supplied headers like
    191     // "Accept-Encoding: gzip" may be lost.
    192     Headers requestHeaders = response.networkResponse().request().headers();
    193     Headers responseHeaders = response.headers();
    194     return varyHeaders(requestHeaders, responseHeaders);
    195   }
    196 
    197   /**
    198    * Returns the subset of the headers in {@code requestHeaders} that
    199    * impact the content of response's body.
    200    */
    201   public static Headers varyHeaders(Headers requestHeaders, Headers responseHeaders) {
    202     Set<String> varyFields = varyFields(responseHeaders);
    203     if (varyFields.isEmpty()) return new Headers.Builder().build();
    204 
    205     Headers.Builder result = new Headers.Builder();
    206     for (int i = 0, size = requestHeaders.size(); i < size; i++) {
    207       String fieldName = requestHeaders.name(i);
    208       if (varyFields.contains(fieldName)) {
    209         result.add(fieldName, requestHeaders.value(i));
    210       }
    211     }
    212     return result.build();
    213   }
    214 
    215   /**
    216    * Returns true if {@code fieldName} is an end-to-end HTTP header, as
    217    * defined by RFC 2616, 13.5.1.
    218    */
    219   static boolean isEndToEnd(String fieldName) {
    220     return !"Connection".equalsIgnoreCase(fieldName)
    221         && !"Keep-Alive".equalsIgnoreCase(fieldName)
    222         && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
    223         && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
    224         && !"TE".equalsIgnoreCase(fieldName)
    225         && !"Trailers".equalsIgnoreCase(fieldName)
    226         && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
    227         && !"Upgrade".equalsIgnoreCase(fieldName);
    228   }
    229 
    230   /**
    231    * Parse RFC 2617 challenges. This API is only interested in the scheme
    232    * name and realm.
    233    */
    234   public static List<Challenge> parseChallenges(Headers responseHeaders, String challengeHeader) {
    235     // auth-scheme = token
    236     // auth-param  = token "=" ( token | quoted-string )
    237     // challenge   = auth-scheme 1*SP 1#auth-param
    238     // realm       = "realm" "=" realm-value
    239     // realm-value = quoted-string
    240     List<Challenge> result = new ArrayList<>();
    241     for (int i = 0, size = responseHeaders.size(); i < size; i++) {
    242       if (!challengeHeader.equalsIgnoreCase(responseHeaders.name(i))) {
    243         continue;
    244       }
    245       String value = responseHeaders.value(i);
    246       int pos = 0;
    247       while (pos < value.length()) {
    248         int tokenStart = pos;
    249         pos = HeaderParser.skipUntil(value, pos, " ");
    250 
    251         String scheme = value.substring(tokenStart, pos).trim();
    252         pos = HeaderParser.skipWhitespace(value, pos);
    253 
    254         // TODO: This currently only handles schemes with a 'realm' parameter;
    255         //       It needs to be fixed to handle any scheme and any parameters
    256         //       http://code.google.com/p/android/issues/detail?id=11140
    257 
    258         if (!value.regionMatches(true, pos, "realm=\"", 0, "realm=\"".length())) {
    259           break; // Unexpected challenge parameter; give up!
    260         }
    261 
    262         pos += "realm=\"".length();
    263         int realmStart = pos;
    264         pos = HeaderParser.skipUntil(value, pos, "\"");
    265         String realm = value.substring(realmStart, pos);
    266         pos++; // Consume '"' close quote.
    267         pos = HeaderParser.skipUntil(value, pos, ",");
    268         pos++; // Consume ',' comma.
    269         pos = HeaderParser.skipWhitespace(value, pos);
    270         result.add(new Challenge(scheme, realm));
    271       }
    272     }
    273     return result;
    274   }
    275 
    276   /**
    277    * React to a failed authorization response by looking up new credentials.
    278    * Returns a request for a subsequent attempt, or null if no further attempts
    279    * should be made.
    280    */
    281   public static Request processAuthHeader(Authenticator authenticator, Response response,
    282       Proxy proxy) throws IOException {
    283     return response.code() == HTTP_PROXY_AUTH
    284         ? authenticator.authenticateProxy(proxy, response)
    285         : authenticator.authenticate(proxy, response);
    286   }
    287 }
    288