summaryrefslogtreecommitdiffstats
path: root/android/src/android/net/http/Connection.java
diff options
context:
space:
mode:
Diffstat (limited to 'android/src/android/net/http/Connection.java')
-rw-r--r--android/src/android/net/http/Connection.java575
1 files changed, 575 insertions, 0 deletions
diff --git a/android/src/android/net/http/Connection.java b/android/src/android/net/http/Connection.java
new file mode 100644
index 0000000..831bd0e
--- /dev/null
+++ b/android/src/android/net/http/Connection.java
@@ -0,0 +1,575 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.util.LinkedList;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import org.apache.http.ConnectionReuseStrategy;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpVersion;
+import org.apache.http.ParseException;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.protocol.ExecutionContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.BasicHttpContext;
+
+/**
+ * {@hide}
+ */
+abstract class Connection {
+
+ /**
+ * Allow a TCP connection 60 idle seconds before erroring out
+ */
+ static final int SOCKET_TIMEOUT = 60000;
+
+ private static final int SEND = 0;
+ private static final int READ = 1;
+ private static final int DRAIN = 2;
+ private static final int DONE = 3;
+ private static final String[] states = {"SEND", "READ", "DRAIN", "DONE"};
+
+ Context mContext;
+
+ /** The low level connection */
+ protected AndroidHttpClientConnection mHttpClientConnection = null;
+
+ /**
+ * The server SSL certificate associated with this connection
+ * (null if the connection is not secure)
+ * It would be nice to store the whole certificate chain, but
+ * we want to keep things as light-weight as possible
+ */
+ protected SslCertificate mCertificate = null;
+
+ /**
+ * The host this connection is connected to. If using proxy,
+ * this is set to the proxy address
+ */
+ HttpHost mHost;
+
+ /** true if the connection can be reused for sending more requests */
+ private boolean mCanPersist;
+
+ /** context required by ConnectionReuseStrategy. */
+ private HttpContext mHttpContext;
+
+ /** set when cancelled */
+ private static int STATE_NORMAL = 0;
+ private static int STATE_CANCEL_REQUESTED = 1;
+ private int mActive = STATE_NORMAL;
+
+ /** The number of times to try to re-connect (if connect fails). */
+ private final static int RETRY_REQUEST_LIMIT = 2;
+
+ private static final int MIN_PIPE = 2;
+ private static final int MAX_PIPE = 3;
+
+ /**
+ * Doesn't seem to exist anymore in the new HTTP client, so copied here.
+ */
+ private static final String HTTP_CONNECTION = "http.connection";
+
+ RequestFeeder mRequestFeeder;
+
+ /**
+ * Buffer for feeding response blocks to webkit. One block per
+ * connection reduces memory churn.
+ */
+ private byte[] mBuf;
+
+ protected Connection(Context context, HttpHost host,
+ RequestFeeder requestFeeder) {
+ mContext = context;
+ mHost = host;
+ mRequestFeeder = requestFeeder;
+
+ mCanPersist = false;
+ mHttpContext = new BasicHttpContext(null);
+ }
+
+ HttpHost getHost() {
+ return mHost;
+ }
+
+ /**
+ * connection factory: returns an HTTP or HTTPS connection as
+ * necessary
+ */
+ static Connection getConnection(
+ Context context, HttpHost host, HttpHost proxy,
+ RequestFeeder requestFeeder) {
+
+ if (host.getSchemeName().equals("http")) {
+ return new HttpConnection(context, host, requestFeeder);
+ }
+
+ // Otherwise, default to https
+ return new HttpsConnection(context, host, proxy, requestFeeder);
+ }
+
+ /**
+ * @return The server SSL certificate associated with this
+ * connection (null if the connection is not secure)
+ */
+ /* package */ SslCertificate getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Close current network connection
+ * Note: this runs in non-network thread
+ */
+ void cancel() {
+ mActive = STATE_CANCEL_REQUESTED;
+ closeConnection();
+ if (HttpLog.LOGV) HttpLog.v(
+ "Connection.cancel(): connection closed " + mHost);
+ }
+
+ /**
+ * Process requests in queue
+ * pipelines requests
+ */
+ void processRequests(Request firstRequest) {
+ Request req = null;
+ boolean empty;
+ int error = EventHandler.OK;
+ Exception exception = null;
+
+ LinkedList<Request> pipe = new LinkedList<Request>();
+
+ int minPipe = MIN_PIPE, maxPipe = MAX_PIPE;
+ int state = SEND;
+
+ while (state != DONE) {
+ if (HttpLog.LOGV) HttpLog.v(
+ states[state] + " pipe " + pipe.size());
+
+ /* If a request was cancelled, give other cancel requests
+ some time to go through so we don't uselessly restart
+ connections */
+ if (mActive == STATE_CANCEL_REQUESTED) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException x) { /* ignore */ }
+ mActive = STATE_NORMAL;
+ }
+
+ switch (state) {
+ case SEND: {
+ if (pipe.size() == maxPipe) {
+ state = READ;
+ break;
+ }
+ /* get a request */
+ if (firstRequest == null) {
+ req = mRequestFeeder.getRequest(mHost);
+ } else {
+ req = firstRequest;
+ firstRequest = null;
+ }
+ if (req == null) {
+ state = DRAIN;
+ break;
+ }
+ req.setConnection(this);
+
+ /* Don't work on cancelled requests. */
+ if (req.mCancelled) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "processRequests(): skipping cancelled request "
+ + req);
+ req.complete();
+ break;
+ }
+
+ if (mHttpClientConnection == null ||
+ !mHttpClientConnection.isOpen()) {
+ /* If this call fails, the address is bad or
+ the net is down. Punt for now.
+
+ FIXME: blow out entire queue here on
+ connection failure if net up? */
+
+ if (!openHttpConnection(req)) {
+ state = DONE;
+ break;
+ }
+ }
+
+ /* we have a connection, let the event handler
+ * know of any associated certificate,
+ * potentially none.
+ */
+ req.mEventHandler.certificate(mCertificate);
+
+ try {
+ /* FIXME: don't increment failure count if old
+ connection? There should not be a penalty for
+ attempting to reuse an old connection */
+ req.sendRequest(mHttpClientConnection);
+ } catch (HttpException e) {
+ exception = e;
+ error = EventHandler.ERROR;
+ } catch (IOException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ } catch (IllegalStateException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ }
+ if (exception != null) {
+ if (httpFailure(req, error, exception) &&
+ !req.mCancelled) {
+ /* retry request if not permanent failure
+ or cancelled */
+ pipe.addLast(req);
+ }
+ exception = null;
+ state = clearPipe(pipe) ? DONE : SEND;
+ minPipe = maxPipe = 1;
+ break;
+ }
+
+ pipe.addLast(req);
+ if (!mCanPersist) state = READ;
+ break;
+
+ }
+ case DRAIN:
+ case READ: {
+ empty = !mRequestFeeder.haveRequest(mHost);
+ int pipeSize = pipe.size();
+ if (state != DRAIN && pipeSize < minPipe &&
+ !empty && mCanPersist) {
+ state = SEND;
+ break;
+ } else if (pipeSize == 0) {
+ /* Done if no other work to do */
+ state = empty ? DONE : SEND;
+ break;
+ }
+
+ req = (Request)pipe.removeFirst();
+ if (HttpLog.LOGV) HttpLog.v(
+ "processRequests() reading " + req);
+
+ try {
+ req.readResponse(mHttpClientConnection);
+ } catch (ParseException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ } catch (IOException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ } catch (IllegalStateException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ }
+ if (exception != null) {
+ if (httpFailure(req, error, exception) &&
+ !req.mCancelled) {
+ /* retry request if not permanent failure
+ or cancelled */
+ req.reset();
+ pipe.addFirst(req);
+ }
+ exception = null;
+ mCanPersist = false;
+ }
+ if (!mCanPersist) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "processRequests(): no persist, closing " +
+ mHost);
+
+ closeConnection();
+
+ mHttpContext.removeAttribute(HTTP_CONNECTION);
+ clearPipe(pipe);
+ minPipe = maxPipe = 1;
+ state = SEND;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * After a send/receive failure, any pipelined requests must be
+ * cleared back to the mRequest queue
+ * @return true if mRequests is empty after pipe cleared
+ */
+ private boolean clearPipe(LinkedList<Request> pipe) {
+ boolean empty = true;
+ if (HttpLog.LOGV) HttpLog.v(
+ "Connection.clearPipe(): clearing pipe " + pipe.size());
+ synchronized (mRequestFeeder) {
+ Request tReq;
+ while (!pipe.isEmpty()) {
+ tReq = (Request)pipe.removeLast();
+ if (HttpLog.LOGV) HttpLog.v(
+ "clearPipe() adding back " + mHost + " " + tReq);
+ mRequestFeeder.requeueRequest(tReq);
+ empty = false;
+ }
+ if (empty) empty = !mRequestFeeder.haveRequest(mHost);
+ }
+ return empty;
+ }
+
+ /**
+ * @return true on success
+ */
+ private boolean openHttpConnection(Request req) {
+
+ long now = SystemClock.uptimeMillis();
+ int error = EventHandler.OK;
+ Exception exception = null;
+
+ try {
+ // reset the certificate to null before opening a connection
+ mCertificate = null;
+ mHttpClientConnection = openConnection(req);
+ if (mHttpClientConnection != null) {
+ mHttpClientConnection.setSocketTimeout(SOCKET_TIMEOUT);
+ mHttpContext.setAttribute(HTTP_CONNECTION,
+ mHttpClientConnection);
+ } else {
+ // we tried to do SSL tunneling, failed,
+ // and need to drop the request;
+ // we have already informed the handler
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ return false;
+ }
+ } catch (UnknownHostException e) {
+ if (HttpLog.LOGV) HttpLog.v("Failed to open connection");
+ error = EventHandler.ERROR_LOOKUP;
+ exception = e;
+ } catch (IllegalArgumentException e) {
+ if (HttpLog.LOGV) HttpLog.v("Illegal argument exception");
+ error = EventHandler.ERROR_CONNECT;
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ exception = e;
+ } catch (SSLConnectionClosedByUserException e) {
+ // hack: if we have an SSL connection failure,
+ // we don't want to reconnect
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ // no error message
+ return false;
+ } catch (SSLHandshakeException e) {
+ // hack: if we have an SSL connection failure,
+ // we don't want to reconnect
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ if (HttpLog.LOGV) HttpLog.v(
+ "SSL exception performing handshake");
+ error = EventHandler.ERROR_FAILED_SSL_HANDSHAKE;
+ exception = e;
+ } catch (IOException e) {
+ error = EventHandler.ERROR_CONNECT;
+ exception = e;
+ }
+
+ if (HttpLog.LOGV) {
+ long now2 = SystemClock.uptimeMillis();
+ HttpLog.v("Connection.openHttpConnection() " +
+ (now2 - now) + " " + mHost);
+ }
+
+ if (error == EventHandler.OK) {
+ return true;
+ } else {
+ if (req.mFailCount < RETRY_REQUEST_LIMIT) {
+ // requeue
+ mRequestFeeder.requeueRequest(req);
+ req.mFailCount++;
+ } else {
+ httpFailure(req, error, exception);
+ }
+ return error == EventHandler.OK;
+ }
+ }
+
+ /**
+ * Helper. Calls the mEventHandler's error() method only if
+ * request failed permanently. Increments mFailcount on failure.
+ *
+ * Increments failcount only if the network is believed to be
+ * connected
+ *
+ * @return true if request can be retried (less than
+ * RETRY_REQUEST_LIMIT failures have occurred).
+ */
+ private boolean httpFailure(Request req, int errorId, Exception e) {
+ boolean ret = true;
+
+ // e.printStackTrace();
+ if (HttpLog.LOGV) HttpLog.v(
+ "httpFailure() ******* " + e + " count " + req.mFailCount +
+ " " + mHost + " " + req.getUri());
+
+ if (++req.mFailCount >= RETRY_REQUEST_LIMIT) {
+ ret = false;
+ String error;
+ if (errorId < 0) {
+ error = getEventHandlerErrorString(errorId);
+ } else {
+ Throwable cause = e.getCause();
+ error = cause != null ? cause.toString() : e.getMessage();
+ }
+ req.mEventHandler.error(errorId, error);
+ req.complete();
+ }
+
+ closeConnection();
+ mHttpContext.removeAttribute(HTTP_CONNECTION);
+
+ return ret;
+ }
+
+ private static String getEventHandlerErrorString(int errorId) {
+ switch (errorId) {
+ case EventHandler.OK:
+ return "OK";
+
+ case EventHandler.ERROR:
+ return "ERROR";
+
+ case EventHandler.ERROR_LOOKUP:
+ return "ERROR_LOOKUP";
+
+ case EventHandler.ERROR_UNSUPPORTED_AUTH_SCHEME:
+ return "ERROR_UNSUPPORTED_AUTH_SCHEME";
+
+ case EventHandler.ERROR_AUTH:
+ return "ERROR_AUTH";
+
+ case EventHandler.ERROR_PROXYAUTH:
+ return "ERROR_PROXYAUTH";
+
+ case EventHandler.ERROR_CONNECT:
+ return "ERROR_CONNECT";
+
+ case EventHandler.ERROR_IO:
+ return "ERROR_IO";
+
+ case EventHandler.ERROR_TIMEOUT:
+ return "ERROR_TIMEOUT";
+
+ case EventHandler.ERROR_REDIRECT_LOOP:
+ return "ERROR_REDIRECT_LOOP";
+
+ case EventHandler.ERROR_UNSUPPORTED_SCHEME:
+ return "ERROR_UNSUPPORTED_SCHEME";
+
+ case EventHandler.ERROR_FAILED_SSL_HANDSHAKE:
+ return "ERROR_FAILED_SSL_HANDSHAKE";
+
+ case EventHandler.ERROR_BAD_URL:
+ return "ERROR_BAD_URL";
+
+ case EventHandler.FILE_ERROR:
+ return "FILE_ERROR";
+
+ case EventHandler.FILE_NOT_FOUND_ERROR:
+ return "FILE_NOT_FOUND_ERROR";
+
+ case EventHandler.TOO_MANY_REQUESTS_ERROR:
+ return "TOO_MANY_REQUESTS_ERROR";
+
+ default:
+ return "UNKNOWN_ERROR";
+ }
+ }
+
+ HttpContext getHttpContext() {
+ return mHttpContext;
+ }
+
+ /**
+ * Use same logic as ConnectionReuseStrategy
+ * @see ConnectionReuseStrategy
+ */
+ private boolean keepAlive(HttpEntity entity,
+ ProtocolVersion ver, int connType, final HttpContext context) {
+ org.apache.http.HttpConnection conn = (org.apache.http.HttpConnection)
+ context.getAttribute(ExecutionContext.HTTP_CONNECTION);
+
+ if (conn != null && !conn.isOpen())
+ return false;
+ // do NOT check for stale connection, that is an expensive operation
+
+ if (entity != null) {
+ if (entity.getContentLength() < 0) {
+ if (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0)) {
+ // if the content length is not known and is not chunk
+ // encoded, the connection cannot be reused
+ return false;
+ }
+ }
+ }
+ // Check for 'Connection' directive
+ if (connType == Headers.CONN_CLOSE) {
+ return false;
+ } else if (connType == Headers.CONN_KEEP_ALIVE) {
+ return true;
+ }
+ // Resorting to protocol version default close connection policy
+ return !ver.lessEquals(HttpVersion.HTTP_1_0);
+ }
+
+ void setCanPersist(HttpEntity entity, ProtocolVersion ver, int connType) {
+ mCanPersist = keepAlive(entity, ver, connType, mHttpContext);
+ }
+
+ void setCanPersist(boolean canPersist) {
+ mCanPersist = canPersist;
+ }
+
+ boolean getCanPersist() {
+ return mCanPersist;
+ }
+
+ /** typically http or https... set by subclass */
+ abstract String getScheme();
+ abstract void closeConnection();
+ abstract AndroidHttpClientConnection openConnection(Request req) throws IOException;
+
+ /**
+ * Prints request queue to log, for debugging.
+ * returns request count
+ */
+ public synchronized String toString() {
+ return mHost.toString();
+ }
+
+ byte[] getBuf() {
+ if (mBuf == null) mBuf = new byte[8192];
+ return mBuf;
+ }
+
+}