Home | History | Annotate | Download | only in okhttp
      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 package com.squareup.okhttp;
     18 
     19 import com.squareup.okhttp.internal.Platform;
     20 import com.squareup.okhttp.internal.http.HttpAuthenticator;
     21 import com.squareup.okhttp.internal.http.HttpEngine;
     22 import com.squareup.okhttp.internal.http.HttpTransport;
     23 import com.squareup.okhttp.internal.http.RawHeaders;
     24 import com.squareup.okhttp.internal.http.SpdyTransport;
     25 import com.squareup.okhttp.internal.spdy.SpdyConnection;
     26 import java.io.BufferedInputStream;
     27 import java.io.BufferedOutputStream;
     28 import java.io.Closeable;
     29 import java.io.IOException;
     30 import java.io.InputStream;
     31 import java.io.OutputStream;
     32 import java.net.Proxy;
     33 import java.net.Socket;
     34 import java.net.SocketTimeoutException;
     35 import java.net.URL;
     36 import java.util.Arrays;
     37 import javax.net.ssl.SSLSocket;
     38 
     39 import static java.net.HttpURLConnection.HTTP_OK;
     40 import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
     41 
     42 /**
     43  * Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection,
     44  * which may be used for multiple HTTP request/response exchanges. Connections
     45  * may be direct to the origin server or via a proxy.
     46  *
     47  * <p>Typically instances of this class are created, connected and exercised
     48  * automatically by the HTTP client. Applications may use this class to monitor
     49  * HTTP connections as members of a {@link ConnectionPool connection pool}.
     50  *
     51  * <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
     52  * which isn't so much a connection as a single request/response exchange.
     53  *
     54  * <h3>Modern TLS</h3>
     55  * There are tradeoffs when selecting which options to include when negotiating
     56  * a secure connection to a remote host. Newer TLS options are quite useful:
     57  * <ul>
     58  * <li>Server Name Indication (SNI) enables one IP address to negotiate secure
     59  * connections for multiple domain names.
     60  * <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
     61  * for both HTTP and SPDY transports.
     62  * </ul>
     63  * Unfortunately, older HTTPS servers refuse to connect when such options are
     64  * presented. Rather than avoiding these options entirely, this class allows a
     65  * connection to be attempted with modern options and then retried without them
     66  * should the attempt fail.
     67  */
     68 public final class Connection implements Closeable {
     69   private static final byte[] NPN_PROTOCOLS = new byte[] {
     70       6, 's', 'p', 'd', 'y', '/', '3',
     71       8, 'h', 't', 't', 'p', '/', '1', '.', '1'
     72   };
     73   private static final byte[] SPDY3 = new byte[] {
     74       's', 'p', 'd', 'y', '/', '3'
     75   };
     76   private static final byte[] HTTP_11 = new byte[] {
     77       'h', 't', 't', 'p', '/', '1', '.', '1'
     78   };
     79 
     80   private final Route route;
     81 
     82   private Socket socket;
     83   private InputStream in;
     84   private OutputStream out;
     85   private boolean connected = false;
     86   private SpdyConnection spdyConnection;
     87   private int httpMinorVersion = 1; // Assume HTTP/1.1
     88   private long idleStartTimeNs;
     89 
     90   public Connection(Route route) {
     91     this.route = route;
     92   }
     93 
     94   public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
     95       throws IOException {
     96     if (connected) {
     97       throw new IllegalStateException("already connected");
     98     }
     99     connected = true;
    100     socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
    101     Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
    102     socket.setSoTimeout(readTimeout);
    103     in = socket.getInputStream();
    104     out = socket.getOutputStream();
    105 
    106     if (route.address.sslSocketFactory != null) {
    107       upgradeToTls(tunnelRequest);
    108     }
    109 
    110     // Use MTU-sized buffers to send fewer packets.
    111     int mtu = Platform.get().getMtu(socket);
    112     if (mtu < 1024) mtu = 1024;
    113     if (mtu > 8192) mtu = 8192;
    114     in = new BufferedInputStream(in, mtu);
    115     out = new BufferedOutputStream(out, mtu);
    116   }
    117 
    118   /**
    119    * Create an {@code SSLSocket} and perform the TLS handshake and certificate
    120    * validation.
    121    */
    122   private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException {
    123     Platform platform = Platform.get();
    124 
    125     // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
    126     if (requiresTunnel()) {
    127       makeTunnel(tunnelRequest);
    128     }
    129 
    130     // Create the wrapper over connected socket.
    131     socket = route.address.sslSocketFactory
    132         .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
    133     SSLSocket sslSocket = (SSLSocket) socket;
    134     if (route.modernTls) {
    135       platform.enableTlsExtensions(sslSocket, route.address.uriHost);
    136     } else {
    137       platform.supportTlsIntolerantServer(sslSocket);
    138     }
    139 
    140     boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3");
    141     if (useNpn) {
    142       platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
    143     }
    144 
    145     // Force handshake. This can throw!
    146     sslSocket.startHandshake();
    147 
    148     // Verify that the socket's certificates are acceptable for the target host.
    149     if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
    150       throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
    151     }
    152 
    153     out = sslSocket.getOutputStream();
    154     in = sslSocket.getInputStream();
    155 
    156     byte[] selectedProtocol;
    157     if (useNpn && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
    158       if (Arrays.equals(selectedProtocol, SPDY3)) {
    159         sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
    160         spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out)
    161             .build();
    162       } else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
    163         throw new IOException(
    164             "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
    165       }
    166     }
    167   }
    168 
    169   /** Returns true if {@link #connect} has been attempted on this connection. */
    170   public boolean isConnected() {
    171     return connected;
    172   }
    173 
    174   @Override public void close() throws IOException {
    175     socket.close();
    176   }
    177 
    178   /** Returns the route used by this connection. */
    179   public Route getRoute() {
    180     return route;
    181   }
    182 
    183   /**
    184    * Returns the socket that this connection uses, or null if the connection
    185    * is not currently connected.
    186    */
    187   public Socket getSocket() {
    188     return socket;
    189   }
    190 
    191   /** Returns true if this connection is alive. */
    192   public boolean isAlive() {
    193     return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
    194   }
    195 
    196   /**
    197    * Returns true if we are confident that we can read data from this
    198    * connection. This is more expensive and more accurate than {@link
    199    * #isAlive()}; callers should check {@link #isAlive()} first.
    200    */
    201   public boolean isReadable() {
    202     if (!(in instanceof BufferedInputStream)) {
    203       return true; // Optimistic.
    204     }
    205     if (isSpdy()) {
    206       return true; // Optimistic. We can't test SPDY because its streams are in use.
    207     }
    208     BufferedInputStream bufferedInputStream = (BufferedInputStream) in;
    209     try {
    210       int readTimeout = socket.getSoTimeout();
    211       try {
    212         socket.setSoTimeout(1);
    213         bufferedInputStream.mark(1);
    214         if (bufferedInputStream.read() == -1) {
    215           return false; // Stream is exhausted; socket is closed.
    216         }
    217         bufferedInputStream.reset();
    218         return true;
    219       } finally {
    220         socket.setSoTimeout(readTimeout);
    221       }
    222     } catch (SocketTimeoutException ignored) {
    223       return true; // Read timed out; socket is good.
    224     } catch (IOException e) {
    225       return false; // Couldn't read; socket is closed.
    226     }
    227   }
    228 
    229   public void resetIdleStartTime() {
    230     if (spdyConnection != null) {
    231       throw new IllegalStateException("spdyConnection != null");
    232     }
    233     this.idleStartTimeNs = System.nanoTime();
    234   }
    235 
    236   /** Returns true if this connection is idle. */
    237   public boolean isIdle() {
    238     return spdyConnection == null || spdyConnection.isIdle();
    239   }
    240 
    241   /**
    242    * Returns true if this connection has been idle for longer than
    243    * {@code keepAliveDurationNs}.
    244    */
    245   public boolean isExpired(long keepAliveDurationNs) {
    246     return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs;
    247   }
    248 
    249   /**
    250    * Returns the time in ns when this connection became idle. Undefined if
    251    * this connection is not idle.
    252    */
    253   public long getIdleStartTimeNs() {
    254     return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs();
    255   }
    256 
    257   /** Returns the transport appropriate for this connection. */
    258   public Object newTransport(HttpEngine httpEngine) throws IOException {
    259     return (spdyConnection != null) ? new SpdyTransport(httpEngine, spdyConnection)
    260         : new HttpTransport(httpEngine, out, in);
    261   }
    262 
    263   /**
    264    * Returns true if this is a SPDY connection. Such connections can be used
    265    * in multiple HTTP requests simultaneously.
    266    */
    267   public boolean isSpdy() {
    268     return spdyConnection != null;
    269   }
    270 
    271   public SpdyConnection getSpdyConnection() {
    272     return spdyConnection;
    273   }
    274 
    275   /**
    276    * Returns the minor HTTP version that should be used for future requests on
    277    * this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default
    278    * value is 1 for new connections.
    279    */
    280   public int getHttpMinorVersion() {
    281     return httpMinorVersion;
    282   }
    283 
    284   public void setHttpMinorVersion(int httpMinorVersion) {
    285     this.httpMinorVersion = httpMinorVersion;
    286   }
    287 
    288   /**
    289    * Returns true if the HTTP connection needs to tunnel one protocol over
    290    * another, such as when using HTTPS through an HTTP proxy. When doing so,
    291    * we must avoid buffering bytes intended for the higher-level protocol.
    292    */
    293   public boolean requiresTunnel() {
    294     return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP;
    295   }
    296 
    297   public final void updateReadTimeout(int newTimeout) throws IOException {
    298     if (!connected) throw new IllegalStateException("updateReadTimeout - not connected");
    299     socket.setSoTimeout(newTimeout);
    300   }
    301 
    302   /**
    303    * To make an HTTPS connection over an HTTP proxy, send an unencrypted
    304    * CONNECT request to create the proxy connection. This may need to be
    305    * retried if the proxy requires authorization.
    306    */
    307   private void makeTunnel(TunnelRequest tunnelRequest) throws IOException {
    308     RawHeaders requestHeaders = tunnelRequest.getRequestHeaders();
    309     while (true) {
    310       out.write(requestHeaders.toBytes());
    311       RawHeaders responseHeaders = RawHeaders.fromBytes(in);
    312 
    313       switch (responseHeaders.getResponseCode()) {
    314         case HTTP_OK:
    315           return;
    316         case HTTP_PROXY_AUTH:
    317           requestHeaders = new RawHeaders(requestHeaders);
    318           URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
    319           boolean credentialsFound = HttpAuthenticator.processAuthHeader(
    320               route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
    321               route.proxy, url);
    322           if (credentialsFound) {
    323             continue;
    324           } else {
    325             throw new IOException("Failed to authenticate with proxy");
    326           }
    327         default:
    328           throw new IOException(
    329               "Unexpected response code for CONNECT: " + responseHeaders.getResponseCode());
    330       }
    331     }
    332   }
    333 }
    334