Home | History | Annotate | Download | only in http
      1 /*
      2  * Copyright (C) 2006 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 android.net.http;
     18 
     19 import java.io.EOFException;
     20 import java.io.InputStream;
     21 import java.io.IOException;
     22 import java.util.Iterator;
     23 import java.util.Map;
     24 import java.util.Map.Entry;
     25 import java.util.zip.GZIPInputStream;
     26 
     27 import org.apache.http.entity.InputStreamEntity;
     28 import org.apache.http.Header;
     29 import org.apache.http.HttpEntity;
     30 import org.apache.http.HttpEntityEnclosingRequest;
     31 import org.apache.http.HttpException;
     32 import org.apache.http.HttpHost;
     33 import org.apache.http.HttpRequest;
     34 import org.apache.http.HttpStatus;
     35 import org.apache.http.ParseException;
     36 import org.apache.http.ProtocolVersion;
     37 
     38 import org.apache.http.StatusLine;
     39 import org.apache.http.message.BasicHttpRequest;
     40 import org.apache.http.message.BasicHttpEntityEnclosingRequest;
     41 import org.apache.http.protocol.RequestContent;
     42 
     43 /**
     44  * Represents an HTTP request for a given host.
     45  */
     46 
     47 class Request {
     48 
     49     /** The eventhandler to call as the request progresses */
     50     EventHandler mEventHandler;
     51 
     52     private Connection mConnection;
     53 
     54     /** The Apache http request */
     55     BasicHttpRequest mHttpRequest;
     56 
     57     /** The path component of this request */
     58     String mPath;
     59 
     60     /** Host serving this request */
     61     HttpHost mHost;
     62 
     63     /** Set if I'm using a proxy server */
     64     HttpHost mProxyHost;
     65 
     66     /** True if request has been cancelled */
     67     volatile boolean mCancelled = false;
     68 
     69     int mFailCount = 0;
     70 
     71     // This will be used to set the Range field if we retry a connection. This
     72     // is http/1.1 feature.
     73     private int mReceivedBytes = 0;
     74 
     75     private InputStream mBodyProvider;
     76     private int mBodyLength;
     77 
     78     private final static String HOST_HEADER = "Host";
     79     private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
     80     private final static String CONTENT_LENGTH_HEADER = "content-length";
     81 
     82     /* Used to synchronize waitUntilComplete() requests */
     83     private final Object mClientResource = new Object();
     84 
     85     /** True if loading should be paused **/
     86     private boolean mLoadingPaused = false;
     87 
     88     /**
     89      * Processor used to set content-length and transfer-encoding
     90      * headers.
     91      */
     92     private static RequestContent requestContentProcessor =
     93             new RequestContent();
     94 
     95     /**
     96      * Instantiates a new Request.
     97      * @param method GET/POST/PUT
     98      * @param host The server that will handle this request
     99      * @param path path part of URI
    100      * @param bodyProvider InputStream providing HTTP body, null if none
    101      * @param bodyLength length of body, must be 0 if bodyProvider is null
    102      * @param eventHandler request will make progress callbacks on
    103      * this interface
    104      * @param headers reqeust headers
    105      */
    106     Request(String method, HttpHost host, HttpHost proxyHost, String path,
    107             InputStream bodyProvider, int bodyLength,
    108             EventHandler eventHandler,
    109             Map<String, String> headers) {
    110         mEventHandler = eventHandler;
    111         mHost = host;
    112         mProxyHost = proxyHost;
    113         mPath = path;
    114         mBodyProvider = bodyProvider;
    115         mBodyLength = bodyLength;
    116 
    117         if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) {
    118             mHttpRequest = new BasicHttpRequest(method, getUri());
    119         } else {
    120             mHttpRequest = new BasicHttpEntityEnclosingRequest(
    121                     method, getUri());
    122             // it is ok to have null entity for BasicHttpEntityEnclosingRequest.
    123             // By using BasicHttpEntityEnclosingRequest, it will set up the
    124             // correct content-length, content-type and content-encoding.
    125             if (bodyProvider != null) {
    126                 setBodyProvider(bodyProvider, bodyLength);
    127             }
    128         }
    129         addHeader(HOST_HEADER, getHostPort());
    130 
    131         /* FIXME: if webcore will make the root document a
    132            high-priority request, we can ask for gzip encoding only on
    133            high priority reqs (saving the trouble for images, etc) */
    134         addHeader(ACCEPT_ENCODING_HEADER, "gzip");
    135         addHeaders(headers);
    136     }
    137 
    138     /**
    139      * @param pause True if the load should be paused.
    140      */
    141     synchronized void setLoadingPaused(boolean pause) {
    142         mLoadingPaused = pause;
    143 
    144         // Wake up the paused thread if we're unpausing the load.
    145         if (!mLoadingPaused) {
    146             notify();
    147         }
    148     }
    149 
    150     /**
    151      * @param connection Request served by this connection
    152      */
    153     void setConnection(Connection connection) {
    154         mConnection = connection;
    155     }
    156 
    157     /* package */ EventHandler getEventHandler() {
    158         return mEventHandler;
    159     }
    160 
    161     /**
    162      * Add header represented by given pair to request.  Header will
    163      * be formatted in request as "name: value\r\n".
    164      * @param name of header
    165      * @param value of header
    166      */
    167     void addHeader(String name, String value) {
    168         if (name == null) {
    169             String damage = "Null http header name";
    170             HttpLog.e(damage);
    171             throw new NullPointerException(damage);
    172         }
    173         if (value == null || value.length() == 0) {
    174             String damage = "Null or empty value for header \"" + name + "\"";
    175             HttpLog.e(damage);
    176             throw new RuntimeException(damage);
    177         }
    178         mHttpRequest.addHeader(name, value);
    179     }
    180 
    181     /**
    182      * Add all headers in given map to this request.  This is a helper
    183      * method: it calls addHeader for each pair in the map.
    184      */
    185     void addHeaders(Map<String, String> headers) {
    186         if (headers == null) {
    187             return;
    188         }
    189 
    190         Entry<String, String> entry;
    191         Iterator<Entry<String, String>> i = headers.entrySet().iterator();
    192         while (i.hasNext()) {
    193             entry = i.next();
    194             addHeader(entry.getKey(), entry.getValue());
    195         }
    196     }
    197 
    198     /**
    199      * Send the request line and headers
    200      */
    201     void sendRequest(AndroidHttpClientConnection httpClientConnection)
    202             throws HttpException, IOException {
    203 
    204         if (mCancelled) return; // don't send cancelled requests
    205 
    206         if (HttpLog.LOGV) {
    207             HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort());
    208             // HttpLog.v(mHttpRequest.getRequestLine().toString());
    209             if (false) {
    210                 Iterator i = mHttpRequest.headerIterator();
    211                 while (i.hasNext()) {
    212                     Header header = (Header)i.next();
    213                     HttpLog.v(header.getName() + ": " + header.getValue());
    214                 }
    215             }
    216         }
    217 
    218         requestContentProcessor.process(mHttpRequest,
    219                                         mConnection.getHttpContext());
    220         httpClientConnection.sendRequestHeader(mHttpRequest);
    221         if (mHttpRequest instanceof HttpEntityEnclosingRequest) {
    222             httpClientConnection.sendRequestEntity(
    223                     (HttpEntityEnclosingRequest) mHttpRequest);
    224         }
    225 
    226         if (HttpLog.LOGV) {
    227             HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath);
    228         }
    229     }
    230 
    231 
    232     /**
    233      * Receive a single http response.
    234      *
    235      * @param httpClientConnection the request to receive the response for.
    236      */
    237     void readResponse(AndroidHttpClientConnection httpClientConnection)
    238             throws IOException, ParseException {
    239 
    240         if (mCancelled) return; // don't send cancelled requests
    241 
    242         StatusLine statusLine = null;
    243         boolean hasBody = false;
    244         httpClientConnection.flush();
    245         int statusCode = 0;
    246 
    247         Headers header = new Headers();
    248         do {
    249             statusLine = httpClientConnection.parseResponseHeader(header);
    250             statusCode = statusLine.getStatusCode();
    251         } while (statusCode < HttpStatus.SC_OK);
    252         if (HttpLog.LOGV) HttpLog.v(
    253                 "Request.readResponseStatus() " +
    254                 statusLine.toString().length() + " " + statusLine);
    255 
    256         ProtocolVersion v = statusLine.getProtocolVersion();
    257         mEventHandler.status(v.getMajor(), v.getMinor(),
    258                 statusCode, statusLine.getReasonPhrase());
    259         mEventHandler.headers(header);
    260         HttpEntity entity = null;
    261         hasBody = canResponseHaveBody(mHttpRequest, statusCode);
    262 
    263         if (hasBody)
    264             entity = httpClientConnection.receiveResponseEntity(header);
    265 
    266         // restrict the range request to the servers claiming that they are
    267         // accepting ranges in bytes
    268         boolean supportPartialContent = "bytes".equalsIgnoreCase(header
    269                 .getAcceptRanges());
    270 
    271         if (entity != null) {
    272             InputStream is = entity.getContent();
    273 
    274             // process gzip content encoding
    275             Header contentEncoding = entity.getContentEncoding();
    276             InputStream nis = null;
    277             byte[] buf = null;
    278             int count = 0;
    279             try {
    280                 if (contentEncoding != null &&
    281                     contentEncoding.getValue().equals("gzip")) {
    282                     nis = new GZIPInputStream(is);
    283                 } else {
    284                     nis = is;
    285                 }
    286 
    287                 /* accumulate enough data to make it worth pushing it
    288                  * up the stack */
    289                 buf = mConnection.getBuf();
    290                 int len = 0;
    291                 int lowWater = buf.length / 2;
    292                 while (len != -1) {
    293                     synchronized(this) {
    294                         while (mLoadingPaused) {
    295                             // Put this (network loading) thread to sleep if WebCore
    296                             // has asked us to. This can happen with plugins for
    297                             // example, if we are streaming data but the plugin has
    298                             // filled its internal buffers.
    299                             try {
    300                                 wait();
    301                             } catch (InterruptedException e) {
    302                                 HttpLog.e("Interrupted exception whilst "
    303                                     + "network thread paused at WebCore's request."
    304                                     + " " + e.getMessage());
    305                             }
    306                         }
    307                     }
    308 
    309                     len = nis.read(buf, count, buf.length - count);
    310 
    311                     if (len != -1) {
    312                         count += len;
    313                         if (supportPartialContent) mReceivedBytes += len;
    314                     }
    315                     if (len == -1 || count >= lowWater) {
    316                         if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count);
    317                         mEventHandler.data(buf, count);
    318                         count = 0;
    319                     }
    320                 }
    321             } catch (EOFException e) {
    322                 /* InflaterInputStream throws an EOFException when the
    323                    server truncates gzipped content.  Handle this case
    324                    as we do truncated non-gzipped content: no error */
    325                 if (count > 0) {
    326                     // if there is uncommited content, we should commit them
    327                     mEventHandler.data(buf, count);
    328                 }
    329                 if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e);
    330             } catch(IOException e) {
    331                 // don't throw if we have a non-OK status code
    332                 if (statusCode == HttpStatus.SC_OK
    333                         || statusCode == HttpStatus.SC_PARTIAL_CONTENT) {
    334                     if (supportPartialContent && count > 0) {
    335                         // if there is uncommited content, we should commit them
    336                         // as we will continue the request
    337                         mEventHandler.data(buf, count);
    338                     }
    339                     throw e;
    340                 }
    341             } finally {
    342                 if (nis != null) {
    343                     nis.close();
    344                 }
    345             }
    346         }
    347         mConnection.setCanPersist(entity, statusLine.getProtocolVersion(),
    348                 header.getConnectionType());
    349         mEventHandler.endData();
    350         complete();
    351 
    352         if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " +
    353                                     mHost.getSchemeName() + "://" + getHostPort() + mPath);
    354     }
    355 
    356     /**
    357      * Data will not be sent to or received from server after cancel()
    358      * call.  Does not close connection--use close() below for that.
    359      *
    360      * Called by RequestHandle from non-network thread
    361      */
    362     synchronized void cancel() {
    363         if (HttpLog.LOGV) {
    364             HttpLog.v("Request.cancel(): " + getUri());
    365         }
    366 
    367         // Ensure that the network thread is not blocked by a hanging request from WebCore to
    368         // pause the load.
    369         mLoadingPaused = false;
    370         notify();
    371 
    372         mCancelled = true;
    373         if (mConnection != null) {
    374             mConnection.cancel();
    375         }
    376     }
    377 
    378     String getHostPort() {
    379         String myScheme = mHost.getSchemeName();
    380         int myPort = mHost.getPort();
    381 
    382         // Only send port when we must... many servers can't deal with it
    383         if (myPort != 80 && myScheme.equals("http") ||
    384             myPort != 443 && myScheme.equals("https")) {
    385             return mHost.toHostString();
    386         } else {
    387             return mHost.getHostName();
    388         }
    389     }
    390 
    391     String getUri() {
    392         if (mProxyHost == null ||
    393             mHost.getSchemeName().equals("https")) {
    394             return mPath;
    395         }
    396         return mHost.getSchemeName() + "://" + getHostPort() + mPath;
    397     }
    398 
    399     /**
    400      * for debugging
    401      */
    402     public String toString() {
    403         return mPath;
    404     }
    405 
    406 
    407     /**
    408      * If this request has been sent once and failed, it must be reset
    409      * before it can be sent again.
    410      */
    411     void reset() {
    412         /* clear content-length header */
    413         mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER);
    414 
    415         if (mBodyProvider != null) {
    416             try {
    417                 mBodyProvider.reset();
    418             } catch (IOException ex) {
    419                 if (HttpLog.LOGV) HttpLog.v(
    420                         "failed to reset body provider " +
    421                         getUri());
    422             }
    423             setBodyProvider(mBodyProvider, mBodyLength);
    424         }
    425 
    426         if (mReceivedBytes > 0) {
    427             // reset the fail count as we continue the request
    428             mFailCount = 0;
    429             // set the "Range" header to indicate that the retry will continue
    430             // instead of restarting the request
    431             HttpLog.v("*** Request.reset() to range:" + mReceivedBytes);
    432             mHttpRequest.setHeader("Range", "bytes=" + mReceivedBytes + "-");
    433         }
    434     }
    435 
    436     /**
    437      * Pause thread request completes.  Used for synchronous requests,
    438      * and testing
    439      */
    440     void waitUntilComplete() {
    441         synchronized (mClientResource) {
    442             try {
    443                 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()");
    444                 mClientResource.wait();
    445                 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting");
    446             } catch (InterruptedException e) {
    447             }
    448         }
    449     }
    450 
    451     void complete() {
    452         synchronized (mClientResource) {
    453             mClientResource.notifyAll();
    454         }
    455     }
    456 
    457     /**
    458      * Decide whether a response comes with an entity.
    459      * The implementation in this class is based on RFC 2616.
    460      * Unknown methods and response codes are supposed to
    461      * indicate responses with an entity.
    462      * <br/>
    463      * Derived executors can override this method to handle
    464      * methods and response codes not specified in RFC 2616.
    465      *
    466      * @param request   the request, to obtain the executed method
    467      * @param response  the response, to obtain the status code
    468      */
    469 
    470     private static boolean canResponseHaveBody(final HttpRequest request,
    471                                                final int status) {
    472 
    473         if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) {
    474             return false;
    475         }
    476         return status >= HttpStatus.SC_OK
    477             && status != HttpStatus.SC_NO_CONTENT
    478             && status != HttpStatus.SC_NOT_MODIFIED;
    479     }
    480 
    481     /**
    482      * Supply an InputStream that provides the body of a request.  It's
    483      * not great that the caller must also provide the length of the data
    484      * returned by that InputStream, but the client needs to know up
    485      * front, and I'm not sure how to get this out of the InputStream
    486      * itself without a costly readthrough.  I'm not sure skip() would
    487      * do what we want.  If you know a better way, please let me know.
    488      */
    489     private void setBodyProvider(InputStream bodyProvider, int bodyLength) {
    490         if (!bodyProvider.markSupported()) {
    491             throw new IllegalArgumentException(
    492                     "bodyProvider must support mark()");
    493         }
    494         // Mark beginning of stream
    495         bodyProvider.mark(Integer.MAX_VALUE);
    496 
    497         ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity(
    498                 new InputStreamEntity(bodyProvider, bodyLength));
    499     }
    500 
    501 
    502     /**
    503      * Handles SSL error(s) on the way down from the user (the user
    504      * has already provided their feedback).
    505      */
    506     public void handleSslErrorResponse(boolean proceed) {
    507         HttpsConnection connection = (HttpsConnection)(mConnection);
    508         if (connection != null) {
    509             connection.restartConnection(proceed);
    510         }
    511     }
    512 
    513     /**
    514      * Helper: calls error() on eventhandler with appropriate message
    515      * This should not be called before the mConnection is set.
    516      */
    517     void error(int errorId, String errorMessage) {
    518         mEventHandler.error(errorId, errorMessage);
    519     }
    520 
    521 }
    522