blob: 53f8d5bd7503bab2a227890623dcac3d00f0dd8c [file] [log] [blame]
/*
* 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;
public 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 */
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
*/
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;
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;
}
}