/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 com.squareup.okhttp.internal.http; import com.squareup.okhttp.Address; import com.squareup.okhttp.Connection; import com.squareup.okhttp.Headers; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.OkResponseCache; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseSource; import com.squareup.okhttp.Route; import com.squareup.okhttp.TunnelRequest; import com.squareup.okhttp.internal.Dns; import java.io.IOException; import java.io.InputStream; import java.net.CacheRequest; import java.net.CookieHandler; import java.net.ProtocolException; import java.net.URL; import java.net.UnknownHostException; import java.security.cert.CertificateException; import java.util.List; import java.util.Map; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocketFactory; import okio.BufferedSink; import okio.GzipSource; import okio.Okio; import okio.Sink; import okio.Source; import static com.squareup.okhttp.internal.Util.closeQuietly; import static com.squareup.okhttp.internal.Util.getDefaultPort; import static com.squareup.okhttp.internal.Util.getEffectivePort; import static com.squareup.okhttp.internal.http.StatusLine.HTTP_CONTINUE; import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; import static java.net.HttpURLConnection.HTTP_NO_CONTENT; /** * Handles a single HTTP request/response pair. Each HTTP engine follows this * lifecycle: *
    *
  1. It is created. *
  2. The HTTP request message is sent with sendRequest(). Once the request * is sent it is an error to modify the request headers. After * sendRequest() has been called the request body can be written to if * it exists. *
  3. The HTTP response message is read with readResponse(). After the * response has been read the response headers and body can be read. * All responses have a response body input stream, though in some * instances this stream is empty. *
* *

The request and response may be served by the HTTP response cache, by the * network, or by both in the event of a conditional GET. */ public class HttpEngine { final OkHttpClient client; private Connection connection; private RouteSelector routeSelector; private Route route; private Transport transport; /** The time when the request headers were written, or -1 if they haven't been written yet. */ long sentRequestMillis = -1; /** * True if this client added an "Accept-Encoding: gzip" header field and is * therefore responsible for also decompressing the transfer stream. */ private boolean transparentGzip; /** * True if the request body must be completely buffered before transmission; * false if it can be streamed. Buffering has two advantages: we don't need * the content-length in advance and we can retransmit if necessary. The * upside of streaming is that we can save memory. */ public final boolean bufferRequestBody; private Request originalRequest; private Request request; private Sink requestBodyOut; private BufferedSink bufferedRequestBody; private ResponseSource responseSource; /** Null until a response is received from the network or the cache. */ private Response response; private Source responseTransferSource; private Source responseBody; private InputStream responseBodyBytes; /** * The cache response currently being validated on a conditional get. Null * if the cached response doesn't exist or doesn't need validation. If the * conditional get succeeds, these will be used for the response. If it fails, * it will be set to null. */ private Response validatingResponse; /** The cache request currently being populated from a network response. */ private CacheRequest cacheRequest; /** * @param request the HTTP request without a body. The body must be * written via the engine's request body stream. * @param connection the connection used for an intermediate response * immediately prior to this request/response pair, such as a same-host * redirect. This engine assumes ownership of the connection and must * release it when it is unneeded. * @param routeSelector the route selector used for a failed attempt * immediately preceding this attempt, or null if this request doesn't * recover from a failure. */ public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody, Connection connection, RouteSelector routeSelector, RetryableSink requestBodyOut) { this.client = client; this.originalRequest = request; this.request = request; this.bufferRequestBody = bufferRequestBody; this.connection = connection; this.routeSelector = routeSelector; this.route = connection != null ? connection.getRoute() : null; this.requestBodyOut = requestBodyOut; } /** * Figures out what the response source will be, and opens a socket to that * source if necessary. Prepares the request headers and gets ready to start * writing the request body if it exists. */ public final void sendRequest() throws IOException { if (responseSource != null) return; // Already sent. if (transport != null) throw new IllegalStateException(); prepareRawRequestHeaders(); OkResponseCache responseCache = client.getOkResponseCache(); Response cacheResponse = responseCache != null ? responseCache.get(request) : null; long now = System.currentTimeMillis(); CacheStrategy cacheStrategy = new CacheStrategy.Factory(now, request, cacheResponse).get(); responseSource = cacheStrategy.source; request = cacheStrategy.request; if (responseCache != null) { responseCache.trackResponse(responseSource); } if (responseSource != ResponseSource.NETWORK) { validatingResponse = cacheStrategy.response; } if (cacheResponse != null && !responseSource.usesCache()) { closeQuietly(cacheResponse.body()); // We don't need this cached response. Close it. } if (responseSource.requiresConnection()) { // Open a connection unless we inherited one from a redirect. if (connection == null) { connect(); } transport = (Transport) connection.newTransport(this); // Create a request body if we don't have one already. We'll already have // one if we're retrying a failed POST. if (hasRequestBody() && requestBodyOut == null) { requestBodyOut = transport.createRequestBody(request); } } else { // We're using a cached response. Recycle a connection we may have inherited from a redirect. if (connection != null) { client.getConnectionPool().recycle(connection); connection = null; } // No need for the network! Promote the cached response immediately. this.response = validatingResponse; if (validatingResponse.body() != null) { initContentStream(validatingResponse.body().source()); } } } private Response cacheableResponse() { // Use an unreadable response body when offering the response to the cache. // The cache isn't allowed to consume the response body bytes! return response.newBuilder().body(null).build(); } /** Connect to the origin server either directly or via a proxy. */ private void connect() throws IOException { if (connection != null) throw new IllegalStateException(); if (routeSelector == null) { String uriHost = request.url().getHost(); if (uriHost == null || uriHost.length() == 0) { throw new UnknownHostException(request.url().toString()); } SSLSocketFactory sslSocketFactory = null; HostnameVerifier hostnameVerifier = null; if (request.isHttps()) { sslSocketFactory = client.getSslSocketFactory(); hostnameVerifier = client.getHostnameVerifier(); } Address address = new Address(uriHost, getEffectivePort(request.url()), sslSocketFactory, hostnameVerifier, client.getAuthenticator(), client.getProxy(), client.getProtocols()); routeSelector = new RouteSelector(address, request.uri(), client.getProxySelector(), client.getConnectionPool(), Dns.DEFAULT, client.getRoutesDatabase()); } connection = routeSelector.next(request.method()); if (!connection.isConnected()) { connection.connect(client.getConnectTimeout(), client.getReadTimeout(), getTunnelConfig()); if (connection.isSpdy()) client.getConnectionPool().share(connection); client.getRoutesDatabase().connected(connection.getRoute()); } else if (!connection.isSpdy()) { connection.updateReadTimeout(client.getReadTimeout()); } route = connection.getRoute(); } /** * Called immediately before the transport transmits HTTP request headers. * This is used to observe the sent time should the request be cached. */ public void writingRequestHeaders() { if (sentRequestMillis != -1) throw new IllegalStateException(); sentRequestMillis = System.currentTimeMillis(); } boolean hasRequestBody() { return HttpMethod.hasRequestBody(request.method()); } /** Returns the request body or null if this request doesn't have a body. */ public final Sink getRequestBody() { if (responseSource == null) throw new IllegalStateException(); return requestBodyOut; } public final BufferedSink getBufferedRequestBody() { BufferedSink result = bufferedRequestBody; if (result != null) return result; Sink requestBody = getRequestBody(); return requestBody != null ? (bufferedRequestBody = Okio.buffer(requestBody)) : null; } public final boolean hasResponse() { return response != null; } public final ResponseSource responseSource() { return responseSource; } public final Request getRequest() { return request; } /** Returns the engine's response. */ // TODO: the returned body will always be null. public final Response getResponse() { if (response == null) throw new IllegalStateException(); return response; } public final Source getResponseBody() { if (response == null) throw new IllegalStateException(); return responseBody; } public final InputStream getResponseBodyBytes() { InputStream result = responseBodyBytes; return result != null ? result : (responseBodyBytes = Okio.buffer(getResponseBody()).inputStream()); } public final Connection getConnection() { return connection; } /** * Report and attempt to recover from {@code e}. Returns a new HTTP engine * that should be used for the retry if {@code e} is recoverable, or null if * the failure is permanent. */ public HttpEngine recover(IOException e) { if (routeSelector != null && connection != null) { routeSelector.connectFailed(connection, e); } boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink; if (routeSelector == null && connection == null // No connection. || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt. || !isRecoverable(e) || !canRetryRequestBody) { return null; } Connection connection = close(); // For failure recovery, use the same route selector with a new connection. return new HttpEngine(client, originalRequest, bufferRequestBody, connection, routeSelector, (RetryableSink) requestBodyOut); } private boolean isRecoverable(IOException e) { // If the problem was a CertificateException from the X509TrustManager, // do not retry, we didn't have an abrupt server-initiated exception. boolean sslFailure = e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException; boolean protocolFailure = e instanceof ProtocolException; return !sslFailure && !protocolFailure; } /** * Returns the route used to retrieve the response. Null if we haven't * connected yet, or if no connection was necessary. */ public Route getRoute() { return route; } private void maybeCache() throws IOException { OkResponseCache responseCache = client.getOkResponseCache(); if (responseCache == null) return; // Should we cache this response for this request? if (!CacheStrategy.isCacheable(response, request)) { responseCache.maybeRemove(request); return; } // Offer this request to the cache. cacheRequest = responseCache.put(cacheableResponse()); } /** * Configure the socket connection to be either pooled or closed when it is * either exhausted or closed. If it is unneeded when this is called, it will * be released immediately. */ public final void releaseConnection() throws IOException { if (transport != null && connection != null) { transport.releaseConnectionOnIdle(); } connection = null; } /** * Release any resources held by this engine. If a connection is still held by * this engine, it is returned. */ public final Connection close() { if (bufferedRequestBody != null) { // This also closes the wrapped requestBodyOut. closeQuietly(bufferedRequestBody); } else if (requestBodyOut != null) { closeQuietly(requestBodyOut); } // If this engine never achieved a response body, its connection cannot be reused. if (responseBody == null) { closeQuietly(connection); connection = null; return null; } // Close the response body. This will recycle the connection if it is eligible. closeQuietly(responseBody); // Clear the buffer held by the response body input stream adapter. closeQuietly(responseBodyBytes); // Close the connection if it cannot be reused. if (transport != null && !transport.canReuseConnection()) { closeQuietly(connection); connection = null; return null; } Connection result = connection; connection = null; return result; } /** * Initialize the response content stream from the response transfer source. * These two sources are the same unless we're doing transparent gzip, in * which case the content source is decompressed. * *

Whenever we do transparent gzip we also strip the corresponding headers. * We strip the Content-Encoding header to prevent the application from * attempting to double decompress. We strip the Content-Length header because * it is the length of the compressed content, but the application is only * interested in the length of the uncompressed content. * *

This method should only be used for non-empty response bodies. Response * codes like "304 Not Modified" can include "Content-Encoding: gzip" without * a response body and we will crash if we attempt to decompress the zero-byte * source. */ private void initContentStream(Source transferSource) throws IOException { responseTransferSource = transferSource; if (transparentGzip && "gzip".equalsIgnoreCase(response.header("Content-Encoding"))) { response = response.newBuilder() .removeHeader("Content-Encoding") .removeHeader("Content-Length") .build(); responseBody = new GzipSource(transferSource); } else { responseBody = transferSource; } } /** * Returns true if the response must have a (possibly 0-length) body. * See RFC 2616 section 4.3. */ public final boolean hasResponseBody() { // HEAD requests never yield a body regardless of the response headers. if (request.method().equals("HEAD")) { return false; } int responseCode = response.code(); if ((responseCode < HTTP_CONTINUE || responseCode >= 200) && responseCode != HTTP_NO_CONTENT && responseCode != HTTP_NOT_MODIFIED) { return true; } // If the Content-Length or Transfer-Encoding headers disagree with the // response code, the response is malformed. For best compatibility, we // honor the headers. if (OkHeaders.contentLength(response) != -1 || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) { return true; } return false; } /** * Populates request with defaults and cookies. * *

This client doesn't specify a default {@code Accept} header because it * doesn't know what content types the application is interested in. */ private void prepareRawRequestHeaders() throws IOException { Request.Builder result = request.newBuilder(); if (request.getUserAgent() == null) { result.setUserAgent(getDefaultUserAgent()); } if (request.header("Host") == null) { result.header("Host", hostHeader(request.url())); } if ((connection == null || connection.getHttpMinorVersion() != 0) && request.header("Connection") == null) { result.header("Connection", "Keep-Alive"); } if (request.header("Accept-Encoding") == null) { transparentGzip = true; result.header("Accept-Encoding", "gzip"); } if (hasRequestBody() && request.header("Content-Type") == null) { result.header("Content-Type", "application/x-www-form-urlencoded"); } CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { // Capture the request headers added so far so that they can be offered to the CookieHandler. // This is mostly to stay close to the RI; it is unlikely any of the headers above would // affect cookie choice besides "Host". Map> headers = OkHeaders.toMultimap(result.build().headers(), null); Map> cookies = cookieHandler.get(request.uri(), headers); // Add any new cookies to the request. OkHeaders.addCookies(result, cookies); } request = result.build(); } public static String getDefaultUserAgent() { String agent = System.getProperty("http.agent"); return agent != null ? agent : ("Java" + System.getProperty("java.version")); } public static String hostHeader(URL url) { return getEffectivePort(url) != getDefaultPort(url.getProtocol()) ? url.getHost() + ":" + url.getPort() : url.getHost(); } /** * Flushes the remaining request header and body, parses the HTTP response * headers and starts reading the HTTP response body if it exists. */ public final void readResponse() throws IOException { if (response != null) return; if (responseSource == null) throw new IllegalStateException("call sendRequest() first!"); if (!responseSource.requiresConnection()) return; // Flush the request body if there's data outstanding. if (bufferedRequestBody != null && bufferedRequestBody.buffer().size() > 0) { bufferedRequestBody.flush(); } if (sentRequestMillis == -1) { if (OkHeaders.contentLength(request) == -1 && requestBodyOut instanceof RetryableSink) { // We might not learn the Content-Length until the request body has been buffered. long contentLength = ((RetryableSink) requestBodyOut).contentLength(); request = request.newBuilder() .header("Content-Length", Long.toString(contentLength)) .build(); } transport.writeRequestHeaders(request); } if (requestBodyOut != null) { if (bufferedRequestBody != null) { // This also closes the wrapped requestBodyOut. bufferedRequestBody.close(); } else { requestBodyOut.close(); } if (requestBodyOut instanceof RetryableSink) { transport.writeRequestBody((RetryableSink) requestBodyOut); } } transport.flushRequest(); response = transport.readResponseHeaders() .request(request) .handshake(connection.getHandshake()) .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis)) .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis())) .setResponseSource(responseSource) .build(); connection.setHttpMinorVersion(response.httpMinorVersion()); receiveHeaders(response.headers()); if (responseSource == ResponseSource.CONDITIONAL_CACHE) { if (validatingResponse.validate(response)) { transport.emptyTransferStream(); releaseConnection(); response = combine(validatingResponse, response); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). OkResponseCache responseCache = client.getOkResponseCache(); responseCache.trackConditionalCacheHit(); responseCache.update(validatingResponse, cacheableResponse()); if (validatingResponse.body() != null) { initContentStream(validatingResponse.body().source()); } return; } else { closeQuietly(validatingResponse.body()); } } if (!hasResponseBody()) { // Don't call initContentStream() when the response doesn't have any content. responseTransferSource = transport.getTransferStream(cacheRequest); responseBody = responseTransferSource; return; } maybeCache(); initContentStream(transport.getTransferStream(cacheRequest)); } /** * Combines cached headers with a network headers as defined by RFC 2616, * 13.5.3. */ private static Response combine(Response cached, Response network) throws IOException { Headers.Builder result = new Headers.Builder(); Headers cachedHeaders = cached.headers(); for (int i = 0; i < cachedHeaders.size(); i++) { String fieldName = cachedHeaders.name(i); String value = cachedHeaders.value(i); if ("Warning".equals(fieldName) && value.startsWith("1")) { continue; // drop 100-level freshness warnings } if (!isEndToEnd(fieldName) || network.header(fieldName) == null) { result.add(fieldName, value); } } Headers networkHeaders = network.headers(); for (int i = 0; i < networkHeaders.size(); i++) { String fieldName = networkHeaders.name(i); if (isEndToEnd(fieldName)) { result.add(fieldName, networkHeaders.value(i)); } } return cached.newBuilder().headers(result.build()).build(); } /** * Returns true if {@code fieldName} is an end-to-end HTTP header, as * defined by RFC 2616, 13.5.1. */ private static boolean isEndToEnd(String fieldName) { return !"Connection".equalsIgnoreCase(fieldName) && !"Keep-Alive".equalsIgnoreCase(fieldName) && !"Proxy-Authenticate".equalsIgnoreCase(fieldName) && !"Proxy-Authorization".equalsIgnoreCase(fieldName) && !"TE".equalsIgnoreCase(fieldName) && !"Trailers".equalsIgnoreCase(fieldName) && !"Transfer-Encoding".equalsIgnoreCase(fieldName) && !"Upgrade".equalsIgnoreCase(fieldName); } private TunnelRequest getTunnelConfig() { if (!request.isHttps()) return null; String userAgent = request.getUserAgent(); if (userAgent == null) userAgent = getDefaultUserAgent(); URL url = request.url(); return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent, request.getProxyAuthorization()); } public void receiveHeaders(Headers headers) throws IOException { CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { cookieHandler.put(request.uri(), OkHeaders.toMultimap(headers, null)); } } }