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