Home | History | Annotate | Download | only in http
      1 /*
      2  * Copyright (C) 2007 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 android.content.Context;
     20 import android.os.SystemClock;
     21 
     22 import java.io.IOException;
     23 import java.net.UnknownHostException;
     24 import java.util.LinkedList;
     25 
     26 import javax.net.ssl.SSLHandshakeException;
     27 
     28 import org.apache.http.ConnectionReuseStrategy;
     29 import org.apache.http.HttpEntity;
     30 import org.apache.http.HttpException;
     31 import org.apache.http.HttpHost;
     32 import org.apache.http.HttpVersion;
     33 import org.apache.http.ParseException;
     34 import org.apache.http.ProtocolVersion;
     35 import org.apache.http.protocol.ExecutionContext;
     36 import org.apache.http.protocol.HttpContext;
     37 import org.apache.http.protocol.BasicHttpContext;
     38 
     39 /**
     40  * {@hide}
     41  */
     42 abstract class Connection {
     43 
     44     /**
     45      * Allow a TCP connection 60 idle seconds before erroring out
     46      */
     47     static final int SOCKET_TIMEOUT = 60000;
     48 
     49     private static final int SEND = 0;
     50     private static final int READ = 1;
     51     private static final int DRAIN = 2;
     52     private static final int DONE = 3;
     53     private static final String[] states = {"SEND",  "READ", "DRAIN", "DONE"};
     54 
     55     Context mContext;
     56 
     57     /** The low level connection */
     58     protected AndroidHttpClientConnection mHttpClientConnection = null;
     59 
     60     /**
     61      * The server SSL certificate associated with this connection
     62      * (null if the connection is not secure)
     63      * It would be nice to store the whole certificate chain, but
     64      * we want to keep things as light-weight as possible
     65      */
     66     protected SslCertificate mCertificate = null;
     67 
     68     /**
     69      * The host this connection is connected to.  If using proxy,
     70      * this is set to the proxy address
     71      */
     72     HttpHost mHost;
     73 
     74     /** true if the connection can be reused for sending more requests */
     75     private boolean mCanPersist;
     76 
     77     /** context required by ConnectionReuseStrategy. */
     78     private HttpContext mHttpContext;
     79 
     80     /** set when cancelled */
     81     private static int STATE_NORMAL = 0;
     82     private static int STATE_CANCEL_REQUESTED = 1;
     83     private int mActive = STATE_NORMAL;
     84 
     85     /** The number of times to try to re-connect (if connect fails). */
     86     private final static int RETRY_REQUEST_LIMIT = 2;
     87 
     88     private static final int MIN_PIPE = 2;
     89     private static final int MAX_PIPE = 3;
     90 
     91     /**
     92      * Doesn't seem to exist anymore in the new HTTP client, so copied here.
     93      */
     94     private static final String HTTP_CONNECTION = "http.connection";
     95 
     96     RequestFeeder mRequestFeeder;
     97 
     98     /**
     99      * Buffer for feeding response blocks to webkit.  One block per
    100      * connection reduces memory churn.
    101      */
    102     private byte[] mBuf;
    103 
    104     protected Connection(Context context, HttpHost host,
    105                          RequestFeeder requestFeeder) {
    106         mContext = context;
    107         mHost = host;
    108         mRequestFeeder = requestFeeder;
    109 
    110         mCanPersist = false;
    111         mHttpContext = new BasicHttpContext(null);
    112     }
    113 
    114     HttpHost getHost() {
    115         return mHost;
    116     }
    117 
    118     /**
    119      * connection factory: returns an HTTP or HTTPS connection as
    120      * necessary
    121      */
    122     static Connection getConnection(
    123             Context context, HttpHost host, HttpHost proxy,
    124             RequestFeeder requestFeeder) {
    125 
    126         if (host.getSchemeName().equals("http")) {
    127             return new HttpConnection(context, host, requestFeeder);
    128         }
    129 
    130         // Otherwise, default to https
    131         return new HttpsConnection(context, host, proxy, requestFeeder);
    132     }
    133 
    134     /**
    135      * @return The server SSL certificate associated with this
    136      * connection (null if the connection is not secure)
    137      */
    138     /* package */ SslCertificate getCertificate() {
    139         return mCertificate;
    140     }
    141 
    142     /**
    143      * Close current network connection
    144      * Note: this runs in non-network thread
    145      */
    146     void cancel() {
    147         mActive = STATE_CANCEL_REQUESTED;
    148         closeConnection();
    149         if (HttpLog.LOGV) HttpLog.v(
    150             "Connection.cancel(): connection closed " + mHost);
    151     }
    152 
    153     /**
    154      * Process requests in queue
    155      * pipelines requests
    156      */
    157     void processRequests(Request firstRequest) {
    158         Request req = null;
    159         boolean empty;
    160         int error = EventHandler.OK;
    161         Exception exception = null;
    162 
    163         LinkedList<Request> pipe = new LinkedList<Request>();
    164 
    165         int minPipe = MIN_PIPE, maxPipe = MAX_PIPE;
    166         int state = SEND;
    167 
    168         while (state != DONE) {
    169             if (HttpLog.LOGV) HttpLog.v(
    170                     states[state] + " pipe " + pipe.size());
    171 
    172             /* If a request was cancelled, give other cancel requests
    173                some time to go through so we don't uselessly restart
    174                connections */
    175             if (mActive == STATE_CANCEL_REQUESTED) {
    176                 try {
    177                     Thread.sleep(100);
    178                 } catch (InterruptedException x) { /* ignore */ }
    179                 mActive = STATE_NORMAL;
    180             }
    181 
    182             switch (state) {
    183                 case SEND: {
    184                     if (pipe.size() == maxPipe) {
    185                         state = READ;
    186                         break;
    187                     }
    188                     /* get a request */
    189                     if (firstRequest == null) {
    190                         req = mRequestFeeder.getRequest(mHost);
    191                     } else {
    192                         req = firstRequest;
    193                         firstRequest = null;
    194                     }
    195                     if (req == null) {
    196                         state = DRAIN;
    197                         break;
    198                     }
    199                     req.setConnection(this);
    200 
    201                     /* Don't work on cancelled requests. */
    202                     if (req.mCancelled) {
    203                         if (HttpLog.LOGV) HttpLog.v(
    204                                 "processRequests(): skipping cancelled request "
    205                                 + req);
    206                         req.complete();
    207                         break;
    208                     }
    209 
    210                     if (mHttpClientConnection == null ||
    211                         !mHttpClientConnection.isOpen()) {
    212                         /* If this call fails, the address is bad or
    213                            the net is down.  Punt for now.
    214 
    215                            FIXME: blow out entire queue here on
    216                            connection failure if net up? */
    217 
    218                         if (!openHttpConnection(req)) {
    219                             state = DONE;
    220                             break;
    221                         }
    222                     }
    223 
    224                     /* we have a connection, let the event handler
    225                      * know of any associated certificate,
    226                      * potentially none.
    227                      */
    228                     req.mEventHandler.certificate(mCertificate);
    229 
    230                     try {
    231                         /* FIXME: don't increment failure count if old
    232                            connection?  There should not be a penalty for
    233                            attempting to reuse an old connection */
    234                         req.sendRequest(mHttpClientConnection);
    235                     } catch (HttpException e) {
    236                         exception = e;
    237                         error = EventHandler.ERROR;
    238                     } catch (IOException e) {
    239                         exception = e;
    240                         error = EventHandler.ERROR_IO;
    241                     } catch (IllegalStateException e) {
    242                         exception = e;
    243                         error = EventHandler.ERROR_IO;
    244                     }
    245                     if (exception != null) {
    246                         if (httpFailure(req, error, exception) &&
    247                             !req.mCancelled) {
    248                             /* retry request if not permanent failure
    249                                or cancelled */
    250                             pipe.addLast(req);
    251                         }
    252                         exception = null;
    253                         state = clearPipe(pipe) ? DONE : SEND;
    254                         minPipe = maxPipe = 1;
    255                         break;
    256                     }
    257 
    258                     pipe.addLast(req);
    259                     if (!mCanPersist) state = READ;
    260                     break;
    261 
    262                 }
    263                 case DRAIN:
    264                 case READ: {
    265                     empty = !mRequestFeeder.haveRequest(mHost);
    266                     int pipeSize = pipe.size();
    267                     if (state != DRAIN && pipeSize < minPipe &&
    268                         !empty && mCanPersist) {
    269                         state = SEND;
    270                         break;
    271                     } else if (pipeSize == 0) {
    272                         /* Done if no other work to do */
    273                         state = empty ? DONE : SEND;
    274                         break;
    275                     }
    276 
    277                     req = (Request)pipe.removeFirst();
    278                     if (HttpLog.LOGV) HttpLog.v(
    279                             "processRequests() reading " + req);
    280 
    281                     try {
    282                         req.readResponse(mHttpClientConnection);
    283                     } catch (ParseException e) {
    284                         exception = e;
    285                         error = EventHandler.ERROR_IO;
    286                     } catch (IOException e) {
    287                         exception = e;
    288                         error = EventHandler.ERROR_IO;
    289                     } catch (IllegalStateException e) {
    290                         exception = e;
    291                         error = EventHandler.ERROR_IO;
    292                     }
    293                     if (exception != null) {
    294                         if (httpFailure(req, error, exception) &&
    295                             !req.mCancelled) {
    296                             /* retry request if not permanent failure
    297                                or cancelled */
    298                             req.reset();
    299                             pipe.addFirst(req);
    300                         }
    301                         exception = null;
    302                         mCanPersist = false;
    303                     }
    304                     if (!mCanPersist) {
    305                         if (HttpLog.LOGV) HttpLog.v(
    306                                 "processRequests(): no persist, closing " +
    307                                 mHost);
    308 
    309                         closeConnection();
    310 
    311                         mHttpContext.removeAttribute(HTTP_CONNECTION);
    312                         clearPipe(pipe);
    313                         minPipe = maxPipe = 1;
    314                         state = SEND;
    315                     }
    316                     break;
    317                 }
    318             }
    319         }
    320     }
    321 
    322     /**
    323      * After a send/receive failure, any pipelined requests must be
    324      * cleared back to the mRequest queue
    325      * @return true if mRequests is empty after pipe cleared
    326      */
    327     private boolean clearPipe(LinkedList<Request> pipe) {
    328         boolean empty = true;
    329         if (HttpLog.LOGV) HttpLog.v(
    330                 "Connection.clearPipe(): clearing pipe " + pipe.size());
    331         synchronized (mRequestFeeder) {
    332             Request tReq;
    333             while (!pipe.isEmpty()) {
    334                 tReq = (Request)pipe.removeLast();
    335                 if (HttpLog.LOGV) HttpLog.v(
    336                         "clearPipe() adding back " + mHost + " " + tReq);
    337                 mRequestFeeder.requeueRequest(tReq);
    338                 empty = false;
    339             }
    340             if (empty) empty = !mRequestFeeder.haveRequest(mHost);
    341         }
    342         return empty;
    343     }
    344 
    345     /**
    346      * @return true on success
    347      */
    348     private boolean openHttpConnection(Request req) {
    349 
    350         long now = SystemClock.uptimeMillis();
    351         int error = EventHandler.OK;
    352         Exception exception = null;
    353 
    354         try {
    355             // reset the certificate to null before opening a connection
    356             mCertificate = null;
    357             mHttpClientConnection = openConnection(req);
    358             if (mHttpClientConnection != null) {
    359                 mHttpClientConnection.setSocketTimeout(SOCKET_TIMEOUT);
    360                 mHttpContext.setAttribute(HTTP_CONNECTION,
    361                                           mHttpClientConnection);
    362             } else {
    363                 // we tried to do SSL tunneling, failed,
    364                 // and need to drop the request;
    365                 // we have already informed the handler
    366                 req.mFailCount = RETRY_REQUEST_LIMIT;
    367                 return false;
    368             }
    369         } catch (UnknownHostException e) {
    370             if (HttpLog.LOGV) HttpLog.v("Failed to open connection");
    371             error = EventHandler.ERROR_LOOKUP;
    372             exception = e;
    373         } catch (IllegalArgumentException e) {
    374             if (HttpLog.LOGV) HttpLog.v("Illegal argument exception");
    375             error = EventHandler.ERROR_CONNECT;
    376             req.mFailCount = RETRY_REQUEST_LIMIT;
    377             exception = e;
    378         } catch (SSLConnectionClosedByUserException e) {
    379             // hack: if we have an SSL connection failure,
    380             // we don't want to reconnect
    381             req.mFailCount = RETRY_REQUEST_LIMIT;
    382             // no error message
    383             return false;
    384         } catch (SSLHandshakeException e) {
    385             // hack: if we have an SSL connection failure,
    386             // we don't want to reconnect
    387             req.mFailCount = RETRY_REQUEST_LIMIT;
    388             if (HttpLog.LOGV) HttpLog.v(
    389                     "SSL exception performing handshake");
    390             error = EventHandler.ERROR_FAILED_SSL_HANDSHAKE;
    391             exception = e;
    392         } catch (IOException e) {
    393             error = EventHandler.ERROR_CONNECT;
    394             exception = e;
    395         }
    396 
    397         if (HttpLog.LOGV) {
    398             long now2 = SystemClock.uptimeMillis();
    399             HttpLog.v("Connection.openHttpConnection() " +
    400                       (now2 - now) + " " + mHost);
    401         }
    402 
    403         if (error == EventHandler.OK) {
    404             return true;
    405         } else {
    406             if (req.mFailCount < RETRY_REQUEST_LIMIT) {
    407                 // requeue
    408                 mRequestFeeder.requeueRequest(req);
    409                 req.mFailCount++;
    410             } else {
    411                 httpFailure(req, error, exception);
    412             }
    413             return error == EventHandler.OK;
    414         }
    415     }
    416 
    417     /**
    418      * Helper.  Calls the mEventHandler's error() method only if
    419      * request failed permanently.  Increments mFailcount on failure.
    420      *
    421      * Increments failcount only if the network is believed to be
    422      * connected
    423      *
    424      * @return true if request can be retried (less than
    425      * RETRY_REQUEST_LIMIT failures have occurred).
    426      */
    427     private boolean httpFailure(Request req, int errorId, Exception e) {
    428         boolean ret = true;
    429 
    430         // e.printStackTrace();
    431         if (HttpLog.LOGV) HttpLog.v(
    432                 "httpFailure() ******* " + e + " count " + req.mFailCount +
    433                 " " + mHost + " " + req.getUri());
    434 
    435         if (++req.mFailCount >= RETRY_REQUEST_LIMIT) {
    436             ret = false;
    437             String error;
    438             if (errorId < 0) {
    439                 error = ErrorStrings.getString(errorId, mContext);
    440             } else {
    441                 Throwable cause = e.getCause();
    442                 error = cause != null ? cause.toString() : e.getMessage();
    443             }
    444             req.mEventHandler.error(errorId, error);
    445             req.complete();
    446         }
    447 
    448         closeConnection();
    449         mHttpContext.removeAttribute(HTTP_CONNECTION);
    450 
    451         return ret;
    452     }
    453 
    454     HttpContext getHttpContext() {
    455         return mHttpContext;
    456     }
    457 
    458     /**
    459      * Use same logic as ConnectionReuseStrategy
    460      * @see ConnectionReuseStrategy
    461      */
    462     private boolean keepAlive(HttpEntity entity,
    463             ProtocolVersion ver, int connType, final HttpContext context) {
    464         org.apache.http.HttpConnection conn = (org.apache.http.HttpConnection)
    465             context.getAttribute(ExecutionContext.HTTP_CONNECTION);
    466 
    467         if (conn != null && !conn.isOpen())
    468             return false;
    469         // do NOT check for stale connection, that is an expensive operation
    470 
    471         if (entity != null) {
    472             if (entity.getContentLength() < 0) {
    473                 if (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0)) {
    474                     // if the content length is not known and is not chunk
    475                     // encoded, the connection cannot be reused
    476                     return false;
    477                 }
    478             }
    479         }
    480         // Check for 'Connection' directive
    481         if (connType == Headers.CONN_CLOSE) {
    482             return false;
    483         } else if (connType == Headers.CONN_KEEP_ALIVE) {
    484             return true;
    485         }
    486         // Resorting to protocol version default close connection policy
    487         return !ver.lessEquals(HttpVersion.HTTP_1_0);
    488     }
    489 
    490     void setCanPersist(HttpEntity entity, ProtocolVersion ver, int connType) {
    491         mCanPersist = keepAlive(entity, ver, connType, mHttpContext);
    492     }
    493 
    494     void setCanPersist(boolean canPersist) {
    495         mCanPersist = canPersist;
    496     }
    497 
    498     boolean getCanPersist() {
    499         return mCanPersist;
    500     }
    501 
    502     /** typically http or https... set by subclass */
    503     abstract String getScheme();
    504     abstract void closeConnection();
    505     abstract AndroidHttpClientConnection openConnection(Request req) throws IOException;
    506 
    507     /**
    508      * Prints request queue to log, for debugging.
    509      * returns request count
    510      */
    511     public synchronized String toString() {
    512         return mHost.toString();
    513     }
    514 
    515     byte[] getBuf() {
    516         if (mBuf == null) mBuf = new byte[8192];
    517         return mBuf;
    518     }
    519 
    520 }
    521