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