HttpURLConnectionImpl.java revision 54cf3446000fdcf88a9e62724f7deb0282e98da1
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.ConnectionPool;
22import com.squareup.okhttp.internal.Util;
23import java.io.FileNotFoundException;
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.OutputStream;
27import java.net.CookieHandler;
28import java.net.HttpRetryException;
29import java.net.HttpURLConnection;
30import java.net.InetSocketAddress;
31import java.net.ProtocolException;
32import java.net.Proxy;
33import java.net.ProxySelector;
34import java.net.ResponseCache;
35import java.net.SocketPermission;
36import java.net.URL;
37import java.security.Permission;
38import java.security.cert.CertificateException;
39import java.util.List;
40import java.util.Map;
41import javax.net.ssl.SSLHandshakeException;
42
43import static com.squareup.okhttp.internal.Util.getEffectivePort;
44
45/**
46 * This implementation uses HttpEngine to send requests and receive responses.
47 * This class may use multiple HttpEngines to follow redirects, authentication
48 * retries, etc. to retrieve the final response body.
49 *
50 * <h3>What does 'connected' mean?</h3>
51 * This class inherits a {@code connected} field from the superclass. That field
52 * is <strong>not</strong> used to indicate not whether this URLConnection is
53 * currently connected. Instead, it indicates whether a connection has ever been
54 * attempted. Once a connection has been attempted, certain properties (request
55 * header fields, request method, etc.) are immutable. Test the {@code
56 * connection} field on this class for null/non-null to determine of an instance
57 * is currently connected to a server.
58 */
59public class HttpURLConnectionImpl extends HttpURLConnection {
60  /**
61   * How many redirects should we follow? Chrome follows 21; Firefox, curl,
62   * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
63   */
64  private static final int MAX_REDIRECTS = 20;
65
66  private final int defaultPort;
67
68  private Proxy proxy;
69  final ProxySelector proxySelector;
70  final CookieHandler cookieHandler;
71  final ResponseCache responseCache;
72  final ConnectionPool connectionPool;
73
74  private final RawHeaders rawRequestHeaders = new RawHeaders();
75
76  private int redirectionCount;
77
78  protected IOException httpEngineFailure;
79  protected HttpEngine httpEngine;
80
81  public HttpURLConnectionImpl(URL url, int defaultPort, Proxy proxy, ProxySelector proxySelector,
82      CookieHandler cookieHandler, ResponseCache responseCache, ConnectionPool connectionPool) {
83    super(url);
84    this.defaultPort = defaultPort;
85    this.proxy = proxy;
86    this.proxySelector = proxySelector;
87    this.cookieHandler = cookieHandler;
88    this.responseCache = responseCache;
89    this.connectionPool = connectionPool;
90  }
91
92  @Override public final void connect() throws IOException {
93    initHttpEngine();
94    boolean success;
95    do {
96      success = execute(false);
97    } while (!success);
98  }
99
100  @Override public final void disconnect() {
101    // Calling disconnect() before a connection exists should have no effect.
102    if (httpEngine != null) {
103      // We close the response body here instead of in
104      // HttpEngine.release because that is called when input
105      // has been completely read from the underlying socket.
106      // However the response body can be a GZIPInputStream that
107      // still has unread data.
108      if (httpEngine.hasResponse()) {
109        Util.closeQuietly(httpEngine.getResponseBody());
110      }
111      httpEngine.release(true);
112    }
113  }
114
115  /**
116   * Returns an input stream from the server in the case of error such as the
117   * requested file (txt, htm, html) is not found on the remote server.
118   */
119  @Override public final InputStream getErrorStream() {
120    try {
121      HttpEngine response = getResponse();
122      if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) {
123        return response.getResponseBody();
124      }
125      return null;
126    } catch (IOException e) {
127      return null;
128    }
129  }
130
131  /**
132   * Returns the value of the field at {@code position}. Returns null if there
133   * are fewer than {@code position} headers.
134   */
135  @Override public final String getHeaderField(int position) {
136    try {
137      return getResponse().getResponseHeaders().getHeaders().getValue(position);
138    } catch (IOException e) {
139      return null;
140    }
141  }
142
143  /**
144   * Returns the value of the field corresponding to the {@code fieldName}, or
145   * null if there is no such field. If the field has multiple values, the
146   * last value is returned.
147   */
148  @Override public final String getHeaderField(String fieldName) {
149    try {
150      RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
151      return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName);
152    } catch (IOException e) {
153      return null;
154    }
155  }
156
157  @Override public final String getHeaderFieldKey(int position) {
158    try {
159      return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
160    } catch (IOException e) {
161      return null;
162    }
163  }
164
165  @Override public final Map<String, List<String>> getHeaderFields() {
166    try {
167      return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
168    } catch (IOException e) {
169      return null;
170    }
171  }
172
173  @Override public final Map<String, List<String>> getRequestProperties() {
174    if (connected) {
175      throw new IllegalStateException(
176          "Cannot access request header fields after connection is set");
177    }
178    return rawRequestHeaders.toMultimap(false);
179  }
180
181  @Override public final InputStream getInputStream() throws IOException {
182    if (!doInput) {
183      throw new ProtocolException("This protocol does not support input");
184    }
185
186    HttpEngine response = getResponse();
187
188    // if the requested file does not exist, throw an exception formerly the
189    // Error page from the server was returned if the requested file was
190    // text/html this has changed to return FileNotFoundException for all
191    // file types
192    if (getResponseCode() >= HTTP_BAD_REQUEST) {
193      throw new FileNotFoundException(url.toString());
194    }
195
196    InputStream result = response.getResponseBody();
197    if (result == null) {
198      throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
199    }
200    return result;
201  }
202
203  @Override public final OutputStream getOutputStream() throws IOException {
204    connect();
205
206    OutputStream result = httpEngine.getRequestBody();
207    if (result == null) {
208      throw new ProtocolException("method does not support a request body: " + method);
209    } else if (httpEngine.hasResponse()) {
210      throw new ProtocolException("cannot write request body after response has been read");
211    }
212
213    return result;
214  }
215
216  @Override public final Permission getPermission() throws IOException {
217    String connectToAddress = getConnectToHost() + ":" + getConnectToPort();
218    return new SocketPermission(connectToAddress, "connect, resolve");
219  }
220
221  private String getConnectToHost() {
222    return usingProxy() ? ((InetSocketAddress) proxy.address()).getHostName() : getURL().getHost();
223  }
224
225  private int getConnectToPort() {
226    int hostPort =
227        usingProxy() ? ((InetSocketAddress) proxy.address()).getPort() : getURL().getPort();
228    return hostPort < 0 ? getDefaultPort() : hostPort;
229  }
230
231  @Override public final String getRequestProperty(String field) {
232    if (field == null) {
233      return null;
234    }
235    return rawRequestHeaders.get(field);
236  }
237
238  private void initHttpEngine() throws IOException {
239    if (httpEngineFailure != null) {
240      throw httpEngineFailure;
241    } else if (httpEngine != null) {
242      return;
243    }
244
245    connected = true;
246    try {
247      if (doOutput) {
248        if (method.equals("GET")) {
249          // they are requesting a stream to write to. This implies a POST method
250          method = "POST";
251        } else if (!method.equals("POST") && !method.equals("PUT")) {
252          // If the request method is neither POST nor PUT, then you're not writing
253          throw new ProtocolException(method + " does not support writing");
254        }
255      }
256      httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
257    } catch (IOException e) {
258      httpEngineFailure = e;
259      throw e;
260    }
261  }
262
263  /**
264   * Create a new HTTP engine. This hook method is non-final so it can be
265   * overridden by HttpsURLConnectionImpl.
266   */
267  protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
268      Connection connection, RetryableOutputStream requestBody) throws IOException {
269    return new HttpEngine(this, method, requestHeaders, connection, requestBody);
270  }
271
272  /**
273   * Aggressively tries to get the final HTTP response, potentially making
274   * many HTTP requests in the process in order to cope with redirects and
275   * authentication.
276   */
277  private HttpEngine getResponse() throws IOException {
278    initHttpEngine();
279
280    if (httpEngine.hasResponse()) {
281      return httpEngine;
282    }
283
284    while (true) {
285      if (!execute(true)) {
286        continue;
287      }
288
289      Retry retry = processResponseHeaders();
290      if (retry == Retry.NONE) {
291        httpEngine.automaticallyReleaseConnectionToPool();
292        return httpEngine;
293      }
294
295      // The first request was insufficient. Prepare for another...
296      String retryMethod = method;
297      OutputStream requestBody = httpEngine.getRequestBody();
298
299      // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
300      // redirect should keep the same method, Chrome, Firefox and the
301      // RI all issue GETs when following any redirect.
302      int responseCode = getResponseCode();
303      if (responseCode == HTTP_MULT_CHOICE
304          || responseCode == HTTP_MOVED_PERM
305          || responseCode == HTTP_MOVED_TEMP
306          || responseCode == HTTP_SEE_OTHER) {
307        retryMethod = "GET";
308        requestBody = null;
309      }
310
311      if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
312        throw new HttpRetryException("Cannot retry streamed HTTP body",
313            httpEngine.getResponseCode());
314      }
315
316      if (retry == Retry.DIFFERENT_CONNECTION) {
317        httpEngine.automaticallyReleaseConnectionToPool();
318      }
319
320      httpEngine.release(false);
321
322      httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
323          (RetryableOutputStream) requestBody);
324    }
325  }
326
327  /**
328   * Sends a request and optionally reads a response. Returns true if the
329   * request was successfully executed, and false if the request can be
330   * retried. Throws an exception if the request failed permanently.
331   */
332  private boolean execute(boolean readResponse) throws IOException {
333    try {
334      httpEngine.sendRequest();
335      if (readResponse) {
336        httpEngine.readResponse();
337      }
338      return true;
339    } catch (IOException e) {
340      RouteSelector routeSelector = httpEngine.routeSelector;
341      if (routeSelector != null && httpEngine.connection != null) {
342        routeSelector.connectFailed(httpEngine.connection, e);
343      }
344      if (routeSelector == null && httpEngine.connection == null) {
345        throw e; // If we failed before finding a route or a connection, give up.
346      }
347
348      // The connection failure isn't fatal if there's another route to attempt.
349      OutputStream requestBody = httpEngine.getRequestBody();
350      if ((routeSelector == null || routeSelector.hasNext()) && isRecoverable(e) && (requestBody
351          == null || requestBody instanceof RetryableOutputStream)) {
352        httpEngine.release(true);
353        httpEngine =
354            newHttpEngine(method, rawRequestHeaders, null, (RetryableOutputStream) requestBody);
355        httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
356        return false;
357      }
358      httpEngineFailure = e;
359      throw e;
360    }
361  }
362
363  private boolean isRecoverable(IOException e) {
364    // If the problem was a CertificateException from the X509TrustManager,
365    // do not retry, we didn't have an abrupt server initiated exception.
366    boolean sslFailure =
367        e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException;
368    boolean protocolFailure = e instanceof ProtocolException;
369    return !sslFailure && !protocolFailure;
370  }
371
372  HttpEngine getHttpEngine() {
373    return httpEngine;
374  }
375
376  enum Retry {
377    NONE,
378    SAME_CONNECTION,
379    DIFFERENT_CONNECTION
380  }
381
382  /**
383   * Returns the retry action to take for the current response headers. The
384   * headers, proxy and target URL or this connection may be adjusted to
385   * prepare for a follow up request.
386   */
387  private Retry processResponseHeaders() throws IOException {
388    switch (getResponseCode()) {
389      case HTTP_PROXY_AUTH:
390        if (!usingProxy()) {
391          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
392        }
393        // fall-through
394      case HTTP_UNAUTHORIZED:
395        boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
396            httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, proxy, url);
397        return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
398
399      case HTTP_MULT_CHOICE:
400      case HTTP_MOVED_PERM:
401      case HTTP_MOVED_TEMP:
402      case HTTP_SEE_OTHER:
403        if (!getInstanceFollowRedirects()) {
404          return Retry.NONE;
405        }
406        if (++redirectionCount > MAX_REDIRECTS) {
407          throw new ProtocolException("Too many redirects: " + redirectionCount);
408        }
409        String location = getHeaderField("Location");
410        if (location == null) {
411          return Retry.NONE;
412        }
413        URL previousUrl = url;
414        url = new URL(previousUrl, location);
415        if (!previousUrl.getProtocol().equals(url.getProtocol())) {
416          return Retry.NONE; // the scheme changed; don't retry.
417        }
418        if (previousUrl.getHost().equals(url.getHost())
419            && getEffectivePort(previousUrl) == getEffectivePort(url)) {
420          return Retry.SAME_CONNECTION;
421        } else {
422          return Retry.DIFFERENT_CONNECTION;
423        }
424
425      default:
426        return Retry.NONE;
427    }
428  }
429
430  final int getDefaultPort() {
431    return defaultPort;
432  }
433
434  /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
435  final int getFixedContentLength() {
436    return fixedContentLength;
437  }
438
439  /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
440  final int getChunkLength() {
441    return chunkLength;
442  }
443
444  final Proxy getProxy() {
445    return proxy;
446  }
447
448  final void setProxy(Proxy proxy) {
449    this.proxy = proxy;
450  }
451
452  @Override public final boolean usingProxy() {
453    return (proxy != null && proxy.type() != Proxy.Type.DIRECT);
454  }
455
456  @Override public String getResponseMessage() throws IOException {
457    return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
458  }
459
460  @Override public final int getResponseCode() throws IOException {
461    return getResponse().getResponseCode();
462  }
463
464  @Override public final void setRequestProperty(String field, String newValue) {
465    if (connected) {
466      throw new IllegalStateException("Cannot set request property after connection is made");
467    }
468    if (field == null) {
469      throw new NullPointerException("field == null");
470    }
471    rawRequestHeaders.set(field, newValue);
472  }
473
474  @Override public final void addRequestProperty(String field, String value) {
475    if (connected) {
476      throw new IllegalStateException("Cannot add request property after connection is made");
477    }
478    if (field == null) {
479      throw new NullPointerException("field == null");
480    }
481    rawRequestHeaders.add(field, value);
482  }
483}
484