• 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 
OkHeaders()58   private OkHeaders() {
59   }
60 
contentLength(Request request)61   public static long contentLength(Request request) {
62     return contentLength(request.headers());
63   }
64 
contentLength(Response response)65   public static long contentLength(Response response) {
66     return contentLength(response.headers());
67   }
68 
contentLength(Headers headers)69   public static long contentLength(Headers headers) {
70     return stringToLong(headers.get("Content-Length"));
71   }
72 
stringToLong(String s)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    */
toMultimap(Headers headers, String valueForNullKey)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 
addCookies(Request.Builder builder, Map<String, List<String>> cookieHeaders)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    */
buildCookieHeader(List<String> cookies)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    */
varyMatches( Response cachedResponse, Headers cachedRequest, Request newRequest)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    */
hasVaryAll(Response response)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    */
hasVaryAll(Headers responseHeaders)156   public static boolean hasVaryAll(Headers responseHeaders) {
157     return varyFields(responseHeaders).contains("*");
158   }
159 
varyFields(Response response)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    */
varyFields(Headers responseHeaders)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    */
varyHeaders(Response response)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    */
varyHeaders(Headers requestHeaders, Headers responseHeaders)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    */
isEndToEnd(String fieldName)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    */
parseChallenges(Headers responseHeaders, String challengeHeader)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    */
processAuthHeader(Authenticator authenticator, Response response, Proxy proxy)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