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