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