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