HttpURLConnectionImpl.java revision 71b9f47b26fb57ac3e436a19519c6e3ec70e86eb
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.huc;
19
20import com.squareup.okhttp.Connection;
21import com.squareup.okhttp.Handshake;
22import com.squareup.okhttp.Headers;
23import com.squareup.okhttp.HttpUrl;
24import com.squareup.okhttp.OkHttpClient;
25import com.squareup.okhttp.Protocol;
26import com.squareup.okhttp.Request;
27import com.squareup.okhttp.RequestBody;
28import com.squareup.okhttp.Response;
29import com.squareup.okhttp.Route;
30import com.squareup.okhttp.internal.Internal;
31import com.squareup.okhttp.internal.Platform;
32import com.squareup.okhttp.internal.Util;
33import com.squareup.okhttp.internal.Version;
34import com.squareup.okhttp.internal.http.HttpDate;
35import com.squareup.okhttp.internal.http.HttpEngine;
36import com.squareup.okhttp.internal.http.HttpMethod;
37import com.squareup.okhttp.internal.http.OkHeaders;
38import com.squareup.okhttp.internal.http.RequestException;
39import com.squareup.okhttp.internal.http.RetryableSink;
40import com.squareup.okhttp.internal.http.RouteException;
41import com.squareup.okhttp.internal.http.StatusLine;
42import java.io.FileNotFoundException;
43import java.io.IOException;
44import java.io.InputStream;
45import java.io.OutputStream;
46import java.net.HttpRetryException;
47import java.net.HttpURLConnection;
48import java.net.InetSocketAddress;
49import java.net.MalformedURLException;
50import java.net.ProtocolException;
51import java.net.Proxy;
52import java.net.SocketPermission;
53import java.net.URL;
54import java.net.UnknownHostException;
55import java.security.Permission;
56import java.util.ArrayList;
57import java.util.Arrays;
58import java.util.Collections;
59import java.util.Date;
60import java.util.LinkedHashSet;
61import java.util.List;
62import java.util.Map;
63import java.util.Set;
64import java.util.concurrent.TimeUnit;
65import okio.BufferedSink;
66import okio.Sink;
67
68/**
69 * This implementation uses HttpEngine to send requests and receive responses.
70 * This class may use multiple HttpEngines to follow redirects, authentication
71 * retries, etc. to retrieve the final response body.
72 *
73 * <h3>What does 'connected' mean?</h3>
74 * This class inherits a {@code connected} field from the superclass. That field
75 * is <strong>not</strong> used to indicate not whether this URLConnection is
76 * currently connected. Instead, it indicates whether a connection has ever been
77 * attempted. Once a connection has been attempted, certain properties (request
78 * header fields, request method, etc.) are immutable.
79 */
80public class HttpURLConnectionImpl extends HttpURLConnection {
81  private static final Set<String> METHODS = new LinkedHashSet<>(
82      Arrays.asList("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH"));
83  private static final RequestBody EMPTY_REQUEST_BODY = RequestBody.create(null, new byte[0]);
84
85  final OkHttpClient client;
86
87  private Headers.Builder requestHeaders = new Headers.Builder();
88
89  /** Like the superclass field of the same name, but a long and available on all platforms. */
90  private long fixedContentLength = -1;
91  private int followUpCount;
92  protected IOException httpEngineFailure;
93  protected HttpEngine httpEngine;
94  /** Lazily created (with synthetic headers) on first call to getHeaders(). */
95  private Headers responseHeaders;
96
97  /**
98   * The most recently attempted route. This will be null if we haven't sent a
99   * request yet, or if the response comes from a cache.
100   */
101  private Route route;
102
103  /**
104   * The most recently received TLS handshake. This will be null if we haven't
105   * connected yet, or if the most recent connection was HTTP (and not HTTPS).
106   */
107  Handshake handshake;
108
109  public HttpURLConnectionImpl(URL url, OkHttpClient client) {
110    super(url);
111    this.client = client;
112  }
113
114  @Override public final void connect() throws IOException {
115    initHttpEngine();
116    boolean success;
117    do {
118      success = execute(false);
119    } while (!success);
120  }
121
122  @Override public final void disconnect() {
123    // Calling disconnect() before a connection exists should have no effect.
124    if (httpEngine == null) return;
125
126    httpEngine.disconnect();
127
128    // This doesn't close the stream because doing so would require all stream
129    // access to be synchronized. It's expected that the thread using the
130    // connection will close its streams directly. If it doesn't, the worst
131    // case is that the GzipSource's Inflater won't be released until it's
132    // finalized. (This logs a warning on Android.)
133  }
134
135  /**
136   * Returns an input stream from the server in the case of error such as the
137   * requested file (txt, htm, html) is not found on the remote server.
138   */
139  @Override public final InputStream getErrorStream() {
140    try {
141      HttpEngine response = getResponse();
142      if (HttpEngine.hasBody(response.getResponse())
143          && response.getResponse().code() >= HTTP_BAD_REQUEST) {
144        return response.getResponse().body().byteStream();
145      }
146      return null;
147    } catch (IOException e) {
148      return null;
149    }
150  }
151
152  private Headers getHeaders() throws IOException {
153    if (responseHeaders == null) {
154      Response response = getResponse().getResponse();
155      Headers headers = response.headers();
156
157      responseHeaders = headers.newBuilder()
158          .add(Platform.get().getPrefix() + "-Response-Source", responseSourceHeader(response))
159          .build();
160    }
161    return responseHeaders;
162  }
163
164  private static String responseSourceHeader(Response response) {
165    if (response.networkResponse() == null) {
166      if (response.cacheResponse() == null) {
167        return "NONE";
168      }
169      return "CACHE " + response.code();
170    }
171    if (response.cacheResponse() == null) {
172      return "NETWORK " + response.code();
173    }
174    return "CONDITIONAL_CACHE " + response.networkResponse().code();
175  }
176
177  /**
178   * Returns the value of the field at {@code position}. Returns null if there
179   * are fewer than {@code position} headers.
180   */
181  @Override public final String getHeaderField(int position) {
182    try {
183      return getHeaders().value(position);
184    } catch (IOException e) {
185      return null;
186    }
187  }
188
189  /**
190   * Returns the value of the field corresponding to the {@code fieldName}, or
191   * null if there is no such field. If the field has multiple values, the
192   * last value is returned.
193   */
194  @Override public final String getHeaderField(String fieldName) {
195    try {
196      return fieldName == null
197          ? StatusLine.get(getResponse().getResponse()).toString()
198          : getHeaders().get(fieldName);
199    } catch (IOException e) {
200      return null;
201    }
202  }
203
204  @Override public final String getHeaderFieldKey(int position) {
205    try {
206      return getHeaders().name(position);
207    } catch (IOException e) {
208      return null;
209    }
210  }
211
212  @Override public final Map<String, List<String>> getHeaderFields() {
213    try {
214      return OkHeaders.toMultimap(getHeaders(),
215          StatusLine.get(getResponse().getResponse()).toString());
216    } catch (IOException e) {
217      return Collections.emptyMap();
218    }
219  }
220
221  @Override public final Map<String, List<String>> getRequestProperties() {
222    if (connected) {
223      throw new IllegalStateException(
224          "Cannot access request header fields after connection is set");
225    }
226
227    return OkHeaders.toMultimap(requestHeaders.build(), null);
228  }
229
230  @Override public final InputStream getInputStream() throws IOException {
231    if (!doInput) {
232      throw new ProtocolException("This protocol does not support input");
233    }
234
235    HttpEngine response = getResponse();
236
237    // if the requested file does not exist, throw an exception formerly the
238    // Error page from the server was returned if the requested file was
239    // text/html this has changed to return FileNotFoundException for all
240    // file types
241    if (getResponseCode() >= HTTP_BAD_REQUEST) {
242      throw new FileNotFoundException(url.toString());
243    }
244
245    return response.getResponse().body().byteStream();
246  }
247
248  @Override public final OutputStream getOutputStream() throws IOException {
249    connect();
250
251    BufferedSink sink = httpEngine.getBufferedRequestBody();
252    if (sink == null) {
253      throw new ProtocolException("method does not support a request body: " + method);
254    } else if (httpEngine.hasResponse()) {
255      throw new ProtocolException("cannot write request body after response has been read");
256    }
257
258    return sink.outputStream();
259  }
260
261  @Override public final Permission getPermission() throws IOException {
262    URL url = getURL();
263    String hostName = url.getHost();
264    int hostPort = url.getPort() != -1
265        ? url.getPort()
266        : HttpUrl.defaultPort(url.getProtocol());
267    if (usingProxy()) {
268      InetSocketAddress proxyAddress = (InetSocketAddress) client.getProxy().address();
269      hostName = proxyAddress.getHostName();
270      hostPort = proxyAddress.getPort();
271    }
272    return new SocketPermission(hostName + ":" + hostPort, "connect, resolve");
273  }
274
275  @Override public final String getRequestProperty(String field) {
276    if (field == null) return null;
277    return requestHeaders.get(field);
278  }
279
280  @Override public void setConnectTimeout(int timeoutMillis) {
281    client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
282  }
283
284  @Override
285  public void setInstanceFollowRedirects(boolean followRedirects) {
286    client.setFollowRedirects(followRedirects);
287  }
288
289  @Override public int getConnectTimeout() {
290    return client.getConnectTimeout();
291  }
292
293  @Override public void setReadTimeout(int timeoutMillis) {
294    client.setReadTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
295  }
296
297  @Override public int getReadTimeout() {
298    return client.getReadTimeout();
299  }
300
301  private void initHttpEngine() throws IOException {
302    if (httpEngineFailure != null) {
303      throw httpEngineFailure;
304    } else if (httpEngine != null) {
305      return;
306    }
307
308    connected = true;
309    try {
310      if (doOutput) {
311        if (method.equals("GET")) {
312          // they are requesting a stream to write to. This implies a POST method
313          method = "POST";
314        } else if (!HttpMethod.permitsRequestBody(method)) {
315          throw new ProtocolException(method + " does not support writing");
316        }
317      }
318      // If the user set content length to zero, we know there will not be a request body.
319      httpEngine = newHttpEngine(method, null, null, null);
320    } catch (IOException e) {
321      httpEngineFailure = e;
322      throw e;
323    }
324  }
325
326  private HttpEngine newHttpEngine(String method, Connection connection, RetryableSink requestBody,
327      Response priorResponse) throws MalformedURLException, UnknownHostException {
328    // OkHttp's Call API requires a placeholder body; the real body will be streamed separately.
329    RequestBody placeholderBody = HttpMethod.requiresRequestBody(method)
330        ? EMPTY_REQUEST_BODY
331        : null;
332    URL url = getURL();
333    HttpUrl httpUrl = Internal.instance.getHttpUrlChecked(url.toString());
334    Request.Builder builder = new Request.Builder()
335        .url(httpUrl)
336        .method(method, placeholderBody);
337    Headers headers = requestHeaders.build();
338    for (int i = 0, size = headers.size(); i < size; i++) {
339      builder.addHeader(headers.name(i), headers.value(i));
340    }
341
342    boolean bufferRequestBody = false;
343    if (HttpMethod.permitsRequestBody(method)) {
344      // Specify how the request body is terminated.
345      if (fixedContentLength != -1) {
346        builder.header("Content-Length", Long.toString(fixedContentLength));
347      } else if (chunkLength > 0) {
348        builder.header("Transfer-Encoding", "chunked");
349      } else {
350        bufferRequestBody = true;
351      }
352
353      // Add a content type for the request body, if one isn't already present.
354      if (headers.get("Content-Type") == null) {
355        builder.header("Content-Type", "application/x-www-form-urlencoded");
356      }
357    }
358
359    if (headers.get("User-Agent") == null) {
360      builder.header("User-Agent", defaultUserAgent());
361    }
362
363    Request request = builder.build();
364
365    // If we're currently not using caches, make sure the engine's client doesn't have one.
366    OkHttpClient engineClient = client;
367    if (Internal.instance.internalCache(engineClient) != null && !getUseCaches()) {
368      engineClient = client.clone().setCache(null);
369    }
370
371    return new HttpEngine(engineClient, request, bufferRequestBody, true, false, connection, null,
372        requestBody, priorResponse);
373  }
374
375  private String defaultUserAgent() {
376    String agent = System.getProperty("http.agent");
377    return agent != null ? Util.toHumanReadableAscii(agent) : Version.userAgent();
378  }
379
380  /**
381   * Aggressively tries to get the final HTTP response, potentially making
382   * many HTTP requests in the process in order to cope with redirects and
383   * authentication.
384   */
385  private HttpEngine getResponse() throws IOException {
386    initHttpEngine();
387
388    if (httpEngine.hasResponse()) {
389      return httpEngine;
390    }
391
392    while (true) {
393      if (!execute(true)) {
394        continue;
395      }
396
397      Response response = httpEngine.getResponse();
398      Request followUp = httpEngine.followUpRequest();
399
400      if (followUp == null) {
401        httpEngine.releaseConnection();
402        return httpEngine;
403      }
404
405      if (++followUpCount > HttpEngine.MAX_FOLLOW_UPS) {
406        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
407      }
408
409      // The first request was insufficient. Prepare for another...
410      url = followUp.url();
411      requestHeaders = followUp.headers().newBuilder();
412
413      // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM redirect
414      // should keep the same method, Chrome, Firefox and the RI all issue GETs
415      // when following any redirect.
416      Sink requestBody = httpEngine.getRequestBody();
417      if (!followUp.method().equals(method)) {
418        requestBody = null;
419      }
420
421      if (requestBody != null && !(requestBody instanceof RetryableSink)) {
422        throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
423      }
424
425      if (!httpEngine.sameConnection(followUp.httpUrl())) {
426        httpEngine.releaseConnection();
427      }
428
429      Connection connection = httpEngine.close();
430      httpEngine = newHttpEngine(followUp.method(), connection, (RetryableSink) requestBody,
431          response);
432    }
433  }
434
435  /**
436   * Sends a request and optionally reads a response. Returns true if the
437   * request was successfully executed, and false if the request can be
438   * retried. Throws an exception if the request failed permanently.
439   */
440  private boolean execute(boolean readResponse) throws IOException {
441    try {
442      httpEngine.sendRequest();
443      route = httpEngine.getRoute();
444      handshake = httpEngine.getConnection() != null
445          ? httpEngine.getConnection().getHandshake()
446          : null;
447      if (readResponse) {
448        httpEngine.readResponse();
449      }
450
451      return true;
452    } catch (RequestException e) {
453      // An attempt to interpret a request failed.
454      IOException toThrow = e.getCause();
455      httpEngineFailure = toThrow;
456      throw toThrow;
457    } catch (RouteException e) {
458      // The attempt to connect via a route failed. The request will not have been sent.
459      HttpEngine retryEngine = httpEngine.recover(e);
460      if (retryEngine != null) {
461        httpEngine = retryEngine;
462        return false;
463      }
464
465      // Give up; recovery is not possible.
466      IOException toThrow = e.getLastConnectException();
467      httpEngineFailure = toThrow;
468      throw toThrow;
469    } catch (IOException e) {
470      // An attempt to communicate with a server failed. The request may have been sent.
471      HttpEngine retryEngine = httpEngine.recover(e);
472      if (retryEngine != null) {
473        httpEngine = retryEngine;
474        return false;
475      }
476
477      // Give up; recovery is not possible.
478      httpEngineFailure = e;
479      throw e;
480    }
481  }
482
483  /**
484   * Returns true if either:
485   * <ul>
486   *   <li>A specific proxy was explicitly configured for this connection.
487   *   <li>The response has already been retrieved, and a proxy was {@link
488   *       java.net.ProxySelector selected} in order to get it.
489   * </ul>
490   *
491   * <p><strong>Warning:</strong> This method may return false before attempting
492   * to connect and true afterwards.
493   */
494  @Override public final boolean usingProxy() {
495    Proxy proxy = route != null
496        ? route.getProxy()
497        : client.getProxy();
498    return proxy != null && proxy.type() != Proxy.Type.DIRECT;
499  }
500
501  @Override public String getResponseMessage() throws IOException {
502    return getResponse().getResponse().message();
503  }
504
505  @Override public final int getResponseCode() throws IOException {
506    return getResponse().getResponse().code();
507  }
508
509  @Override public final void setRequestProperty(String field, String newValue) {
510    if (connected) {
511      throw new IllegalStateException("Cannot set request property after connection is made");
512    }
513    if (field == null) {
514      throw new NullPointerException("field == null");
515    }
516    if (newValue == null) {
517      // Silently ignore null header values for backwards compatibility with older
518      // android versions as well as with other URLConnection implementations.
519      //
520      // Some implementations send a malformed HTTP header when faced with
521      // such requests, we respect the spec and ignore the header.
522      Platform.get().logW("Ignoring header " + field + " because its value was null.");
523      return;
524    }
525
526    // TODO: Deprecate use of X-Android-Transports header?
527    if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) {
528      setProtocols(newValue, false /* append */);
529    } else {
530      requestHeaders.set(field, newValue);
531    }
532  }
533
534  @Override public void setIfModifiedSince(long newValue) {
535    super.setIfModifiedSince(newValue);
536    if (ifModifiedSince != 0) {
537      requestHeaders.set("If-Modified-Since", HttpDate.format(new Date(ifModifiedSince)));
538    } else {
539      requestHeaders.removeAll("If-Modified-Since");
540    }
541  }
542
543  @Override public final void addRequestProperty(String field, String value) {
544    if (connected) {
545      throw new IllegalStateException("Cannot add request property after connection is made");
546    }
547    if (field == null) {
548      throw new NullPointerException("field == null");
549    }
550    if (value == null) {
551      // Silently ignore null header values for backwards compatibility with older
552      // android versions as well as with other URLConnection implementations.
553      //
554      // Some implementations send a malformed HTTP header when faced with
555      // such requests, we respect the spec and ignore the header.
556      Platform.get().logW("Ignoring header " + field + " because its value was null.");
557      return;
558    }
559
560    // TODO: Deprecate use of X-Android-Transports header?
561    if ("X-Android-Transports".equals(field) || "X-Android-Protocols".equals(field)) {
562      setProtocols(value, true /* append */);
563    } else {
564      requestHeaders.add(field, value);
565    }
566  }
567
568  /*
569   * Splits and validates a comma-separated string of protocols.
570   * When append == false, we require that the transport list contains "http/1.1".
571   * Throws {@link IllegalStateException} when one of the protocols isn't
572   * defined in {@link Protocol OkHttp's protocol enumeration}.
573   */
574  private void setProtocols(String protocolsString, boolean append) {
575    List<Protocol> protocolsList = new ArrayList<>();
576    if (append) {
577      protocolsList.addAll(client.getProtocols());
578    }
579    for (String protocol : protocolsString.split(",", -1)) {
580      try {
581        protocolsList.add(Protocol.get(protocol));
582      } catch (IOException e) {
583        throw new IllegalStateException(e);
584      }
585    }
586    client.setProtocols(protocolsList);
587  }
588
589  @Override public void setRequestMethod(String method) throws ProtocolException {
590    if (!METHODS.contains(method)) {
591      throw new ProtocolException("Expected one of " + METHODS + " but was " + method);
592    }
593    this.method = method;
594  }
595
596  @Override public void setFixedLengthStreamingMode(int contentLength) {
597    setFixedLengthStreamingMode((long) contentLength);
598  }
599
600  @Override public void setFixedLengthStreamingMode(long contentLength) {
601    if (super.connected) throw new IllegalStateException("Already connected");
602    if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode");
603    if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0");
604    this.fixedContentLength = contentLength;
605    super.fixedContentLength = (int) Math.min(contentLength, Integer.MAX_VALUE);
606  }
607}
608