| /* |
| * 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; |
| |
| 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; |
| } |
| |
| } |