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