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