1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.squareup.okhttp.internal.http; 19 20 import com.squareup.okhttp.internal.Util; 21 import java.io.IOException; 22 import java.io.InputStream; 23 import java.io.UnsupportedEncodingException; 24 import java.net.ProtocolException; 25 import java.util.ArrayList; 26 import java.util.Collections; 27 import java.util.Comparator; 28 import java.util.HashSet; 29 import java.util.List; 30 import java.util.Locale; 31 import java.util.Map; 32 import java.util.Map.Entry; 33 import java.util.Set; 34 import java.util.TreeMap; 35 import java.util.TreeSet; 36 37 /** 38 * The HTTP status and unparsed header fields of a single HTTP message. Values 39 * are represented as uninterpreted strings; use {@link RequestHeaders} and 40 * {@link ResponseHeaders} for interpreted headers. This class maintains the 41 * order of the header fields within the HTTP message. 42 * 43 * <p>This class tracks fields line-by-line. A field with multiple comma- 44 * separated values on the same line will be treated as a field with a single 45 * value by this class. It is the caller's responsibility to detect and split 46 * on commas if their field permits multiple values. This simplifies use of 47 * single-valued fields whose values routinely contain commas, such as cookies 48 * or dates. 49 * 50 * <p>This class trims whitespace from values. It never returns values with 51 * leading or trailing whitespace. 52 */ 53 public final class RawHeaders { 54 private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() { 55 // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ") 56 @Override public int compare(String a, String b) { 57 if (a == b) { 58 return 0; 59 } else if (a == null) { 60 return -1; 61 } else if (b == null) { 62 return 1; 63 } else { 64 return String.CASE_INSENSITIVE_ORDER.compare(a, b); 65 } 66 } 67 }; 68 69 private final List<String> namesAndValues = new ArrayList<String>(20); 70 private String requestLine; 71 private String statusLine; 72 private int httpMinorVersion = 1; 73 private int responseCode = -1; 74 private String responseMessage; 75 76 public RawHeaders() { 77 } 78 79 public RawHeaders(RawHeaders copyFrom) { 80 namesAndValues.addAll(copyFrom.namesAndValues); 81 requestLine = copyFrom.requestLine; 82 statusLine = copyFrom.statusLine; 83 httpMinorVersion = copyFrom.httpMinorVersion; 84 responseCode = copyFrom.responseCode; 85 responseMessage = copyFrom.responseMessage; 86 } 87 88 /** Sets the request line (like "GET / HTTP/1.1"). */ 89 public void setRequestLine(String requestLine) { 90 requestLine = requestLine.trim(); 91 this.requestLine = requestLine; 92 } 93 94 /** Sets the response status line (like "HTTP/1.0 200 OK"). */ 95 public void setStatusLine(String statusLine) throws IOException { 96 // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t 97 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 98 if (this.responseMessage != null) { 99 throw new IllegalStateException("statusLine is already set"); 100 } 101 // We allow empty message without leading white space since some servers 102 // do not send the white space when the message is empty. 103 boolean hasMessage = statusLine.length() > 13; 104 if (!statusLine.startsWith("HTTP/1.") 105 || statusLine.length() < 12 106 || statusLine.charAt(8) != ' ' 107 || (hasMessage && statusLine.charAt(12) != ' ')) { 108 throw new ProtocolException("Unexpected status line: " + statusLine); 109 } 110 int httpMinorVersion = statusLine.charAt(7) - '0'; 111 if (httpMinorVersion < 0 || httpMinorVersion > 9) { 112 throw new ProtocolException("Unexpected status line: " + statusLine); 113 } 114 int responseCode; 115 try { 116 responseCode = Integer.parseInt(statusLine.substring(9, 12)); 117 } catch (NumberFormatException e) { 118 throw new ProtocolException("Unexpected status line: " + statusLine); 119 } 120 this.responseMessage = hasMessage ? statusLine.substring(13) : ""; 121 this.responseCode = responseCode; 122 this.statusLine = statusLine; 123 this.httpMinorVersion = httpMinorVersion; 124 } 125 126 /** 127 * @param method like "GET", "POST", "HEAD", etc. 128 * @param path like "/foo/bar.html" 129 * @param version like "HTTP/1.1" 130 * @param host like "www.android.com:1234" 131 * @param scheme like "https" 132 */ 133 public void addSpdyRequestHeaders(String method, String path, String version, String host, 134 String scheme) { 135 // TODO: populate the statusLine for the client's benefit? 136 add(":method", method); 137 add(":scheme", scheme); 138 add(":path", path); 139 add(":version", version); 140 add(":host", host); 141 } 142 143 public String getStatusLine() { 144 return statusLine; 145 } 146 147 /** 148 * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0 149 * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown. 150 */ 151 public int getHttpMinorVersion() { 152 return httpMinorVersion != -1 ? httpMinorVersion : 1; 153 } 154 155 /** Returns the HTTP status code or -1 if it is unknown. */ 156 public int getResponseCode() { 157 return responseCode; 158 } 159 160 /** Returns the HTTP status message or null if it is unknown. */ 161 public String getResponseMessage() { 162 return responseMessage; 163 } 164 165 /** 166 * Add an HTTP header line containing a field name, a literal colon, and a 167 * value. 168 */ 169 public void addLine(String line) { 170 int index = line.indexOf(":"); 171 if (index == -1) { 172 addLenient("", line); 173 } else { 174 addLenient(line.substring(0, index), line.substring(index + 1)); 175 } 176 } 177 178 /** Add a field with the specified value. */ 179 public void add(String fieldName, String value) { 180 if (fieldName == null) throw new IllegalArgumentException("fieldname == null"); 181 if (value == null) throw new IllegalArgumentException("value == null"); 182 if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) { 183 throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value); 184 } 185 addLenient(fieldName, value); 186 } 187 188 /** 189 * Add a field with the specified value without any validation. Only 190 * appropriate for headers from the remote peer. 191 */ 192 private void addLenient(String fieldName, String value) { 193 namesAndValues.add(fieldName); 194 namesAndValues.add(value.trim()); 195 } 196 197 public void removeAll(String fieldName) { 198 for (int i = 0; i < namesAndValues.size(); i += 2) { 199 if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { 200 namesAndValues.remove(i); // field name 201 namesAndValues.remove(i); // value 202 } 203 } 204 } 205 206 public void addAll(String fieldName, List<String> headerFields) { 207 for (String value : headerFields) { 208 add(fieldName, value); 209 } 210 } 211 212 /** 213 * Set a field with the specified value. If the field is not found, it is 214 * added. If the field is found, the existing values are replaced. 215 */ 216 public void set(String fieldName, String value) { 217 removeAll(fieldName); 218 add(fieldName, value); 219 } 220 221 /** Returns the number of field values. */ 222 public int length() { 223 return namesAndValues.size() / 2; 224 } 225 226 /** Returns the field at {@code position} or null if that is out of range. */ 227 public String getFieldName(int index) { 228 int fieldNameIndex = index * 2; 229 if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) { 230 return null; 231 } 232 return namesAndValues.get(fieldNameIndex); 233 } 234 235 /** Returns an immutable case-insensitive set of header names. */ 236 public Set<String> names() { 237 TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); 238 for (int i = 0; i < length(); i++) { 239 result.add(getFieldName(i)); 240 } 241 return Collections.unmodifiableSet(result); 242 } 243 244 /** Returns the value at {@code index} or null if that is out of range. */ 245 public String getValue(int index) { 246 int valueIndex = index * 2 + 1; 247 if (valueIndex < 0 || valueIndex >= namesAndValues.size()) { 248 return null; 249 } 250 return namesAndValues.get(valueIndex); 251 } 252 253 /** Returns the last value corresponding to the specified field, or null. */ 254 public String get(String fieldName) { 255 for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { 256 if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { 257 return namesAndValues.get(i + 1); 258 } 259 } 260 return null; 261 } 262 263 /** Returns an immutable list of the header values for {@code name}. */ 264 public List<String> values(String name) { 265 List<String> result = null; 266 for (int i = 0; i < length(); i++) { 267 if (name.equalsIgnoreCase(getFieldName(i))) { 268 if (result == null) result = new ArrayList<String>(2); 269 result.add(getValue(i)); 270 } 271 } 272 return result != null 273 ? Collections.unmodifiableList(result) 274 : Collections.<String>emptyList(); 275 } 276 277 /** @param fieldNames a case-insensitive set of HTTP header field names. */ 278 public RawHeaders getAll(Set<String> fieldNames) { 279 RawHeaders result = new RawHeaders(); 280 for (int i = 0; i < namesAndValues.size(); i += 2) { 281 String fieldName = namesAndValues.get(i); 282 if (fieldNames.contains(fieldName)) { 283 result.add(fieldName, namesAndValues.get(i + 1)); 284 } 285 } 286 return result; 287 } 288 289 /** Returns bytes of a request header for sending on an HTTP transport. */ 290 public byte[] toBytes() throws UnsupportedEncodingException { 291 StringBuilder result = new StringBuilder(256); 292 result.append(requestLine).append("\r\n"); 293 for (int i = 0; i < namesAndValues.size(); i += 2) { 294 result.append(namesAndValues.get(i)) 295 .append(": ") 296 .append(namesAndValues.get(i + 1)) 297 .append("\r\n"); 298 } 299 result.append("\r\n"); 300 return result.toString().getBytes("ISO-8859-1"); 301 } 302 303 /** Parses bytes of a response header from an HTTP transport. */ 304 public static RawHeaders fromBytes(InputStream in) throws IOException { 305 RawHeaders headers; 306 do { 307 headers = new RawHeaders(); 308 headers.setStatusLine(Util.readAsciiLine(in)); 309 readHeaders(in, headers); 310 } while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE); 311 return headers; 312 } 313 314 /** Reads headers or trailers into {@code out}. */ 315 public static void readHeaders(InputStream in, RawHeaders out) throws IOException { 316 // parse the result headers until the first blank line 317 String line; 318 while ((line = Util.readAsciiLine(in)).length() != 0) { 319 out.addLine(line); 320 } 321 } 322 323 /** 324 * Returns an immutable map containing each field to its list of values. The 325 * status line is mapped to null. 326 */ 327 public Map<String, List<String>> toMultimap(boolean response) { 328 Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR); 329 for (int i = 0; i < namesAndValues.size(); i += 2) { 330 String fieldName = namesAndValues.get(i); 331 String value = namesAndValues.get(i + 1); 332 333 List<String> allValues = new ArrayList<String>(); 334 List<String> otherValues = result.get(fieldName); 335 if (otherValues != null) { 336 allValues.addAll(otherValues); 337 } 338 allValues.add(value); 339 result.put(fieldName, Collections.unmodifiableList(allValues)); 340 } 341 if (response && statusLine != null) { 342 result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine))); 343 } else if (requestLine != null) { 344 result.put(null, Collections.unmodifiableList(Collections.singletonList(requestLine))); 345 } 346 return Collections.unmodifiableMap(result); 347 } 348 349 /** 350 * Creates a new instance from the given map of fields to values. If 351 * present, the null field's last element will be used to set the status 352 * line. 353 */ 354 public static RawHeaders fromMultimap(Map<String, List<String>> map, boolean response) 355 throws IOException { 356 if (!response) throw new UnsupportedOperationException(); 357 RawHeaders result = new RawHeaders(); 358 for (Entry<String, List<String>> entry : map.entrySet()) { 359 String fieldName = entry.getKey(); 360 List<String> values = entry.getValue(); 361 if (fieldName != null) { 362 for (String value : values) { 363 result.addLenient(fieldName, value); 364 } 365 } else if (!values.isEmpty()) { 366 result.setStatusLine(values.get(values.size() - 1)); 367 } 368 } 369 return result; 370 } 371 372 /** 373 * Returns a list of alternating names and values. Names are all lower case. 374 * No names are repeated. If any name has multiple values, they are 375 * concatenated using "\0" as a delimiter. 376 */ 377 public List<String> toNameValueBlock() { 378 Set<String> names = new HashSet<String>(); 379 List<String> result = new ArrayList<String>(); 380 for (int i = 0; i < namesAndValues.size(); i += 2) { 381 String name = namesAndValues.get(i).toLowerCase(Locale.US); 382 String value = namesAndValues.get(i + 1); 383 384 // Drop headers that are forbidden when layering HTTP over SPDY. 385 if (name.equals("connection") 386 || name.equals("host") 387 || name.equals("keep-alive") 388 || name.equals("proxy-connection") 389 || name.equals("transfer-encoding")) { 390 continue; 391 } 392 393 // If we haven't seen this name before, add the pair to the end of the list... 394 if (names.add(name)) { 395 result.add(name); 396 result.add(value); 397 continue; 398 } 399 400 // ...otherwise concatenate the existing values and this value. 401 for (int j = 0; j < result.size(); j += 2) { 402 if (name.equals(result.get(j))) { 403 result.set(j + 1, result.get(j + 1) + "\0" + value); 404 break; 405 } 406 } 407 } 408 return result; 409 } 410 411 /** Returns headers for a name value block containing a SPDY response. */ 412 public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) throws IOException { 413 if (nameValueBlock.size() % 2 != 0) { 414 throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock); 415 } 416 String status = null; 417 String version = null; 418 RawHeaders result = new RawHeaders(); 419 for (int i = 0; i < nameValueBlock.size(); i += 2) { 420 String name = nameValueBlock.get(i); 421 String values = nameValueBlock.get(i + 1); 422 for (int start = 0; start < values.length(); ) { 423 int end = values.indexOf('\0', start); 424 if (end == -1) { 425 end = values.length(); 426 } 427 String value = values.substring(start, end); 428 if (":status".equals(name)) { 429 status = value; 430 } else if (":version".equals(name)) { 431 version = value; 432 } else { 433 result.namesAndValues.add(name); 434 result.namesAndValues.add(value); 435 } 436 start = end + 1; 437 } 438 } 439 if (status == null) throw new ProtocolException("Expected ':status' header not present"); 440 if (version == null) throw new ProtocolException("Expected ':version' header not present"); 441 result.setStatusLine(version + " " + status); 442 return result; 443 } 444 } 445