Connection.java revision 86806ce11a89260147d7c2efa2c192b711d923db
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    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                    try {
226                        /* FIXME: don't increment failure count if old
227                           connection?  There should not be a penalty for
228                           attempting to reuse an old connection */
229                        req.sendRequest(mHttpClientConnection);
230                    } catch (HttpException e) {
231                        exception = e;
232                        error = EventHandler.ERROR;
233                    } catch (IOException e) {
234                        exception = e;
235                        error = EventHandler.ERROR_IO;
236                    } catch (IllegalStateException e) {
237                        exception = e;
238                        error = EventHandler.ERROR_IO;
239                    }
240                    if (exception != null) {
241                        if (httpFailure(req, error, exception) &&
242                            !req.mCancelled) {
243                            /* retry request if not permanent failure
244                               or cancelled */
245                            pipe.addLast(req);
246                        }
247                        exception = null;
248                        state = clearPipe(pipe) ? DONE : SEND;
249                        minPipe = maxPipe = 1;
250                        break;
251                    }
252
253                    pipe.addLast(req);
254                    if (!mCanPersist) state = READ;
255                    break;
256
257                }
258                case DRAIN:
259                case READ: {
260                    empty = !mRequestFeeder.haveRequest(mHost);
261                    int pipeSize = pipe.size();
262                    if (state != DRAIN && pipeSize < minPipe &&
263                        !empty && mCanPersist) {
264                        state = SEND;
265                        break;
266                    } else if (pipeSize == 0) {
267                        /* Done if no other work to do */
268                        state = empty ? DONE : SEND;
269                        break;
270                    }
271
272                    req = (Request)pipe.removeFirst();
273                    if (HttpLog.LOGV) HttpLog.v(
274                            "processRequests() reading " + req);
275
276                    try {
277                        req.readResponse(mHttpClientConnection);
278                    } catch (ParseException e) {
279                        exception = e;
280                        error = EventHandler.ERROR_IO;
281                    } catch (IOException e) {
282                        exception = e;
283                        error = EventHandler.ERROR_IO;
284                    } catch (IllegalStateException e) {
285                        exception = e;
286                        error = EventHandler.ERROR_IO;
287                    }
288                    if (exception != null) {
289                        if (httpFailure(req, error, exception) &&
290                            !req.mCancelled) {
291                            /* retry request if not permanent failure
292                               or cancelled */
293                            req.reset();
294                            pipe.addFirst(req);
295                        }
296                        exception = null;
297                        mCanPersist = false;
298                    }
299                    if (!mCanPersist) {
300                        if (HttpLog.LOGV) HttpLog.v(
301                                "processRequests(): no persist, closing " +
302                                mHost);
303
304                        closeConnection();
305
306                        mHttpContext.removeAttribute(HTTP_CONNECTION);
307                        clearPipe(pipe);
308                        minPipe = maxPipe = 1;
309                        state = SEND;
310                    }
311                    break;
312                }
313            }
314        }
315    }
316
317    /**
318     * After a send/receive failure, any pipelined requests must be
319     * cleared back to the mRequest queue
320     * @return true if mRequests is empty after pipe cleared
321     */
322    private boolean clearPipe(LinkedList<Request> pipe) {
323        boolean empty = true;
324        if (HttpLog.LOGV) HttpLog.v(
325                "Connection.clearPipe(): clearing pipe " + pipe.size());
326        synchronized (mRequestFeeder) {
327            Request tReq;
328            while (!pipe.isEmpty()) {
329                tReq = (Request)pipe.removeLast();
330                if (HttpLog.LOGV) HttpLog.v(
331                        "clearPipe() adding back " + mHost + " " + tReq);
332                mRequestFeeder.requeueRequest(tReq);
333                empty = false;
334            }
335            if (empty) empty = !mRequestFeeder.haveRequest(mHost);
336        }
337        return empty;
338    }
339
340    /**
341     * @return true on success
342     */
343    private boolean openHttpConnection(Request req) {
344
345        long now = SystemClock.uptimeMillis();
346        int error = EventHandler.OK;
347        Exception exception = null;
348
349        try {
350            // reset the certificate to null before opening a connection
351            mCertificate = null;
352            mHttpClientConnection = openConnection(req);
353            if (mHttpClientConnection != null) {
354                mHttpClientConnection.setSocketTimeout(SOCKET_TIMEOUT);
355                mHttpContext.setAttribute(HTTP_CONNECTION,
356                                          mHttpClientConnection);
357            } else {
358                // we tried to do SSL tunneling, failed,
359                // and need to drop the request;
360                // we have already informed the handler
361                req.mFailCount = RETRY_REQUEST_LIMIT;
362                return false;
363            }
364        } catch (UnknownHostException e) {
365            if (HttpLog.LOGV) HttpLog.v("Failed to open connection");
366            error = EventHandler.ERROR_LOOKUP;
367            exception = e;
368        } catch (IllegalArgumentException e) {
369            if (HttpLog.LOGV) HttpLog.v("Illegal argument exception");
370            error = EventHandler.ERROR_CONNECT;
371            req.mFailCount = RETRY_REQUEST_LIMIT;
372            exception = e;
373        } catch (SSLConnectionClosedByUserException e) {
374            // hack: if we have an SSL connection failure,
375            // we don't want to reconnect
376            req.mFailCount = RETRY_REQUEST_LIMIT;
377            // no error message
378            return false;
379        } catch (SSLHandshakeException e) {
380            // hack: if we have an SSL connection failure,
381            // we don't want to reconnect
382            req.mFailCount = RETRY_REQUEST_LIMIT;
383            if (HttpLog.LOGV) HttpLog.v(
384                    "SSL exception performing handshake");
385            error = EventHandler.ERROR_FAILED_SSL_HANDSHAKE;
386            exception = e;
387        } catch (IOException e) {
388            error = EventHandler.ERROR_CONNECT;
389            exception = e;
390        }
391
392        if (HttpLog.LOGV) {
393            long now2 = SystemClock.uptimeMillis();
394            HttpLog.v("Connection.openHttpConnection() " +
395                      (now2 - now) + " " + mHost);
396        }
397
398        if (error == EventHandler.OK) {
399            return true;
400        } else {
401            if (req.mFailCount < RETRY_REQUEST_LIMIT) {
402                // requeue
403                mRequestFeeder.requeueRequest(req);
404                req.mFailCount++;
405            } else {
406                httpFailure(req, error, exception);
407            }
408            return error == EventHandler.OK;
409        }
410    }
411
412    /**
413     * Helper.  Calls the mEventHandler's error() method only if
414     * request failed permanently.  Increments mFailcount on failure.
415     *
416     * Increments failcount only if the network is believed to be
417     * connected
418     *
419     * @return true if request can be retried (less than
420     * RETRY_REQUEST_LIMIT failures have occurred).
421     */
422    private boolean httpFailure(Request req, int errorId, Exception e) {
423        boolean ret = true;
424
425        // e.printStackTrace();
426        if (HttpLog.LOGV) HttpLog.v(
427                "httpFailure() ******* " + e + " count " + req.mFailCount +
428                " " + mHost + " " + req.getUri());
429
430        if (++req.mFailCount >= RETRY_REQUEST_LIMIT) {
431            ret = false;
432            String error;
433            if (errorId < 0) {
434                error = mContext.getText(
435                        EventHandler.errorStringResources[-errorId]).toString();
436            } else {
437                Throwable cause = e.getCause();
438                error = cause != null ? cause.toString() : e.getMessage();
439            }
440            req.mEventHandler.error(errorId, error);
441            req.complete();
442        }
443
444        closeConnection();
445        mHttpContext.removeAttribute(HTTP_CONNECTION);
446
447        return ret;
448    }
449
450    HttpContext getHttpContext() {
451        return mHttpContext;
452    }
453
454    /**
455     * Use same logic as ConnectionReuseStrategy
456     * @see ConnectionReuseStrategy
457     */
458    private boolean keepAlive(HttpEntity entity,
459            ProtocolVersion ver, int connType, final HttpContext context) {
460        org.apache.http.HttpConnection conn = (org.apache.http.HttpConnection)
461            context.getAttribute(ExecutionContext.HTTP_CONNECTION);
462
463        if (conn != null && !conn.isOpen())
464            return false;
465        // do NOT check for stale connection, that is an expensive operation
466
467        if (entity != null) {
468            if (entity.getContentLength() < 0) {
469                if (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0)) {
470                    // if the content length is not known and is not chunk
471                    // encoded, the connection cannot be reused
472                    return false;
473                }
474            }
475        }
476        // Check for 'Connection' directive
477        if (connType == Headers.CONN_CLOSE) {
478            return false;
479        } else if (connType == Headers.CONN_KEEP_ALIVE) {
480            return true;
481        }
482        // Resorting to protocol version default close connection policy
483        return !ver.lessEquals(HttpVersion.HTTP_1_0);
484    }
485
486    void setCanPersist(HttpEntity entity, ProtocolVersion ver, int connType) {
487        mCanPersist = keepAlive(entity, ver, connType, mHttpContext);
488    }
489
490    void setCanPersist(boolean canPersist) {
491        mCanPersist = canPersist;
492    }
493
494    boolean getCanPersist() {
495        return mCanPersist;
496    }
497
498    /** typically http or https... set by subclass */
499    abstract String getScheme();
500    abstract void closeConnection();
501    abstract AndroidHttpClientConnection openConnection(Request req) throws IOException;
502
503    /**
504     * Prints request queue to log, for debugging.
505     * returns request count
506     */
507    public synchronized String toString() {
508        return mHost.toString();
509    }
510
511    byte[] getBuf() {
512        if (mBuf == null) mBuf = new byte[8192];
513        return mBuf;
514    }
515
516}
517