Home | History | Annotate | Download | only in http
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.squareup.okhttp.internal.http;
     18 
     19 import com.squareup.okhttp.Headers;
     20 import com.squareup.okhttp.Protocol;
     21 import com.squareup.okhttp.Request;
     22 import com.squareup.okhttp.Response;
     23 import com.squareup.okhttp.internal.Util;
     24 import com.squareup.okhttp.internal.spdy.ErrorCode;
     25 import com.squareup.okhttp.internal.spdy.Header;
     26 import com.squareup.okhttp.internal.spdy.SpdyConnection;
     27 import com.squareup.okhttp.internal.spdy.SpdyStream;
     28 import java.io.IOException;
     29 import java.io.OutputStream;
     30 import java.net.CacheRequest;
     31 import java.net.ProtocolException;
     32 import java.util.ArrayList;
     33 import java.util.LinkedHashSet;
     34 import java.util.List;
     35 import java.util.Locale;
     36 import java.util.Set;
     37 import okio.ByteString;
     38 import okio.Deadline;
     39 import okio.OkBuffer;
     40 import okio.Okio;
     41 import okio.Sink;
     42 import okio.Source;
     43 
     44 import static com.squareup.okhttp.internal.spdy.Header.RESPONSE_STATUS;
     45 import static com.squareup.okhttp.internal.spdy.Header.TARGET_AUTHORITY;
     46 import static com.squareup.okhttp.internal.spdy.Header.TARGET_HOST;
     47 import static com.squareup.okhttp.internal.spdy.Header.TARGET_METHOD;
     48 import static com.squareup.okhttp.internal.spdy.Header.TARGET_PATH;
     49 import static com.squareup.okhttp.internal.spdy.Header.TARGET_SCHEME;
     50 import static com.squareup.okhttp.internal.spdy.Header.VERSION;
     51 
     52 public final class SpdyTransport implements Transport {
     53   /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */
     54   private static final List<ByteString> SPDY_3_PROHIBITED_HEADERS = Util.immutableList(
     55       ByteString.encodeUtf8("connection"),
     56       ByteString.encodeUtf8("host"),
     57       ByteString.encodeUtf8("keep-alive"),
     58       ByteString.encodeUtf8("proxy-connection"),
     59       ByteString.encodeUtf8("transfer-encoding"));
     60 
     61   /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */
     62   private static final List<ByteString> HTTP_2_PROHIBITED_HEADERS = Util.immutableList(
     63       ByteString.encodeUtf8("connection"),
     64       ByteString.encodeUtf8("host"),
     65       ByteString.encodeUtf8("keep-alive"),
     66       ByteString.encodeUtf8("proxy-connection"),
     67       ByteString.encodeUtf8("te"),
     68       ByteString.encodeUtf8("transfer-encoding"),
     69       ByteString.encodeUtf8("encoding"),
     70       ByteString.encodeUtf8("upgrade"));
     71 
     72   private final HttpEngine httpEngine;
     73   private final SpdyConnection spdyConnection;
     74   private SpdyStream stream;
     75 
     76   public SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) {
     77     this.httpEngine = httpEngine;
     78     this.spdyConnection = spdyConnection;
     79   }
     80 
     81   @Override public Sink createRequestBody(Request request) throws IOException {
     82     // TODO: if bufferRequestBody is set, we must buffer the whole request
     83     writeRequestHeaders(request);
     84     return stream.getSink();
     85   }
     86 
     87   @Override public void writeRequestHeaders(Request request) throws IOException {
     88     if (stream != null) return;
     89 
     90     httpEngine.writingRequestHeaders();
     91     boolean hasRequestBody = httpEngine.hasRequestBody();
     92     boolean hasResponseBody = true;
     93     String version = RequestLine.version(httpEngine.getConnection().getHttpMinorVersion());
     94     stream = spdyConnection.newStream(
     95         writeNameValueBlock(request, spdyConnection.getProtocol(), version), hasRequestBody,
     96         hasResponseBody);
     97     stream.setReadTimeout(httpEngine.client.getReadTimeout());
     98   }
     99 
    100   @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
    101     throw new UnsupportedOperationException();
    102   }
    103 
    104   @Override public void flushRequest() throws IOException {
    105     stream.getSink().close();
    106   }
    107 
    108   @Override public Response.Builder readResponseHeaders() throws IOException {
    109     return readNameValueBlock(stream.getResponseHeaders(), spdyConnection.getProtocol());
    110   }
    111 
    112   /**
    113    * Returns a list of alternating names and values containing a SPDY request.
    114    * Names are all lowercase. No names are repeated. If any name has multiple
    115    * values, they are concatenated using "\0" as a delimiter.
    116    */
    117   public static List<Header> writeNameValueBlock(Request request, Protocol protocol,
    118       String version) {
    119     Headers headers = request.headers();
    120     // TODO: make the known header names constants.
    121     List<Header> result = new ArrayList<Header>(headers.size() + 10);
    122     result.add(new Header(TARGET_METHOD, request.method()));
    123     result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.url())));
    124     String host = HttpEngine.hostHeader(request.url());
    125     if (Protocol.SPDY_3 == protocol) {
    126       result.add(new Header(VERSION, version));
    127       result.add(new Header(TARGET_HOST, host));
    128     } else if (Protocol.HTTP_2 == protocol) {
    129       result.add(new Header(TARGET_AUTHORITY, host));
    130     } else {
    131       throw new AssertionError();
    132     }
    133     result.add(new Header(TARGET_SCHEME, request.url().getProtocol()));
    134 
    135     Set<ByteString> names = new LinkedHashSet<ByteString>();
    136     for (int i = 0; i < headers.size(); i++) {
    137       // header names must be lowercase.
    138       ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
    139       String value = headers.value(i);
    140 
    141       // Drop headers that are forbidden when layering HTTP over SPDY.
    142       if (isProhibitedHeader(protocol, name)) continue;
    143 
    144       // They shouldn't be set, but if they are, drop them. We've already written them!
    145       if (name.equals(TARGET_METHOD)
    146           || name.equals(TARGET_PATH)
    147           || name.equals(TARGET_SCHEME)
    148           || name.equals(TARGET_AUTHORITY)
    149           || name.equals(TARGET_HOST)
    150           || name.equals(VERSION)) {
    151         continue;
    152       }
    153 
    154       // If we haven't seen this name before, add the pair to the end of the list...
    155       if (names.add(name)) {
    156         result.add(new Header(name, value));
    157         continue;
    158       }
    159 
    160       // ...otherwise concatenate the existing values and this value.
    161       for (int j = 0; j < result.size(); j++) {
    162         if (result.get(j).name.equals(name)) {
    163           String concatenated = joinOnNull(result.get(j).value.utf8(), value);
    164           result.set(j, new Header(name, concatenated));
    165           break;
    166         }
    167       }
    168     }
    169     return result;
    170   }
    171 
    172   private static String joinOnNull(String first, String second) {
    173     return new StringBuilder(first).append('\0').append(second).toString();
    174   }
    175 
    176   /** Returns headers for a name value block containing a SPDY response. */
    177   public static Response.Builder readNameValueBlock(List<Header> headerBlock,
    178       Protocol protocol) throws IOException {
    179     String status = null;
    180     String version = "HTTP/1.1"; // :version present only in spdy/3.
    181 
    182     Headers.Builder headersBuilder = new Headers.Builder();
    183     headersBuilder.set(OkHeaders.SELECTED_PROTOCOL, protocol.name.utf8());
    184     for (int i = 0; i < headerBlock.size(); i++) {
    185       ByteString name = headerBlock.get(i).name;
    186       String values = headerBlock.get(i).value.utf8();
    187       for (int start = 0; start < values.length(); ) {
    188         int end = values.indexOf('\0', start);
    189         if (end == -1) {
    190           end = values.length();
    191         }
    192         String value = values.substring(start, end);
    193         if (name.equals(RESPONSE_STATUS)) {
    194           status = value;
    195         } else if (name.equals(VERSION)) {
    196           version = value;
    197         } else if (!isProhibitedHeader(protocol, name)) { // Don't write forbidden headers!
    198           headersBuilder.add(name.utf8(), value);
    199         }
    200         start = end + 1;
    201       }
    202     }
    203     if (status == null) throw new ProtocolException("Expected ':status' header not present");
    204     if (version == null) throw new ProtocolException("Expected ':version' header not present");
    205 
    206     return new Response.Builder()
    207         .statusLine(new StatusLine(version + " " + status))
    208         .headers(headersBuilder.build());
    209   }
    210 
    211   @Override public void emptyTransferStream() {
    212     // Do nothing.
    213   }
    214 
    215   @Override public Source getTransferStream(CacheRequest cacheRequest) throws IOException {
    216     return new SpdySource(stream, cacheRequest);
    217   }
    218 
    219   @Override public void releaseConnectionOnIdle() {
    220   }
    221 
    222   @Override public void disconnect(HttpEngine engine) throws IOException {
    223     stream.close(ErrorCode.CANCEL);
    224   }
    225 
    226   @Override public boolean canReuseConnection() {
    227     return true; // TODO: spdyConnection.isClosed() ?
    228   }
    229 
    230   /** When true, this header should not be emitted or consumed. */
    231   private static boolean isProhibitedHeader(Protocol protocol, ByteString name) {
    232     if (protocol == Protocol.SPDY_3) {
    233       return SPDY_3_PROHIBITED_HEADERS.contains(name);
    234     } else if (protocol == Protocol.HTTP_2) {
    235       return HTTP_2_PROHIBITED_HEADERS.contains(name);
    236     } else {
    237       throw new AssertionError(protocol);
    238     }
    239   }
    240 
    241   /** An HTTP message body terminated by the end of the underlying stream. */
    242   private static class SpdySource implements Source {
    243     private final SpdyStream stream;
    244     private final Source source;
    245     private final CacheRequest cacheRequest;
    246     private final OutputStream cacheBody;
    247 
    248     private boolean inputExhausted;
    249     private boolean closed;
    250 
    251     SpdySource(SpdyStream stream, CacheRequest cacheRequest) throws IOException {
    252       this.stream = stream;
    253       this.source = stream.getSource();
    254 
    255       // Some apps return a null body; for compatibility we treat that like a null cache request.
    256       OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
    257       if (cacheBody == null) {
    258         cacheRequest = null;
    259       }
    260 
    261       this.cacheBody = cacheBody;
    262       this.cacheRequest = cacheRequest;
    263     }
    264 
    265     @Override public long read(OkBuffer sink, long byteCount)
    266         throws IOException {
    267       if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    268       if (closed) throw new IllegalStateException("closed");
    269       if (inputExhausted) return -1;
    270 
    271       long read = source.read(sink, byteCount);
    272       if (read == -1) {
    273         inputExhausted = true;
    274         if (cacheRequest != null) {
    275           cacheBody.close();
    276         }
    277         return -1;
    278       }
    279 
    280       if (cacheBody != null) {
    281         Okio.copy(sink, sink.size() - read, read, cacheBody);
    282       }
    283 
    284       return read;
    285     }
    286 
    287     @Override public Source deadline(Deadline deadline) {
    288       source.deadline(deadline);
    289       return this;
    290     }
    291 
    292     @Override public void close() throws IOException {
    293       if (closed) return;
    294 
    295       if (!inputExhausted && cacheBody != null) {
    296         discardStream(); // Could make inputExhausted true!
    297       }
    298 
    299       closed = true;
    300 
    301       if (!inputExhausted) {
    302         stream.closeLater(ErrorCode.CANCEL);
    303         if (cacheRequest != null) {
    304           cacheRequest.abort();
    305         }
    306       }
    307     }
    308 
    309     private boolean discardStream() {
    310       try {
    311         long socketTimeout = stream.getReadTimeoutMillis();
    312         stream.setReadTimeout(socketTimeout);
    313         stream.setReadTimeout(DISCARD_STREAM_TIMEOUT_MILLIS);
    314         try {
    315           Util.skipAll(this, DISCARD_STREAM_TIMEOUT_MILLIS);
    316           return true;
    317         } finally {
    318           stream.setReadTimeout(socketTimeout);
    319         }
    320       } catch (IOException e) {
    321         return false;
    322       }
    323     }
    324   }
    325 }
    326