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 libcore.net.http;
     19 
     20 import com.squareup.okhttp.OkHttpConnection;
     21 import java.io.ByteArrayInputStream;
     22 import java.io.IOException;
     23 import java.io.InputStream;
     24 import java.io.OutputStream;
     25 import java.net.CacheRequest;
     26 import java.net.CacheResponse;
     27 import java.net.CookieHandler;
     28 import java.net.Proxy;
     29 import java.net.ResponseCache;
     30 import java.net.URI;
     31 import java.net.URISyntaxException;
     32 import java.net.URL;
     33 import java.util.Collections;
     34 import java.util.Date;
     35 import java.util.HashMap;
     36 import java.util.List;
     37 import java.util.Map;
     38 import java.util.zip.GZIPInputStream;
     39 import javax.net.ssl.SSLSocketFactory;
     40 import libcore.io.IoUtils;
     41 import libcore.util.EmptyArray;
     42 import libcore.util.ExtendedResponseCache;
     43 import libcore.util.Libcore;
     44 import libcore.util.ResponseSource;
     45 
     46 /**
     47  * Handles a single HTTP request/response pair. Each HTTP engine follows this
     48  * lifecycle:
     49  * <ol>
     50  *     <li>It is created.
     51  *     <li>The HTTP request message is sent with sendRequest(). Once the request
     52  *         is sent it is an error to modify the request headers. After
     53  *         sendRequest() has been called the request body can be written to if
     54  *         it exists.
     55  *     <li>The HTTP response message is read with readResponse(). After the
     56  *         response has been read the response headers and body can be read.
     57  *         All responses have a response body input stream, though in some
     58  *         instances this stream is empty.
     59  * </ol>
     60  *
     61  * <p>The request and response may be served by the HTTP response cache, by the
     62  * network, or by both in the event of a conditional GET.
     63  *
     64  * <p>This class may hold a socket connection that needs to be released or
     65  * recycled. By default, this socket connection is held when the last byte of
     66  * the response is consumed. To release the connection when it is no longer
     67  * required, use {@link #automaticallyReleaseConnectionToPool()}.
     68  */
     69 public class HttpEngine {
     70     private static final CacheResponse BAD_GATEWAY_RESPONSE = new CacheResponse() {
     71         @Override public Map<String, List<String>> getHeaders() throws IOException {
     72             Map<String, List<String>> result = new HashMap<String, List<String>>();
     73             result.put(null, Collections.singletonList("HTTP/1.1 502 Bad Gateway"));
     74             return result;
     75         }
     76         @Override public InputStream getBody() throws IOException {
     77             return new ByteArrayInputStream(EmptyArray.BYTE);
     78         }
     79     };
     80     public static final int DEFAULT_CHUNK_LENGTH = 1024;
     81 
     82     public static final String OPTIONS = "OPTIONS";
     83     public static final String GET = "GET";
     84     public static final String HEAD = "HEAD";
     85     public static final String POST = "POST";
     86     public static final String PUT = "PUT";
     87     public static final String DELETE = "DELETE";
     88     public static final String TRACE = "TRACE";
     89     public static final String CONNECT = "CONNECT";
     90 
     91     public static final int HTTP_CONTINUE = 100;
     92 
     93     protected final HttpURLConnectionImpl policy;
     94 
     95     protected final String method;
     96 
     97     private ResponseSource responseSource;
     98 
     99     protected HttpConnection connection;
    100     private OutputStream requestBodyOut;
    101 
    102     private Transport transport;
    103 
    104     private InputStream responseBodyIn;
    105 
    106     private final ResponseCache responseCache = ResponseCache.getDefault();
    107     private CacheResponse cacheResponse;
    108     private CacheRequest cacheRequest;
    109 
    110     /** The time when the request headers were written, or -1 if they haven't been written yet. */
    111     long sentRequestMillis = -1;
    112 
    113     /**
    114      * True if this client added an "Accept-Encoding: gzip" header field and is
    115      * therefore responsible for also decompressing the transfer stream.
    116      */
    117     private boolean transparentGzip;
    118 
    119     final URI uri;
    120 
    121     final RequestHeaders requestHeaders;
    122 
    123     /** Null until a response is received from the network or the cache. */
    124     ResponseHeaders responseHeaders;
    125 
    126     /*
    127      * The cache response currently being validated on a conditional get. Null
    128      * if the cached response doesn't exist or doesn't need validation. If the
    129      * conditional get succeeds, these will be used for the response headers and
    130      * body. If it fails, these be closed and set to null.
    131      */
    132     private ResponseHeaders cachedResponseHeaders;
    133     private InputStream cachedResponseBody;
    134 
    135     /**
    136      * True if the socket connection should be released to the connection pool
    137      * when the response has been fully read.
    138      */
    139     private boolean automaticallyReleaseConnectionToPool;
    140 
    141     /** True if the socket connection is no longer needed by this engine. */
    142     private boolean connectionReleased;
    143 
    144     /**
    145      * @param requestHeaders the client's supplied request headers. This class
    146      *     creates a private copy that it can mutate.
    147      * @param connection the connection used for an intermediate response
    148      *     immediately prior to this request/response pair, such as a same-host
    149      *     redirect. This engine assumes ownership of the connection and must
    150      *     release it when it is unneeded.
    151      */
    152     public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
    153             HttpConnection connection, RetryableOutputStream requestBodyOut) throws IOException {
    154         this.policy = policy;
    155         this.method = method;
    156         this.connection = connection;
    157         this.requestBodyOut = requestBodyOut;
    158 
    159         try {
    160             uri = Libcore.toUriLenient(policy.getURL());
    161         } catch (URISyntaxException e) {
    162             throw new IOException(e);
    163         }
    164 
    165         this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
    166     }
    167 
    168     public URI getUri() {
    169         return uri;
    170     }
    171 
    172     /**
    173      * Figures out what the response source will be, and opens a socket to that
    174      * source if necessary. Prepares the request headers and gets ready to start
    175      * writing the request body if it exists.
    176      */
    177     public final void sendRequest() throws IOException {
    178         if (responseSource != null) {
    179             return;
    180         }
    181 
    182         prepareRawRequestHeaders();
    183         initResponseSource();
    184         if (responseCache instanceof ExtendedResponseCache) {
    185             ((ExtendedResponseCache) responseCache).trackResponse(responseSource);
    186         }
    187 
    188         /*
    189          * The raw response source may require the network, but the request
    190          * headers may forbid network use. In that case, dispose of the network
    191          * response and use a BAD_GATEWAY response instead.
    192          */
    193         if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) {
    194             if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
    195                 IoUtils.closeQuietly(cachedResponseBody);
    196             }
    197             this.responseSource = ResponseSource.CACHE;
    198             this.cacheResponse = BAD_GATEWAY_RESPONSE;
    199             RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders());
    200             setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody());
    201         }
    202 
    203         if (responseSource.requiresConnection()) {
    204             sendSocketRequest();
    205         } else if (connection != null) {
    206             HttpConnectionPool.INSTANCE.recycle(connection);
    207             connection = null;
    208         }
    209     }
    210 
    211     /**
    212      * Initialize the source for this response. It may be corrected later if the
    213      * request headers forbids network use.
    214      */
    215     private void initResponseSource() throws IOException {
    216         responseSource = ResponseSource.NETWORK;
    217         if (!policy.getUseCaches() || responseCache == null) {
    218             return;
    219         }
    220 
    221         CacheResponse candidate = responseCache.get(uri, method,
    222                 requestHeaders.getHeaders().toMultimap());
    223         if (candidate == null) {
    224             return;
    225         }
    226 
    227         Map<String, List<String>> responseHeadersMap = candidate.getHeaders();
    228         cachedResponseBody = candidate.getBody();
    229         if (!acceptCacheResponseType(candidate)
    230                 || responseHeadersMap == null
    231                 || cachedResponseBody == null) {
    232             IoUtils.closeQuietly(cachedResponseBody);
    233             return;
    234         }
    235 
    236         RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap);
    237         cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders);
    238         long now = System.currentTimeMillis();
    239         this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders);
    240         if (responseSource == ResponseSource.CACHE) {
    241             this.cacheResponse = candidate;
    242             setResponse(cachedResponseHeaders, cachedResponseBody);
    243         } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
    244             this.cacheResponse = candidate;
    245         } else if (responseSource == ResponseSource.NETWORK) {
    246             IoUtils.closeQuietly(cachedResponseBody);
    247         } else {
    248             throw new AssertionError();
    249         }
    250     }
    251 
    252     private void sendSocketRequest() throws IOException {
    253         if (connection == null) {
    254             connect();
    255         }
    256 
    257         if (transport != null) {
    258             throw new IllegalStateException();
    259         }
    260 
    261         transport = connection.newTransport(this);
    262 
    263         if (hasRequestBody() && requestBodyOut == null) {
    264             // Create a request body if we don't have one already. We'll already
    265             // have one if we're retrying a failed POST.
    266             requestBodyOut = transport.createRequestBody();
    267         }
    268     }
    269 
    270     /**
    271      * Connect to the origin server either directly or via a proxy.
    272      */
    273     protected void connect() throws IOException {
    274         if (connection == null) {
    275             connection = openSocketConnection();
    276         }
    277     }
    278 
    279     protected final HttpConnection openSocketConnection() throws IOException {
    280         HttpConnection result = HttpConnection.connect(uri, getSslSocketFactory(),
    281                 policy.getProxy(), requiresTunnel(), policy.getConnectTimeout());
    282         Proxy proxy = result.getAddress().getProxy();
    283         if (proxy != null) {
    284             policy.setProxy(proxy);
    285             // Add the authority to the request line when we're using a proxy.
    286             requestHeaders.getHeaders().setStatusLine(getRequestLine());
    287         }
    288         result.setSoTimeout(policy.getReadTimeout());
    289         return result;
    290     }
    291 
    292     /**
    293      * @param body the response body, or null if it doesn't exist or isn't
    294      *     available.
    295      */
    296     private void setResponse(ResponseHeaders headers, InputStream body) throws IOException {
    297         if (this.responseBodyIn != null) {
    298             throw new IllegalStateException();
    299         }
    300         this.responseHeaders = headers;
    301         if (body != null) {
    302             initContentStream(body);
    303         }
    304     }
    305 
    306     boolean hasRequestBody() {
    307         return method == POST || method == PUT;
    308     }
    309 
    310     /**
    311      * Returns the request body or null if this request doesn't have a body.
    312      */
    313     public final OutputStream getRequestBody() {
    314         if (responseSource == null) {
    315             throw new IllegalStateException();
    316         }
    317         return requestBodyOut;
    318     }
    319 
    320     public final boolean hasResponse() {
    321         return responseHeaders != null;
    322     }
    323 
    324     public final RequestHeaders getRequestHeaders() {
    325         return requestHeaders;
    326     }
    327 
    328     public final ResponseHeaders getResponseHeaders() {
    329         if (responseHeaders == null) {
    330             throw new IllegalStateException();
    331         }
    332         return responseHeaders;
    333     }
    334 
    335     public final int getResponseCode() {
    336         if (responseHeaders == null) {
    337             throw new IllegalStateException();
    338         }
    339         return responseHeaders.getHeaders().getResponseCode();
    340     }
    341 
    342     public final InputStream getResponseBody() {
    343         if (responseHeaders == null) {
    344             throw new IllegalStateException();
    345         }
    346         return responseBodyIn;
    347     }
    348 
    349     public final CacheResponse getCacheResponse() {
    350         return cacheResponse;
    351     }
    352 
    353     public final HttpConnection getConnection() {
    354         return connection;
    355     }
    356 
    357     public final boolean hasRecycledConnection() {
    358         return connection != null && connection.isRecycled();
    359     }
    360 
    361     /**
    362      * Returns true if {@code cacheResponse} is of the right type. This
    363      * condition is necessary but not sufficient for the cached response to
    364      * be used.
    365      */
    366     protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
    367         return true;
    368     }
    369 
    370     private void maybeCache() throws IOException {
    371         // Are we caching at all?
    372         if (!policy.getUseCaches() || responseCache == null) {
    373             return;
    374         }
    375 
    376         // Should we cache this response for this request?
    377         if (!responseHeaders.isCacheable(requestHeaders)) {
    378             return;
    379         }
    380 
    381         // Offer this request to the cache.
    382         cacheRequest = responseCache.put(uri, getHttpConnectionToCache());
    383     }
    384 
    385     protected OkHttpConnection getHttpConnectionToCache() {
    386         return policy;
    387     }
    388 
    389     /**
    390      * Cause the socket connection to be released to the connection pool when
    391      * it is no longer needed. If it is already unneeded, it will be pooled
    392      * immediately. Otherwise the connection is held so that redirects can be
    393      * handled by the same connection.
    394      */
    395     public final void automaticallyReleaseConnectionToPool() {
    396         automaticallyReleaseConnectionToPool = true;
    397         if (connection != null && connectionReleased) {
    398             HttpConnectionPool.INSTANCE.recycle(connection);
    399             connection = null;
    400         }
    401     }
    402 
    403     /**
    404      * Releases this engine so that its resources may be either reused or
    405      * closed. Also call {@link #automaticallyReleaseConnectionToPool} unless
    406      * the connection will be used to follow a redirect.
    407      */
    408     public final void release(boolean reusable) {
    409         // If the response body comes from the cache, close it.
    410         if (responseBodyIn == cachedResponseBody) {
    411             IoUtils.closeQuietly(responseBodyIn);
    412         }
    413 
    414         if (!connectionReleased && connection != null) {
    415             connectionReleased = true;
    416 
    417             if (!reusable || !transport.makeReusable(requestBodyOut, responseBodyIn)) {
    418                 connection.closeSocketAndStreams();
    419                 connection = null;
    420             } else if (automaticallyReleaseConnectionToPool) {
    421                 HttpConnectionPool.INSTANCE.recycle(connection);
    422                 connection = null;
    423             }
    424         }
    425     }
    426 
    427     private void initContentStream(InputStream transferStream) throws IOException {
    428         if (transparentGzip && responseHeaders.isContentEncodingGzip()) {
    429             /*
    430              * If the response was transparently gzipped, remove the gzip header field
    431              * so clients don't double decompress. http://b/3009828
    432              */
    433             responseHeaders.stripContentEncoding();
    434             responseBodyIn = new GZIPInputStream(transferStream);
    435         } else {
    436             responseBodyIn = transferStream;
    437         }
    438     }
    439 
    440     /**
    441      * Returns true if the response must have a (possibly 0-length) body.
    442      * See RFC 2616 section 4.3.
    443      */
    444     public final boolean hasResponseBody() {
    445         int responseCode = responseHeaders.getHeaders().getResponseCode();
    446 
    447         // HEAD requests never yield a body regardless of the response headers.
    448         if (method == HEAD) {
    449             return false;
    450         }
    451 
    452         if (method != CONNECT
    453                 && (responseCode < HTTP_CONTINUE || responseCode >= 200)
    454                 && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
    455                 && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) {
    456             return true;
    457         }
    458 
    459         /*
    460          * If the Content-Length or Transfer-Encoding headers disagree with the
    461          * response code, the response is malformed. For best compatibility, we
    462          * honor the headers.
    463          */
    464         if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) {
    465             return true;
    466         }
    467 
    468         return false;
    469     }
    470 
    471     /**
    472      * Populates requestHeaders with defaults and cookies.
    473      *
    474      * <p>This client doesn't specify a default {@code Accept} header because it
    475      * doesn't know what content types the application is interested in.
    476      */
    477     private void prepareRawRequestHeaders() throws IOException {
    478         requestHeaders.getHeaders().setStatusLine(getRequestLine());
    479 
    480         if (requestHeaders.getUserAgent() == null) {
    481             requestHeaders.setUserAgent(getDefaultUserAgent());
    482         }
    483 
    484         if (requestHeaders.getHost() == null) {
    485             requestHeaders.setHost(getOriginAddress(policy.getURL()));
    486         }
    487 
    488         // TODO: this shouldn't be set for SPDY (it's ignored)
    489         if ((connection == null || connection.httpMinorVersion != 0)
    490                 && requestHeaders.getConnection() == null) {
    491             requestHeaders.setConnection("Keep-Alive");
    492         }
    493 
    494         if (requestHeaders.getAcceptEncoding() == null) {
    495             transparentGzip = true;
    496             // TODO: this shouldn't be set for SPDY (it isn't necessary)
    497             requestHeaders.setAcceptEncoding("gzip");
    498         }
    499 
    500         if (hasRequestBody() && requestHeaders.getContentType() == null) {
    501             requestHeaders.setContentType("application/x-www-form-urlencoded");
    502         }
    503 
    504         long ifModifiedSince = policy.getIfModifiedSince();
    505         if (ifModifiedSince != 0) {
    506             requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
    507         }
    508 
    509         CookieHandler cookieHandler = CookieHandler.getDefault();
    510         if (cookieHandler != null) {
    511             requestHeaders.addCookies(
    512                     cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap()));
    513         }
    514     }
    515 
    516     /**
    517      * Returns the request status line, like "GET / HTTP/1.1". This is exposed
    518      * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so
    519      * it needs to be set even if the transport is SPDY.
    520      */
    521     String getRequestLine() {
    522         String protocol = (connection == null || connection.httpMinorVersion != 0)
    523                 ? "HTTP/1.1"
    524                 : "HTTP/1.0";
    525         return method + " " + requestString() + " " + protocol;
    526     }
    527 
    528     private String requestString() {
    529         URL url = policy.getURL();
    530         if (includeAuthorityInRequestLine()) {
    531             return url.toString();
    532         } else {
    533             String fileOnly = url.getFile();
    534             if (fileOnly == null) {
    535                 fileOnly = "/";
    536             } else if (!fileOnly.startsWith("/")) {
    537                 fileOnly = "/" + fileOnly;
    538             }
    539             return fileOnly;
    540         }
    541     }
    542 
    543     /**
    544      * Returns true if the request line should contain the full URL with host
    545      * and port (like "GET http://android.com/foo HTTP/1.1") or only the path
    546      * (like "GET /foo HTTP/1.1").
    547      *
    548      * <p>This is non-final because for HTTPS it's never necessary to supply the
    549      * full URL, even if a proxy is in use.
    550      */
    551     protected boolean includeAuthorityInRequestLine() {
    552         return policy.usingProxy();
    553     }
    554 
    555     /**
    556      * Returns the SSL configuration for connections created by this engine.
    557      * We cannot reuse HTTPS connections if the socket factory has changed.
    558      */
    559     protected SSLSocketFactory getSslSocketFactory() {
    560         return null;
    561     }
    562 
    563     protected final String getDefaultUserAgent() {
    564         String agent = System.getProperty("http.agent");
    565         return agent != null ? agent : ("Java" + System.getProperty("java.version"));
    566     }
    567 
    568     protected final String getOriginAddress(URL url) {
    569         int port = url.getPort();
    570         String result = url.getHost();
    571         if (port > 0 && port != policy.getDefaultPort()) {
    572             result = result + ":" + port;
    573         }
    574         return result;
    575     }
    576 
    577     protected boolean requiresTunnel() {
    578         return false;
    579     }
    580 
    581     /**
    582      * Flushes the remaining request header and body, parses the HTTP response
    583      * headers and starts reading the HTTP response body if it exists.
    584      */
    585     public final void readResponse() throws IOException {
    586         if (hasResponse()) {
    587             return;
    588         }
    589 
    590         if (responseSource == null) {
    591             throw new IllegalStateException("readResponse() without sendRequest()");
    592         }
    593 
    594         if (!responseSource.requiresConnection()) {
    595             return;
    596         }
    597 
    598         if (sentRequestMillis == -1) {
    599             if (requestBodyOut instanceof RetryableOutputStream) {
    600                 int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
    601                 requestHeaders.setContentLength(contentLength);
    602             }
    603             transport.writeRequestHeaders();
    604         }
    605 
    606         if (requestBodyOut != null) {
    607             requestBodyOut.close();
    608             if (requestBodyOut instanceof RetryableOutputStream) {
    609                 transport.writeRequestBody((RetryableOutputStream) requestBodyOut);
    610             }
    611         }
    612 
    613         transport.flushRequest();
    614 
    615         responseHeaders = transport.readResponseHeaders();
    616         responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
    617 
    618         if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
    619             if (cachedResponseHeaders.validate(responseHeaders)) {
    620                 release(true);
    621                 ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
    622                 setResponse(combinedHeaders, cachedResponseBody);
    623                 if (responseCache instanceof ExtendedResponseCache) {
    624                     ExtendedResponseCache httpResponseCache = (ExtendedResponseCache) responseCache;
    625                     httpResponseCache.trackConditionalCacheHit();
    626                     httpResponseCache.update(cacheResponse, getHttpConnectionToCache());
    627                 }
    628                 return;
    629             } else {
    630                 IoUtils.closeQuietly(cachedResponseBody);
    631             }
    632         }
    633 
    634         if (hasResponseBody()) {
    635             maybeCache(); // reentrant. this calls into user code which may call back into this!
    636         }
    637 
    638         initContentStream(transport.getTransferStream(cacheRequest));
    639     }
    640 }
    641