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