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.LinkedList;
25
26import javax.net.ssl.SSLHandshakeException;
27
28import org.apache.http.ConnectionReuseStrategy;
29import org.apache.http.HttpEntity;
30import org.apache.http.HttpException;
31import org.apache.http.HttpHost;
32import org.apache.http.HttpVersion;
33import org.apache.http.ParseException;
34import org.apache.http.ProtocolVersion;
35import org.apache.http.protocol.ExecutionContext;
36import org.apache.http.protocol.HttpContext;
37import org.apache.http.protocol.BasicHttpContext;
38
39/**
40 * {@hide}
41 */
42abstract 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