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