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