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