Home | History | Annotate | Download | only in http
      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