1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package libcore.net.http; 19 20 import com.squareup.okhttp.OkHttpConnection; 21 import java.io.ByteArrayInputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.OutputStream; 25 import java.net.CacheRequest; 26 import java.net.CacheResponse; 27 import java.net.CookieHandler; 28 import java.net.Proxy; 29 import java.net.ResponseCache; 30 import java.net.URI; 31 import java.net.URISyntaxException; 32 import java.net.URL; 33 import java.util.Collections; 34 import java.util.Date; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.zip.GZIPInputStream; 39 import javax.net.ssl.SSLSocketFactory; 40 import libcore.io.IoUtils; 41 import libcore.util.EmptyArray; 42 import libcore.util.ExtendedResponseCache; 43 import libcore.util.Libcore; 44 import libcore.util.ResponseSource; 45 46 /** 47 * Handles a single HTTP request/response pair. Each HTTP engine follows this 48 * lifecycle: 49 * <ol> 50 * <li>It is created. 51 * <li>The HTTP request message is sent with sendRequest(). Once the request 52 * is sent it is an error to modify the request headers. After 53 * sendRequest() has been called the request body can be written to if 54 * it exists. 55 * <li>The HTTP response message is read with readResponse(). After the 56 * response has been read the response headers and body can be read. 57 * All responses have a response body input stream, though in some 58 * instances this stream is empty. 59 * </ol> 60 * 61 * <p>The request and response may be served by the HTTP response cache, by the 62 * network, or by both in the event of a conditional GET. 63 * 64 * <p>This class may hold a socket connection that needs to be released or 65 * recycled. By default, this socket connection is held when the last byte of 66 * the response is consumed. To release the connection when it is no longer 67 * required, use {@link #automaticallyReleaseConnectionToPool()}. 68 */ 69 public class HttpEngine { 70 private static final CacheResponse BAD_GATEWAY_RESPONSE = new CacheResponse() { 71 @Override public Map<String, List<String>> getHeaders() throws IOException { 72 Map<String, List<String>> result = new HashMap<String, List<String>>(); 73 result.put(null, Collections.singletonList("HTTP/1.1 502 Bad Gateway")); 74 return result; 75 } 76 @Override public InputStream getBody() throws IOException { 77 return new ByteArrayInputStream(EmptyArray.BYTE); 78 } 79 }; 80 public static final int DEFAULT_CHUNK_LENGTH = 1024; 81 82 public static final String OPTIONS = "OPTIONS"; 83 public static final String GET = "GET"; 84 public static final String HEAD = "HEAD"; 85 public static final String POST = "POST"; 86 public static final String PUT = "PUT"; 87 public static final String DELETE = "DELETE"; 88 public static final String TRACE = "TRACE"; 89 public static final String CONNECT = "CONNECT"; 90 91 public static final int HTTP_CONTINUE = 100; 92 93 protected final HttpURLConnectionImpl policy; 94 95 protected final String method; 96 97 private ResponseSource responseSource; 98 99 protected HttpConnection connection; 100 private OutputStream requestBodyOut; 101 102 private Transport transport; 103 104 private InputStream responseBodyIn; 105 106 private final ResponseCache responseCache = ResponseCache.getDefault(); 107 private CacheResponse cacheResponse; 108 private CacheRequest cacheRequest; 109 110 /** The time when the request headers were written, or -1 if they haven't been written yet. */ 111 long sentRequestMillis = -1; 112 113 /** 114 * True if this client added an "Accept-Encoding: gzip" header field and is 115 * therefore responsible for also decompressing the transfer stream. 116 */ 117 private boolean transparentGzip; 118 119 final URI uri; 120 121 final RequestHeaders requestHeaders; 122 123 /** Null until a response is received from the network or the cache. */ 124 ResponseHeaders responseHeaders; 125 126 /* 127 * The cache response currently being validated on a conditional get. Null 128 * if the cached response doesn't exist or doesn't need validation. If the 129 * conditional get succeeds, these will be used for the response headers and 130 * body. If it fails, these be closed and set to null. 131 */ 132 private ResponseHeaders cachedResponseHeaders; 133 private InputStream cachedResponseBody; 134 135 /** 136 * True if the socket connection should be released to the connection pool 137 * when the response has been fully read. 138 */ 139 private boolean automaticallyReleaseConnectionToPool; 140 141 /** True if the socket connection is no longer needed by this engine. */ 142 private boolean connectionReleased; 143 144 /** 145 * @param requestHeaders the client's supplied request headers. This class 146 * creates a private copy that it can mutate. 147 * @param connection the connection used for an intermediate response 148 * immediately prior to this request/response pair, such as a same-host 149 * redirect. This engine assumes ownership of the connection and must 150 * release it when it is unneeded. 151 */ 152 public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, 153 HttpConnection connection, RetryableOutputStream requestBodyOut) throws IOException { 154 this.policy = policy; 155 this.method = method; 156 this.connection = connection; 157 this.requestBodyOut = requestBodyOut; 158 159 try { 160 uri = Libcore.toUriLenient(policy.getURL()); 161 } catch (URISyntaxException e) { 162 throw new IOException(e); 163 } 164 165 this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders)); 166 } 167 168 public URI getUri() { 169 return uri; 170 } 171 172 /** 173 * Figures out what the response source will be, and opens a socket to that 174 * source if necessary. Prepares the request headers and gets ready to start 175 * writing the request body if it exists. 176 */ 177 public final void sendRequest() throws IOException { 178 if (responseSource != null) { 179 return; 180 } 181 182 prepareRawRequestHeaders(); 183 initResponseSource(); 184 if (responseCache instanceof ExtendedResponseCache) { 185 ((ExtendedResponseCache) responseCache).trackResponse(responseSource); 186 } 187 188 /* 189 * The raw response source may require the network, but the request 190 * headers may forbid network use. In that case, dispose of the network 191 * response and use a BAD_GATEWAY response instead. 192 */ 193 if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) { 194 if (responseSource == ResponseSource.CONDITIONAL_CACHE) { 195 IoUtils.closeQuietly(cachedResponseBody); 196 } 197 this.responseSource = ResponseSource.CACHE; 198 this.cacheResponse = BAD_GATEWAY_RESPONSE; 199 RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders()); 200 setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody()); 201 } 202 203 if (responseSource.requiresConnection()) { 204 sendSocketRequest(); 205 } else if (connection != null) { 206 HttpConnectionPool.INSTANCE.recycle(connection); 207 connection = null; 208 } 209 } 210 211 /** 212 * Initialize the source for this response. It may be corrected later if the 213 * request headers forbids network use. 214 */ 215 private void initResponseSource() throws IOException { 216 responseSource = ResponseSource.NETWORK; 217 if (!policy.getUseCaches() || responseCache == null) { 218 return; 219 } 220 221 CacheResponse candidate = responseCache.get(uri, method, 222 requestHeaders.getHeaders().toMultimap()); 223 if (candidate == null) { 224 return; 225 } 226 227 Map<String, List<String>> responseHeadersMap = candidate.getHeaders(); 228 cachedResponseBody = candidate.getBody(); 229 if (!acceptCacheResponseType(candidate) 230 || responseHeadersMap == null 231 || cachedResponseBody == null) { 232 IoUtils.closeQuietly(cachedResponseBody); 233 return; 234 } 235 236 RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap); 237 cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders); 238 long now = System.currentTimeMillis(); 239 this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders); 240 if (responseSource == ResponseSource.CACHE) { 241 this.cacheResponse = candidate; 242 setResponse(cachedResponseHeaders, cachedResponseBody); 243 } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) { 244 this.cacheResponse = candidate; 245 } else if (responseSource == ResponseSource.NETWORK) { 246 IoUtils.closeQuietly(cachedResponseBody); 247 } else { 248 throw new AssertionError(); 249 } 250 } 251 252 private void sendSocketRequest() throws IOException { 253 if (connection == null) { 254 connect(); 255 } 256 257 if (transport != null) { 258 throw new IllegalStateException(); 259 } 260 261 transport = connection.newTransport(this); 262 263 if (hasRequestBody() && requestBodyOut == null) { 264 // Create a request body if we don't have one already. We'll already 265 // have one if we're retrying a failed POST. 266 requestBodyOut = transport.createRequestBody(); 267 } 268 } 269 270 /** 271 * Connect to the origin server either directly or via a proxy. 272 */ 273 protected void connect() throws IOException { 274 if (connection == null) { 275 connection = openSocketConnection(); 276 } 277 } 278 279 protected final HttpConnection openSocketConnection() throws IOException { 280 HttpConnection result = HttpConnection.connect(uri, getSslSocketFactory(), 281 policy.getProxy(), requiresTunnel(), policy.getConnectTimeout()); 282 Proxy proxy = result.getAddress().getProxy(); 283 if (proxy != null) { 284 policy.setProxy(proxy); 285 // Add the authority to the request line when we're using a proxy. 286 requestHeaders.getHeaders().setStatusLine(getRequestLine()); 287 } 288 result.setSoTimeout(policy.getReadTimeout()); 289 return result; 290 } 291 292 /** 293 * @param body the response body, or null if it doesn't exist or isn't 294 * available. 295 */ 296 private void setResponse(ResponseHeaders headers, InputStream body) throws IOException { 297 if (this.responseBodyIn != null) { 298 throw new IllegalStateException(); 299 } 300 this.responseHeaders = headers; 301 if (body != null) { 302 initContentStream(body); 303 } 304 } 305 306 boolean hasRequestBody() { 307 return method == POST || method == PUT; 308 } 309 310 /** 311 * Returns the request body or null if this request doesn't have a body. 312 */ 313 public final OutputStream getRequestBody() { 314 if (responseSource == null) { 315 throw new IllegalStateException(); 316 } 317 return requestBodyOut; 318 } 319 320 public final boolean hasResponse() { 321 return responseHeaders != null; 322 } 323 324 public final RequestHeaders getRequestHeaders() { 325 return requestHeaders; 326 } 327 328 public final ResponseHeaders getResponseHeaders() { 329 if (responseHeaders == null) { 330 throw new IllegalStateException(); 331 } 332 return responseHeaders; 333 } 334 335 public final int getResponseCode() { 336 if (responseHeaders == null) { 337 throw new IllegalStateException(); 338 } 339 return responseHeaders.getHeaders().getResponseCode(); 340 } 341 342 public final InputStream getResponseBody() { 343 if (responseHeaders == null) { 344 throw new IllegalStateException(); 345 } 346 return responseBodyIn; 347 } 348 349 public final CacheResponse getCacheResponse() { 350 return cacheResponse; 351 } 352 353 public final HttpConnection getConnection() { 354 return connection; 355 } 356 357 public final boolean hasRecycledConnection() { 358 return connection != null && connection.isRecycled(); 359 } 360 361 /** 362 * Returns true if {@code cacheResponse} is of the right type. This 363 * condition is necessary but not sufficient for the cached response to 364 * be used. 365 */ 366 protected boolean acceptCacheResponseType(CacheResponse cacheResponse) { 367 return true; 368 } 369 370 private void maybeCache() throws IOException { 371 // Are we caching at all? 372 if (!policy.getUseCaches() || responseCache == null) { 373 return; 374 } 375 376 // Should we cache this response for this request? 377 if (!responseHeaders.isCacheable(requestHeaders)) { 378 return; 379 } 380 381 // Offer this request to the cache. 382 cacheRequest = responseCache.put(uri, getHttpConnectionToCache()); 383 } 384 385 protected OkHttpConnection getHttpConnectionToCache() { 386 return policy; 387 } 388 389 /** 390 * Cause the socket connection to be released to the connection pool when 391 * it is no longer needed. If it is already unneeded, it will be pooled 392 * immediately. Otherwise the connection is held so that redirects can be 393 * handled by the same connection. 394 */ 395 public final void automaticallyReleaseConnectionToPool() { 396 automaticallyReleaseConnectionToPool = true; 397 if (connection != null && connectionReleased) { 398 HttpConnectionPool.INSTANCE.recycle(connection); 399 connection = null; 400 } 401 } 402 403 /** 404 * Releases this engine so that its resources may be either reused or 405 * closed. Also call {@link #automaticallyReleaseConnectionToPool} unless 406 * the connection will be used to follow a redirect. 407 */ 408 public final void release(boolean reusable) { 409 // If the response body comes from the cache, close it. 410 if (responseBodyIn == cachedResponseBody) { 411 IoUtils.closeQuietly(responseBodyIn); 412 } 413 414 if (!connectionReleased && connection != null) { 415 connectionReleased = true; 416 417 if (!reusable || !transport.makeReusable(requestBodyOut, responseBodyIn)) { 418 connection.closeSocketAndStreams(); 419 connection = null; 420 } else if (automaticallyReleaseConnectionToPool) { 421 HttpConnectionPool.INSTANCE.recycle(connection); 422 connection = null; 423 } 424 } 425 } 426 427 private void initContentStream(InputStream transferStream) throws IOException { 428 if (transparentGzip && responseHeaders.isContentEncodingGzip()) { 429 /* 430 * If the response was transparently gzipped, remove the gzip header field 431 * so clients don't double decompress. http://b/3009828 432 */ 433 responseHeaders.stripContentEncoding(); 434 responseBodyIn = new GZIPInputStream(transferStream); 435 } else { 436 responseBodyIn = transferStream; 437 } 438 } 439 440 /** 441 * Returns true if the response must have a (possibly 0-length) body. 442 * See RFC 2616 section 4.3. 443 */ 444 public final boolean hasResponseBody() { 445 int responseCode = responseHeaders.getHeaders().getResponseCode(); 446 447 // HEAD requests never yield a body regardless of the response headers. 448 if (method == HEAD) { 449 return false; 450 } 451 452 if (method != CONNECT 453 && (responseCode < HTTP_CONTINUE || responseCode >= 200) 454 && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT 455 && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) { 456 return true; 457 } 458 459 /* 460 * If the Content-Length or Transfer-Encoding headers disagree with the 461 * response code, the response is malformed. For best compatibility, we 462 * honor the headers. 463 */ 464 if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) { 465 return true; 466 } 467 468 return false; 469 } 470 471 /** 472 * Populates requestHeaders with defaults and cookies. 473 * 474 * <p>This client doesn't specify a default {@code Accept} header because it 475 * doesn't know what content types the application is interested in. 476 */ 477 private void prepareRawRequestHeaders() throws IOException { 478 requestHeaders.getHeaders().setStatusLine(getRequestLine()); 479 480 if (requestHeaders.getUserAgent() == null) { 481 requestHeaders.setUserAgent(getDefaultUserAgent()); 482 } 483 484 if (requestHeaders.getHost() == null) { 485 requestHeaders.setHost(getOriginAddress(policy.getURL())); 486 } 487 488 // TODO: this shouldn't be set for SPDY (it's ignored) 489 if ((connection == null || connection.httpMinorVersion != 0) 490 && requestHeaders.getConnection() == null) { 491 requestHeaders.setConnection("Keep-Alive"); 492 } 493 494 if (requestHeaders.getAcceptEncoding() == null) { 495 transparentGzip = true; 496 // TODO: this shouldn't be set for SPDY (it isn't necessary) 497 requestHeaders.setAcceptEncoding("gzip"); 498 } 499 500 if (hasRequestBody() && requestHeaders.getContentType() == null) { 501 requestHeaders.setContentType("application/x-www-form-urlencoded"); 502 } 503 504 long ifModifiedSince = policy.getIfModifiedSince(); 505 if (ifModifiedSince != 0) { 506 requestHeaders.setIfModifiedSince(new Date(ifModifiedSince)); 507 } 508 509 CookieHandler cookieHandler = CookieHandler.getDefault(); 510 if (cookieHandler != null) { 511 requestHeaders.addCookies( 512 cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap())); 513 } 514 } 515 516 /** 517 * Returns the request status line, like "GET / HTTP/1.1". This is exposed 518 * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so 519 * it needs to be set even if the transport is SPDY. 520 */ 521 String getRequestLine() { 522 String protocol = (connection == null || connection.httpMinorVersion != 0) 523 ? "HTTP/1.1" 524 : "HTTP/1.0"; 525 return method + " " + requestString() + " " + protocol; 526 } 527 528 private String requestString() { 529 URL url = policy.getURL(); 530 if (includeAuthorityInRequestLine()) { 531 return url.toString(); 532 } else { 533 String fileOnly = url.getFile(); 534 if (fileOnly == null) { 535 fileOnly = "/"; 536 } else if (!fileOnly.startsWith("/")) { 537 fileOnly = "/" + fileOnly; 538 } 539 return fileOnly; 540 } 541 } 542 543 /** 544 * Returns true if the request line should contain the full URL with host 545 * and port (like "GET http://android.com/foo HTTP/1.1") or only the path 546 * (like "GET /foo HTTP/1.1"). 547 * 548 * <p>This is non-final because for HTTPS it's never necessary to supply the 549 * full URL, even if a proxy is in use. 550 */ 551 protected boolean includeAuthorityInRequestLine() { 552 return policy.usingProxy(); 553 } 554 555 /** 556 * Returns the SSL configuration for connections created by this engine. 557 * We cannot reuse HTTPS connections if the socket factory has changed. 558 */ 559 protected SSLSocketFactory getSslSocketFactory() { 560 return null; 561 } 562 563 protected final String getDefaultUserAgent() { 564 String agent = System.getProperty("http.agent"); 565 return agent != null ? agent : ("Java" + System.getProperty("java.version")); 566 } 567 568 protected final String getOriginAddress(URL url) { 569 int port = url.getPort(); 570 String result = url.getHost(); 571 if (port > 0 && port != policy.getDefaultPort()) { 572 result = result + ":" + port; 573 } 574 return result; 575 } 576 577 protected boolean requiresTunnel() { 578 return false; 579 } 580 581 /** 582 * Flushes the remaining request header and body, parses the HTTP response 583 * headers and starts reading the HTTP response body if it exists. 584 */ 585 public final void readResponse() throws IOException { 586 if (hasResponse()) { 587 return; 588 } 589 590 if (responseSource == null) { 591 throw new IllegalStateException("readResponse() without sendRequest()"); 592 } 593 594 if (!responseSource.requiresConnection()) { 595 return; 596 } 597 598 if (sentRequestMillis == -1) { 599 if (requestBodyOut instanceof RetryableOutputStream) { 600 int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength(); 601 requestHeaders.setContentLength(contentLength); 602 } 603 transport.writeRequestHeaders(); 604 } 605 606 if (requestBodyOut != null) { 607 requestBodyOut.close(); 608 if (requestBodyOut instanceof RetryableOutputStream) { 609 transport.writeRequestBody((RetryableOutputStream) requestBodyOut); 610 } 611 } 612 613 transport.flushRequest(); 614 615 responseHeaders = transport.readResponseHeaders(); 616 responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis()); 617 618 if (responseSource == ResponseSource.CONDITIONAL_CACHE) { 619 if (cachedResponseHeaders.validate(responseHeaders)) { 620 release(true); 621 ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders); 622 setResponse(combinedHeaders, cachedResponseBody); 623 if (responseCache instanceof ExtendedResponseCache) { 624 ExtendedResponseCache httpResponseCache = (ExtendedResponseCache) responseCache; 625 httpResponseCache.trackConditionalCacheHit(); 626 httpResponseCache.update(cacheResponse, getHttpConnectionToCache()); 627 } 628 return; 629 } else { 630 IoUtils.closeQuietly(cachedResponseBody); 631 } 632 } 633 634 if (hasResponseBody()) { 635 maybeCache(); // reentrant. this calls into user code which may call back into this! 636 } 637 638 initContentStream(transport.getTransferStream(cacheRequest)); 639 } 640 } 641