HttpEngine.java revision 76739264ce52fe7a6c5c3558dad87b649118deff
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
18package com.squareup.okhttp.internal.http;
19
20import com.squareup.okhttp.Address;
21import com.squareup.okhttp.Connection;
22import com.squareup.okhttp.OkHttpClient;
23import com.squareup.okhttp.OkResponseCache;
24import com.squareup.okhttp.ResponseSource;
25import com.squareup.okhttp.TunnelRequest;
26import com.squareup.okhttp.internal.Dns;
27import com.squareup.okhttp.internal.Platform;
28import com.squareup.okhttp.internal.Util;
29import java.io.ByteArrayInputStream;
30import java.io.IOException;
31import java.io.InputStream;
32import java.io.OutputStream;
33import java.net.CacheRequest;
34import java.net.CacheResponse;
35import java.net.CookieHandler;
36import java.net.HttpURLConnection;
37import java.net.Proxy;
38import java.net.URI;
39import java.net.URISyntaxException;
40import java.net.URL;
41import java.net.UnknownHostException;
42import java.util.Collections;
43import java.util.Date;
44import java.util.HashMap;
45import java.util.List;
46import java.util.Map;
47import java.util.zip.GZIPInputStream;
48import javax.net.ssl.HostnameVerifier;
49import javax.net.ssl.SSLSocketFactory;
50
51import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY;
52import static com.squareup.okhttp.internal.Util.getDefaultPort;
53import 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 */
78public 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    connected = true;
313  }
314
315  /**
316   * Called immediately before the transport transmits HTTP request headers.
317   * This is used to observe the sent time should the request be cached.
318   */
319  public void writingRequestHeaders() {
320    if (sentRequestMillis != -1) {
321      throw new IllegalStateException();
322    }
323    sentRequestMillis = System.currentTimeMillis();
324  }
325
326  /**
327   * @param body the response body, or null if it doesn't exist or isn't
328   * available.
329   */
330  private void setResponse(ResponseHeaders headers, InputStream body) throws IOException {
331    if (this.responseBodyIn != null) {
332      throw new IllegalStateException();
333    }
334    this.responseHeaders = headers;
335    if (body != null) {
336      initContentStream(body);
337    }
338  }
339
340  boolean hasRequestBody() {
341    return method.equals("POST") || method.equals("PUT");
342  }
343
344  /** Returns the request body or null if this request doesn't have a body. */
345  public final OutputStream getRequestBody() {
346    if (responseSource == null) {
347      throw new IllegalStateException();
348    }
349    return requestBodyOut;
350  }
351
352  public final boolean hasResponse() {
353    return responseHeaders != null;
354  }
355
356  public final RequestHeaders getRequestHeaders() {
357    return requestHeaders;
358  }
359
360  public final ResponseHeaders getResponseHeaders() {
361    if (responseHeaders == null) {
362      throw new IllegalStateException();
363    }
364    return responseHeaders;
365  }
366
367  public final int getResponseCode() {
368    if (responseHeaders == null) {
369      throw new IllegalStateException();
370    }
371    return responseHeaders.getHeaders().getResponseCode();
372  }
373
374  public final InputStream getResponseBody() {
375    if (responseHeaders == null) {
376      throw new IllegalStateException();
377    }
378    return responseBodyIn;
379  }
380
381  public final CacheResponse getCacheResponse() {
382    return cacheResponse;
383  }
384
385  public final Connection getConnection() {
386    return connection;
387  }
388
389  /**
390   * Returns true if {@code cacheResponse} is of the right type. This
391   * condition is necessary but not sufficient for the cached response to
392   * be used.
393   */
394  protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
395    return true;
396  }
397
398  private void maybeCache() throws IOException {
399    // Are we caching at all?
400    if (!policy.getUseCaches()) return;
401    OkResponseCache responseCache = client.getOkResponseCache();
402    if (responseCache == null) return;
403
404    HttpURLConnection connectionToCache = policy.getHttpConnectionToCache();
405
406    // Should we cache this response for this request?
407    if (!responseHeaders.isCacheable(requestHeaders)) {
408      responseCache.maybeRemove(connectionToCache.getRequestMethod(), uri);
409      return;
410    }
411
412    // Offer this request to the cache.
413    cacheRequest = responseCache.put(uri, connectionToCache);
414  }
415
416  /**
417   * Cause the socket connection to be released to the connection pool when
418   * it is no longer needed. If it is already unneeded, it will be pooled
419   * immediately. Otherwise the connection is held so that redirects can be
420   * handled by the same connection.
421   */
422  public final void automaticallyReleaseConnectionToPool() {
423    automaticallyReleaseConnectionToPool = true;
424    if (connection != null && connectionReleased) {
425      client.getConnectionPool().recycle(connection);
426      connection = null;
427    }
428  }
429
430  /**
431   * Releases this engine so that its resources may be either reused or
432   * closed. Also call {@link #automaticallyReleaseConnectionToPool} unless
433   * the connection will be used to follow a redirect.
434   */
435  public final void release(boolean streamCancelled) {
436    // If the response body comes from the cache, close it.
437    if (responseBodyIn == cachedResponseBody) {
438      Util.closeQuietly(responseBodyIn);
439    }
440
441    if (!connectionReleased && connection != null) {
442      connectionReleased = true;
443
444      if (transport == null || !transport.makeReusable(streamCancelled, requestBodyOut,
445          responseTransferIn)) {
446        Util.closeQuietly(connection);
447        connection = null;
448      } else if (automaticallyReleaseConnectionToPool) {
449        client.getConnectionPool().recycle(connection);
450        connection = null;
451      }
452    }
453  }
454
455  private void initContentStream(InputStream transferStream) throws IOException {
456    responseTransferIn = transferStream;
457    if (transparentGzip && responseHeaders.isContentEncodingGzip()) {
458      // If the response was transparently gzipped, remove the gzip header field
459      // so clients don't double decompress. http://b/3009828
460      //
461      // Also remove the Content-Length in this case because it contains the
462      // length 528 of the gzipped response. This isn't terribly useful and is
463      // dangerous because 529 clients can query the content length, but not
464      // the content encoding.
465      responseHeaders.stripContentEncoding();
466      responseHeaders.stripContentLength();
467      responseBodyIn = new GZIPInputStream(transferStream);
468    } else {
469      responseBodyIn = transferStream;
470    }
471  }
472
473  /**
474   * Returns true if the response must have a (possibly 0-length) body.
475   * See RFC 2616 section 4.3.
476   */
477  public final boolean hasResponseBody() {
478    int responseCode = responseHeaders.getHeaders().getResponseCode();
479
480    // HEAD requests never yield a body regardless of the response headers.
481    if (method.equals("HEAD")) {
482      return false;
483    }
484
485    if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
486        && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT
487        && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) {
488      return true;
489    }
490
491    // If the Content-Length or Transfer-Encoding headers disagree with the
492    // response code, the response is malformed. For best compatibility, we
493    // honor the headers.
494    if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) {
495      return true;
496    }
497
498    return false;
499  }
500
501  /**
502   * Populates requestHeaders with defaults and cookies.
503   *
504   * <p>This client doesn't specify a default {@code Accept} header because it
505   * doesn't know what content types the application is interested in.
506   */
507  private void prepareRawRequestHeaders() throws IOException {
508    requestHeaders.getHeaders().setRequestLine(getRequestLine());
509
510    if (requestHeaders.getUserAgent() == null) {
511      requestHeaders.setUserAgent(getDefaultUserAgent());
512    }
513
514    if (requestHeaders.getHost() == null) {
515      requestHeaders.setHost(getOriginAddress(policy.getURL()));
516    }
517
518    if ((connection == null || connection.getHttpMinorVersion() != 0)
519        && requestHeaders.getConnection() == null) {
520      requestHeaders.setConnection("Keep-Alive");
521    }
522
523    if (requestHeaders.getAcceptEncoding() == null) {
524      transparentGzip = true;
525      requestHeaders.setAcceptEncoding("gzip");
526    }
527
528    if (hasRequestBody() && requestHeaders.getContentType() == null) {
529      requestHeaders.setContentType("application/x-www-form-urlencoded");
530    }
531
532    long ifModifiedSince = policy.getIfModifiedSince();
533    if (ifModifiedSince != 0) {
534      requestHeaders.setIfModifiedSince(new Date(ifModifiedSince));
535    }
536
537    CookieHandler cookieHandler = client.getCookieHandler();
538    if (cookieHandler != null) {
539      requestHeaders.addCookies(
540          cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false)));
541    }
542  }
543
544  /**
545   * Returns the request status line, like "GET / HTTP/1.1". This is exposed
546   * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so
547   * it needs to be set even if the transport is SPDY.
548   */
549  String getRequestLine() {
550    String protocol =
551        (connection == null || connection.getHttpMinorVersion() != 0) ? "HTTP/1.1" : "HTTP/1.0";
552    return method + " " + requestString() + " " + protocol;
553  }
554
555  private String requestString() {
556    URL url = policy.getURL();
557    if (includeAuthorityInRequestLine()) {
558      return url.toString();
559    } else {
560      return requestPath(url);
561    }
562  }
563
564  /**
565   * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never
566   * empty, even if the request URL is. Includes the query component if it
567   * exists.
568   */
569  public static String requestPath(URL url) {
570    String fileOnly = url.getFile();
571    if (fileOnly == null) {
572      return "/";
573    } else if (!fileOnly.startsWith("/")) {
574      return "/" + fileOnly;
575    } else {
576      return fileOnly;
577    }
578  }
579
580  /**
581   * Returns true if the request line should contain the full URL with host
582   * and port (like "GET http://android.com/foo HTTP/1.1") or only the path
583   * (like "GET /foo HTTP/1.1").
584   *
585   * <p>This is non-final because for HTTPS it's never necessary to supply the
586   * full URL, even if a proxy is in use.
587   */
588  protected boolean includeAuthorityInRequestLine() {
589    return connection == null
590        ? policy.usingProxy() // A proxy was requested.
591        : connection.getRoute().getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
592  }
593
594  public static String getDefaultUserAgent() {
595    String agent = System.getProperty("http.agent");
596    return agent != null ? agent : ("Java" + System.getProperty("java.version"));
597  }
598
599  public static String getOriginAddress(URL url) {
600    int port = url.getPort();
601    String result = url.getHost();
602    if (port > 0 && port != getDefaultPort(url.getProtocol())) {
603      result = result + ":" + port;
604    }
605    return result;
606  }
607
608  /**
609   * Flushes the remaining request header and body, parses the HTTP response
610   * headers and starts reading the HTTP response body if it exists.
611   */
612  public final void readResponse() throws IOException {
613    if (hasResponse()) {
614      responseHeaders.setResponseSource(responseSource);
615      return;
616    }
617
618    if (responseSource == null) {
619      throw new IllegalStateException("readResponse() without sendRequest()");
620    }
621
622    if (!responseSource.requiresConnection()) {
623      return;
624    }
625
626    if (sentRequestMillis == -1) {
627      if (requestBodyOut instanceof RetryableOutputStream) {
628        int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength();
629        requestHeaders.setContentLength(contentLength);
630      }
631      transport.writeRequestHeaders();
632    }
633
634    if (requestBodyOut != null) {
635      requestBodyOut.close();
636      if (requestBodyOut instanceof RetryableOutputStream) {
637        transport.writeRequestBody((RetryableOutputStream) requestBodyOut);
638      }
639    }
640
641    transport.flushRequest();
642
643    responseHeaders = transport.readResponseHeaders();
644    responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis());
645    responseHeaders.setResponseSource(responseSource);
646
647    if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
648      if (cachedResponseHeaders.validate(responseHeaders)) {
649        release(false);
650        ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
651        setResponse(combinedHeaders, cachedResponseBody);
652        OkResponseCache responseCache = client.getOkResponseCache();
653        responseCache.trackConditionalCacheHit();
654        responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
655        return;
656      } else {
657        Util.closeQuietly(cachedResponseBody);
658      }
659    }
660
661    if (hasResponseBody()) {
662      maybeCache(); // reentrant. this calls into user code which may call back into this!
663    }
664
665    initContentStream(transport.getTransferStream(cacheRequest));
666  }
667
668  protected TunnelRequest getTunnelConfig() {
669    return null;
670  }
671
672  public void receiveHeaders(RawHeaders headers) throws IOException {
673    CookieHandler cookieHandler = client.getCookieHandler();
674    if (cookieHandler != null) {
675      cookieHandler.put(uri, headers.toMultimap(true));
676    }
677  }
678}
679