HttpURLConnectionImpl.java revision a82f42bbeedd0b07f3892f3b0efaa8122dc8f264
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.Connection;
21import com.squareup.okhttp.OkHttpClient;
22import com.squareup.okhttp.internal.AbstractOutputStream;
23import com.squareup.okhttp.internal.FaultRecoveringOutputStream;
24import com.squareup.okhttp.internal.Platform;
25import com.squareup.okhttp.internal.Util;
26import java.io.FileNotFoundException;
27import java.io.IOException;
28import java.io.InputStream;
29import java.io.OutputStream;
30import java.net.HttpRetryException;
31import java.net.HttpURLConnection;
32import java.net.InetSocketAddress;
33import java.net.ProtocolException;
34import java.net.Proxy;
35import java.net.SocketPermission;
36import java.net.URL;
37import java.security.Permission;
38import java.security.cert.CertificateException;
39import java.util.ArrayList;
40import java.util.List;
41import java.util.Map;
42import java.util.concurrent.TimeUnit;
43import javax.net.ssl.SSLHandshakeException;
44
45import static com.squareup.okhttp.internal.Util.getEffectivePort;
46
47/**
48 * This implementation uses HttpEngine to send requests and receive responses.
49 * This class may use multiple HttpEngines to follow redirects, authentication
50 * retries, etc. to retrieve the final response body.
51 *
52 * <h3>What does 'connected' mean?</h3>
53 * This class inherits a {@code connected} field from the superclass. That field
54 * is <strong>not</strong> used to indicate not whether this URLConnection is
55 * currently connected. Instead, it indicates whether a connection has ever been
56 * attempted. Once a connection has been attempted, certain properties (request
57 * header fields, request method, etc.) are immutable. Test the {@code
58 * connection} field on this class for null/non-null to determine of an instance
59 * is currently connected to a server.
60 */
61public class HttpURLConnectionImpl extends HttpURLConnection implements Policy {
62
63  /** Numeric status code, 307: Temporary Redirect. */
64  static final int HTTP_TEMP_REDIRECT = 307;
65
66  /**
67   * How many redirects should we follow? Chrome follows 21; Firefox, curl,
68   * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
69   */
70  private static final int MAX_REDIRECTS = 20;
71
72  /**
73   * The minimum number of request body bytes to transmit before we're willing
74   * to let a routine {@link IOException} bubble up to the user. This is used to
75   * size a buffer for data that will be replayed upon error.
76   */
77  private static final int MAX_REPLAY_BUFFER_LENGTH = 8192;
78
79  final OkHttpClient client;
80
81  private final RawHeaders rawRequestHeaders = new RawHeaders();
82  /** Like the superclass field of the same name, but a long and available on all platforms. */
83  private long fixedContentLength = -1;
84  private int redirectionCount;
85  private FaultRecoveringOutputStream faultRecoveringRequestBody;
86  protected IOException httpEngineFailure;
87  protected HttpEngine httpEngine;
88
89  public HttpURLConnectionImpl(URL url, OkHttpClient client) {
90    super(url);
91    this.client = client;
92  }
93
94  @Override public final void connect() throws IOException {
95    initHttpEngine();
96    boolean success;
97    do {
98      success = execute(false);
99    } while (!success);
100  }
101
102  @Override public final void disconnect() {
103    // Calling disconnect() before a connection exists should have no effect.
104    if (httpEngine != null) {
105      // We close the response body here instead of in
106      // HttpEngine.release because that is called when input
107      // has been completely read from the underlying socket.
108      // However the response body can be a GZIPInputStream that
109      // still has unread data.
110      if (httpEngine.hasResponse()) {
111        Util.closeQuietly(httpEngine.getResponseBody());
112      }
113      httpEngine.release(true);
114    }
115  }
116
117  /**
118   * Returns an input stream from the server in the case of error such as the
119   * requested file (txt, htm, html) is not found on the remote server.
120   */
121  @Override public final InputStream getErrorStream() {
122    try {
123      HttpEngine response = getResponse();
124      if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) {
125        return response.getResponseBody();
126      }
127      return null;
128    } catch (IOException e) {
129      return null;
130    }
131  }
132
133  /**
134   * Returns the value of the field at {@code position}. Returns null if there
135   * are fewer than {@code position} headers.
136   */
137  @Override public final String getHeaderField(int position) {
138    try {
139      return getResponse().getResponseHeaders().getHeaders().getValue(position);
140    } catch (IOException e) {
141      return null;
142    }
143  }
144
145  /**
146   * Returns the value of the field corresponding to the {@code fieldName}, or
147   * null if there is no such field. If the field has multiple values, the
148   * last value is returned.
149   */
150  @Override public final String getHeaderField(String fieldName) {
151    try {
152      RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
153      return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName);
154    } catch (IOException e) {
155      return null;
156    }
157  }
158
159  @Override public final String getHeaderFieldKey(int position) {
160    try {
161      return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
162    } catch (IOException e) {
163      return null;
164    }
165  }
166
167  @Override public final Map<String, List<String>> getHeaderFields() {
168    try {
169      return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
170    } catch (IOException e) {
171      return null;
172    }
173  }
174
175  @Override public final Map<String, List<String>> getRequestProperties() {
176    if (connected) {
177      throw new IllegalStateException(
178          "Cannot access request header fields after connection is set");
179    }
180    return rawRequestHeaders.toMultimap(false);
181  }
182
183  @Override public final InputStream getInputStream() throws IOException {
184    if (!doInput) {
185      throw new ProtocolException("This protocol does not support input");
186    }
187
188    HttpEngine response = getResponse();
189
190    // if the requested file does not exist, throw an exception formerly the
191    // Error page from the server was returned if the requested file was
192    // text/html this has changed to return FileNotFoundException for all
193    // file types
194    if (getResponseCode() >= HTTP_BAD_REQUEST) {
195      throw new FileNotFoundException(url.toString());
196    }
197
198    InputStream result = response.getResponseBody();
199    if (result == null) {
200      throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
201    }
202    return result;
203  }
204
205  @Override public final OutputStream getOutputStream() throws IOException {
206    connect();
207
208    OutputStream out = httpEngine.getRequestBody();
209    if (out == null) {
210      throw new ProtocolException("method does not support a request body: " + method);
211    } else if (httpEngine.hasResponse()) {
212      throw new ProtocolException("cannot write request body after response has been read");
213    }
214
215    if (faultRecoveringRequestBody == null) {
216      faultRecoveringRequestBody = new FaultRecoveringOutputStream(MAX_REPLAY_BUFFER_LENGTH, out) {
217        @Override protected OutputStream replacementStream(IOException e) throws IOException {
218          if (httpEngine.getRequestBody() instanceof AbstractOutputStream
219              && ((AbstractOutputStream) httpEngine.getRequestBody()).isClosed()) {
220            return null; // Don't recover once the underlying stream has been closed.
221          }
222          if (handleFailure(e)) {
223            return httpEngine.getRequestBody();
224          }
225          return null; // This is a permanent failure.
226        }
227      };
228    }
229
230    return faultRecoveringRequestBody;
231  }
232
233  @Override public final Permission getPermission() throws IOException {
234    String hostName = getURL().getHost();
235    int hostPort = Util.getEffectivePort(getURL());
236    if (usingProxy()) {
237      InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address();
238      hostName = proxyAddress.getHostName();
239      hostPort = proxyAddress.getPort();
240    }
241    return new SocketPermission(hostName + ":" + hostPort, "connect, resolve");
242  }
243
244  @Override public final String getRequestProperty(String field) {
245    if (field == null) {
246      return null;
247    }
248    return rawRequestHeaders.get(field);
249  }
250
251  @Override public void setConnectTimeout(int timeoutMillis) {
252    client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
253  }
254
255  @Override public int getConnectTimeout() {
256    return client.getConnectTimeout();
257  }
258
259  @Override public void setReadTimeout(int timeoutMillis) {
260    client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
261  }
262
263  @Override public int getReadTimeout() {
264    return client.getReadTimeout();
265  }
266
267  private void initHttpEngine() throws IOException {
268    if (httpEngineFailure != null) {
269      throw httpEngineFailure;
270    } else if (httpEngine != null) {
271      return;
272    }
273
274    connected = true;
275    try {
276      if (doOutput) {
277        if (method.equals("GET")) {
278          // they are requesting a stream to write to. This implies a POST method
279          method = "POST";
280        } else if (!method.equals("POST") && !method.equals("PUT")) {
281          // If the request method is neither POST nor PUT, then you're not writing
282          throw new ProtocolException(method + " does not support writing");
283        }
284      }
285      httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
286    } catch (IOException e) {
287      httpEngineFailure = e;
288      throw e;
289    }
290  }
291
292  @Override public HttpURLConnection getHttpConnectionToCache() {
293    return this;
294  }
295
296  private HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
297      Connection connection, RetryableOutputStream requestBody) throws IOException {
298    if (url.getProtocol().equals("http")) {
299      return new HttpEngine(client, this, method, requestHeaders, connection, requestBody);
300    } else if (url.getProtocol().equals("https")) {
301      return new HttpsEngine(client, this, method, requestHeaders, connection, requestBody);
302    } else {
303      throw new AssertionError();
304    }
305  }
306
307  /**
308   * Aggressively tries to get the final HTTP response, potentially making
309   * many HTTP requests in the process in order to cope with redirects and
310   * authentication.
311   */
312  private HttpEngine getResponse() throws IOException {
313    initHttpEngine();
314
315    if (httpEngine.hasResponse()) {
316      return httpEngine;
317    }
318
319    while (true) {
320      if (!execute(true)) {
321        continue;
322      }
323
324      Retry retry = processResponseHeaders();
325      if (retry == Retry.NONE) {
326        httpEngine.automaticallyReleaseConnectionToPool();
327        return httpEngine;
328      }
329
330      // The first request was insufficient. Prepare for another...
331      String retryMethod = method;
332      OutputStream requestBody = httpEngine.getRequestBody();
333
334      // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
335      // redirect should keep the same method, Chrome, Firefox and the
336      // RI all issue GETs when following any redirect.
337      int responseCode = getResponseCode();
338      if (responseCode == HTTP_MULT_CHOICE
339          || responseCode == HTTP_MOVED_PERM
340          || responseCode == HTTP_MOVED_TEMP
341          || responseCode == HTTP_SEE_OTHER) {
342        retryMethod = "GET";
343        requestBody = null;
344      }
345
346      if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
347        throw new HttpRetryException("Cannot retry streamed HTTP body",
348            httpEngine.getResponseCode());
349      }
350
351      if (retry == Retry.DIFFERENT_CONNECTION) {
352        httpEngine.automaticallyReleaseConnectionToPool();
353      }
354
355      httpEngine.release(false);
356
357      httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
358          (RetryableOutputStream) requestBody);
359    }
360  }
361
362  /**
363   * Sends a request and optionally reads a response. Returns true if the
364   * request was successfully executed, and false if the request can be
365   * retried. Throws an exception if the request failed permanently.
366   */
367  private boolean execute(boolean readResponse) throws IOException {
368    try {
369      httpEngine.sendRequest();
370      if (readResponse) {
371        httpEngine.readResponse();
372      }
373      return true;
374    } catch (IOException e) {
375      if (handleFailure(e)) {
376        return false;
377      } else {
378        throw e;
379      }
380    }
381  }
382
383  /**
384   * Report and attempt to recover from {@code e}. Returns true if the HTTP
385   * engine was replaced and the request should be retried. Otherwise the
386   * failure is permanent.
387   */
388  private boolean handleFailure(IOException e) throws IOException {
389    RouteSelector routeSelector = httpEngine.routeSelector;
390    if (routeSelector != null && httpEngine.connection != null) {
391      routeSelector.connectFailed(httpEngine.connection, e);
392    }
393
394    OutputStream requestBody = httpEngine.getRequestBody();
395    boolean canRetryRequestBody = requestBody == null
396        || requestBody instanceof RetryableOutputStream
397        || (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable());
398    if (routeSelector == null && httpEngine.connection == null // No connection.
399        || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
400        || !isRecoverable(e)
401        || !canRetryRequestBody) {
402      httpEngineFailure = e;
403      return false;
404    }
405
406    httpEngine.release(true);
407    RetryableOutputStream retryableOutputStream = requestBody instanceof RetryableOutputStream
408        ? (RetryableOutputStream) requestBody
409        : null;
410    httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream);
411    httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
412    if (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()) {
413      httpEngine.sendRequest();
414      faultRecoveringRequestBody.replaceStream(httpEngine.getRequestBody());
415    }
416    return true;
417  }
418
419  private boolean isRecoverable(IOException e) {
420    // If the problem was a CertificateException from the X509TrustManager,
421    // do not retry, we didn't have an abrupt server initiated exception.
422    boolean sslFailure =
423        e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException;
424    boolean protocolFailure = e instanceof ProtocolException;
425    return !sslFailure && !protocolFailure;
426  }
427
428  public HttpEngine getHttpEngine() {
429    return httpEngine;
430  }
431
432  enum Retry {
433    NONE,
434    SAME_CONNECTION,
435    DIFFERENT_CONNECTION
436  }
437
438  /**
439   * Returns the retry action to take for the current response headers. The
440   * headers, proxy and target URL or this connection may be adjusted to
441   * prepare for a follow up request.
442   */
443  private Retry processResponseHeaders() throws IOException {
444    Proxy selectedProxy = httpEngine.connection != null
445        ? httpEngine.connection.getRoute().getProxy()
446        : client.getProxy();
447    final int responseCode = getResponseCode();
448    switch (responseCode) {
449      case HTTP_PROXY_AUTH:
450        if (selectedProxy.type() != Proxy.Type.HTTP) {
451          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
452        }
453        // fall-through
454      case HTTP_UNAUTHORIZED:
455        boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
456            getResponseCode(), httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders,
457            selectedProxy, url);
458        return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
459
460      case HTTP_MULT_CHOICE:
461      case HTTP_MOVED_PERM:
462      case HTTP_MOVED_TEMP:
463      case HTTP_SEE_OTHER:
464      case HTTP_TEMP_REDIRECT:
465        if (!getInstanceFollowRedirects()) {
466          return Retry.NONE;
467        }
468        if (++redirectionCount > MAX_REDIRECTS) {
469          throw new ProtocolException("Too many redirects: " + redirectionCount);
470        }
471        if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
472          // "If the 307 status code is received in response to a request other than GET or HEAD,
473          // the user agent MUST NOT automatically redirect the request"
474          return Retry.NONE;
475        }
476        String location = getHeaderField("Location");
477        if (location == null) {
478          return Retry.NONE;
479        }
480        URL previousUrl = url;
481        url = new URL(previousUrl, location);
482        if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
483          return Retry.NONE; // Don't follow redirects to unsupported protocols.
484        }
485        boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol());
486        if (!sameProtocol && !client.getFollowProtocolRedirects()) {
487          return Retry.NONE; // This client doesn't follow redirects across protocols.
488        }
489        boolean sameHost = previousUrl.getHost().equals(url.getHost());
490        boolean samePort = getEffectivePort(previousUrl) == getEffectivePort(url);
491        if (sameHost && samePort && sameProtocol) {
492          return Retry.SAME_CONNECTION;
493        } else {
494          return Retry.DIFFERENT_CONNECTION;
495        }
496
497      default:
498        return Retry.NONE;
499    }
500  }
501
502  /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
503  @Override public final long getFixedContentLength() {
504    return fixedContentLength;
505  }
506
507  @Override public final int getChunkLength() {
508    return chunkLength;
509  }
510
511  @Override public final boolean usingProxy() {
512    Proxy proxy = client.getProxy();
513    return proxy != null && proxy.type() != Proxy.Type.DIRECT;
514  }
515
516  @Override public String getResponseMessage() throws IOException {
517    return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
518  }
519
520  @Override public final int getResponseCode() throws IOException {
521    return getResponse().getResponseCode();
522  }
523
524  @Override public final void setRequestProperty(String field, String newValue) {
525    if (connected) {
526      throw new IllegalStateException("Cannot set request property after connection is made");
527    }
528    if (field == null) {
529      throw new NullPointerException("field == null");
530    }
531    if (newValue == null) {
532      // Silently ignore null header values for backwards compatibility with older
533      // android versions as well as with other URLConnection implementations.
534      //
535      // Some implementations send a malformed HTTP header when faced with
536      // such requests, we respect the spec and ignore the header.
537      Platform.get().logW("Ignoring header " + field + " because its value was null.");
538      return;
539    }
540
541    if ("X-Android-Transports".equals(field)) {
542      setTransports(newValue, false /* append */);
543    } else {
544      rawRequestHeaders.set(field, newValue);
545    }
546  }
547
548  @Override public final void addRequestProperty(String field, String value) {
549    if (connected) {
550      throw new IllegalStateException("Cannot add request property after connection is made");
551    }
552    if (field == null) {
553      throw new NullPointerException("field == null");
554    }
555    if (value == null) {
556      // Silently ignore null header values for backwards compatibility with older
557      // android versions as well as with other URLConnection implementations.
558      //
559      // Some implementations send a malformed HTTP header when faced with
560      // such requests, we respect the spec and ignore the header.
561      Platform.get().logW("Ignoring header " + field + " because its value was null.");
562      return;
563    }
564
565    if ("X-Android-Transports".equals(field)) {
566      setTransports(value, true /* append */);
567    } else {
568      rawRequestHeaders.add(field, value);
569    }
570  }
571
572  /*
573   * Splits and validates a comma-separated string of transports.
574   * When append == false, we require that the transport list contains "http/1.1".
575   */
576  private void setTransports(String transportsString, boolean append) {
577    List<String> transportsList = new ArrayList<String>();
578    if (append) {
579      transportsList.addAll(client.getTransports());
580    }
581    for (String transport : transportsString.split(",", -1)) {
582      transportsList.add(transport);
583    }
584    client.setTransports(transportsList);
585  }
586
587  @Override public void setFixedLengthStreamingMode(int contentLength) {
588    setFixedLengthStreamingMode((long) contentLength);
589  }
590
591  // @Override Don't override: this overload method doesn't exist prior to Java 1.7.
592  public void setFixedLengthStreamingMode(long contentLength) {
593    if (super.connected) throw new IllegalStateException("Already connected");
594    if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode");
595    if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0");
596    this.fixedContentLength = contentLength;
597    super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE);
598  }
599}
600