Home | History | Annotate | Download | only in http
      1 /*
      2  * Copyright (C) 2015 Square, Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.squareup.okhttp.internal.http;
     17 
     18 import com.squareup.okhttp.Address;
     19 import com.squareup.okhttp.CertificatePinner;
     20 import com.squareup.okhttp.Connection;
     21 import com.squareup.okhttp.ConnectionPool;
     22 import com.squareup.okhttp.ConnectionSpec;
     23 import com.squareup.okhttp.Handshake;
     24 import com.squareup.okhttp.Protocol;
     25 import com.squareup.okhttp.Request;
     26 import com.squareup.okhttp.Response;
     27 import com.squareup.okhttp.Route;
     28 import com.squareup.okhttp.internal.Platform;
     29 import com.squareup.okhttp.internal.ConnectionSpecSelector;
     30 import com.squareup.okhttp.internal.Util;
     31 import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
     32 
     33 import java.io.IOException;
     34 import java.net.Proxy;
     35 import java.net.Socket;
     36 import java.net.URL;
     37 import java.security.cert.X509Certificate;
     38 import java.util.List;
     39 import java.util.concurrent.TimeUnit;
     40 import javax.net.ssl.SSLPeerUnverifiedException;
     41 import javax.net.ssl.SSLSocket;
     42 import javax.net.ssl.SSLSocketFactory;
     43 
     44 import okio.Source;
     45 
     46 import static com.squareup.okhttp.internal.Util.closeQuietly;
     47 import static com.squareup.okhttp.internal.Util.getDefaultPort;
     48 import static com.squareup.okhttp.internal.Util.getEffectivePort;
     49 import static java.net.HttpURLConnection.HTTP_OK;
     50 import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
     51 
     52 /**
     53  * Helper that can establish a socket connection to a {@link com.squareup.okhttp.Route} using the
     54  * specified {@link ConnectionSpec} set. A {@link SocketConnector} can be used multiple times.
     55  */
     56 public class SocketConnector {
     57   private final Connection connection;
     58   private final ConnectionPool connectionPool;
     59 
     60   public SocketConnector(Connection connection, ConnectionPool connectionPool) {
     61     this.connection = connection;
     62     this.connectionPool = connectionPool;
     63   }
     64 
     65   public ConnectedSocket connectCleartext(int connectTimeout, int readTimeout, Route route)
     66       throws RouteException {
     67     Socket socket = connectRawSocket(readTimeout, connectTimeout, route);
     68     return new ConnectedSocket(route, socket);
     69   }
     70 
     71   public ConnectedSocket connectTls(int connectTimeout, int readTimeout,
     72       int writeTimeout, Request request, Route route, List<ConnectionSpec> connectionSpecs,
     73       boolean connectionRetryEnabled) throws RouteException {
     74 
     75     Address address = route.getAddress();
     76     ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
     77     RouteException routeException = null;
     78     do {
     79       Socket socket = connectRawSocket(readTimeout, connectTimeout, route);
     80       if (route.requiresTunnel()) {
     81         createTunnel(readTimeout, writeTimeout, request, route, socket);
     82       }
     83 
     84       SSLSocket sslSocket = null;
     85       try {
     86         SSLSocketFactory sslSocketFactory = address.getSslSocketFactory();
     87 
     88         // Create the wrapper over the connected socket.
     89         sslSocket = (SSLSocket) sslSocketFactory
     90             .createSocket(socket, address.getUriHost(), address.getUriPort(), true /* autoClose */);
     91 
     92         // Configure the socket's ciphers, TLS versions, and extensions.
     93         ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
     94         Platform platform = Platform.get();
     95         Handshake handshake = null;
     96         Protocol alpnProtocol = null;
     97         try {
     98           if (connectionSpec.supportsTlsExtensions()) {
     99             platform.configureTlsExtensions(
    100                 sslSocket, address.getUriHost(), address.getProtocols());
    101           }
    102           // Force handshake. This can throw!
    103           sslSocket.startHandshake();
    104 
    105           handshake = Handshake.get(sslSocket.getSession());
    106 
    107           String maybeProtocol;
    108           if (connectionSpec.supportsTlsExtensions()
    109               && (maybeProtocol = platform.getSelectedProtocol(sslSocket)) != null) {
    110             alpnProtocol = Protocol.get(maybeProtocol); // Throws IOE on unknown.
    111           }
    112         } finally {
    113           platform.afterHandshake(sslSocket);
    114         }
    115 
    116         // Verify that the socket's certificates are acceptable for the target host.
    117         if (!address.getHostnameVerifier().verify(address.getUriHost(), sslSocket.getSession())) {
    118           X509Certificate cert = (X509Certificate) sslSocket.getSession()
    119               .getPeerCertificates()[0];
    120           throw new SSLPeerUnverifiedException(
    121               "Hostname " + address.getUriHost() + " not verified:"
    122               + "\n    certificate: " + CertificatePinner.pin(cert)
    123               + "\n    DN: " + cert.getSubjectDN().getName()
    124               + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
    125         }
    126 
    127         // Check that the certificate pinner is satisfied by the certificates presented.
    128         address.getCertificatePinner().check(address.getUriHost(), handshake.peerCertificates());
    129 
    130         return new ConnectedSocket(route, sslSocket, alpnProtocol, handshake);
    131       } catch (IOException e) {
    132         boolean canRetry = connectionRetryEnabled && connectionSpecSelector.connectionFailed(e);
    133         closeQuietly(sslSocket);
    134         closeQuietly(socket);
    135         if (routeException == null) {
    136           routeException = new RouteException(e);
    137         } else {
    138           routeException.addConnectException(e);
    139         }
    140         if (!canRetry) {
    141           throw routeException;
    142         }
    143       }
    144     } while (true);
    145   }
    146 
    147   private Socket connectRawSocket(int soTimeout, int connectTimeout, Route route)
    148       throws RouteException {
    149     Platform platform = Platform.get();
    150     try {
    151       Proxy proxy = route.getProxy();
    152       Address address = route.getAddress();
    153       Socket socket;
    154       if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP) {
    155         socket = address.getSocketFactory().createSocket();
    156       } else {
    157         socket = new Socket(proxy);
    158       }
    159       socket.setSoTimeout(soTimeout);
    160       platform.connectSocket(socket, route.getSocketAddress(), connectTimeout);
    161 
    162       return socket;
    163     } catch (IOException e) {
    164       throw new RouteException(e);
    165     }
    166   }
    167 
    168   /**
    169    * To make an HTTPS connection over an HTTP proxy, send an unencrypted
    170    * CONNECT request to create the proxy connection. This may need to be
    171    * retried if the proxy requires authorization.
    172    */
    173   private void createTunnel(int readTimeout, int writeTimeout, Request request, Route route,
    174       Socket socket) throws RouteException {
    175     // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
    176     try {
    177       Request tunnelRequest = createTunnelRequest(request);
    178       HttpConnection tunnelConnection = new HttpConnection(connectionPool, connection, socket);
    179       tunnelConnection.setTimeouts(readTimeout, writeTimeout);
    180       URL url = tunnelRequest.url();
    181       String requestLine = "CONNECT " + url.getHost() + ":" + url.getPort() + " HTTP/1.1";
    182       while (true) {
    183         tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
    184         tunnelConnection.flush();
    185         Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
    186         // The response body from a CONNECT should be empty, but if it is not then we should consume
    187         // it before proceeding.
    188         long contentLength = OkHeaders.contentLength(response);
    189         if (contentLength == -1L) {
    190           contentLength = 0L;
    191         }
    192         Source body = tunnelConnection.newFixedLengthSource(contentLength);
    193         Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
    194         body.close();
    195 
    196         switch (response.code()) {
    197           case HTTP_OK:
    198             // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
    199             // that happens, then we will have buffered bytes that are needed by the SSLSocket!
    200             // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
    201             // that it will almost certainly fail because the proxy has sent unexpected data.
    202             if (tunnelConnection.bufferSize() > 0) {
    203               throw new IOException("TLS tunnel buffered too many bytes!");
    204             }
    205             return;
    206 
    207           case HTTP_PROXY_AUTH:
    208             tunnelRequest = OkHeaders.processAuthHeader(
    209                 route.getAddress().getAuthenticator(), response, route.getProxy());
    210             if (tunnelRequest != null) continue;
    211             throw new IOException("Failed to authenticate with proxy");
    212 
    213           default:
    214             throw new IOException(
    215                 "Unexpected response code for CONNECT: " + response.code());
    216         }
    217       }
    218     } catch (IOException e) {
    219       throw new RouteException(e);
    220     }
    221   }
    222 
    223   /**
    224    * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if
    225    * no tunnel is necessary. Everything in the tunnel request is sent
    226    * unencrypted to the proxy server, so tunnels include only the minimum set of
    227    * headers. This avoids sending potentially sensitive data like HTTP cookies
    228    * to the proxy unencrypted.
    229    */
    230   private Request createTunnelRequest(Request request) throws IOException {
    231     String host = request.url().getHost();
    232     int port = getEffectivePort(request.url());
    233     String authority = (port == getDefaultPort("https")) ? host : (host + ":" + port);
    234     Request.Builder result = new Request.Builder()
    235         .url(new URL("https", host, port, "/"))
    236         .header("Host", authority)
    237         .header("Proxy-Connection", "Keep-Alive"); // For HTTP/1.0 proxies like Squid.
    238 
    239     // Copy over the User-Agent header if it exists.
    240     String userAgent = request.header("User-Agent");
    241     if (userAgent != null) {
    242       result.header("User-Agent", userAgent);
    243     }
    244 
    245     // Copy over the Proxy-Authorization header if it exists.
    246     String proxyAuthorization = request.header("Proxy-Authorization");
    247     if (proxyAuthorization != null) {
    248       result.header("Proxy-Authorization", proxyAuthorization);
    249     }
    250 
    251     return result.build();
    252   }
    253 
    254   /**
    255    * A connected socket with metadata.
    256    */
    257   public static class ConnectedSocket {
    258     public final Route route;
    259     public final Socket socket;
    260     public final Protocol alpnProtocol;
    261     public final Handshake handshake;
    262 
    263     /** A connected plain / raw (i.e. unencrypted communication) socket. */
    264     public ConnectedSocket(Route route, Socket socket) {
    265       this.route = route;
    266       this.socket = socket;
    267       alpnProtocol = null;
    268       handshake = null;
    269     }
    270 
    271     /** A connected {@link SSLSocket}. */
    272     public ConnectedSocket(Route route, SSLSocket socket, Protocol alpnProtocol,
    273         Handshake handshake) {
    274       this.route = route;
    275       this.socket = socket;
    276       this.alpnProtocol = alpnProtocol;
    277       this.handshake = handshake;
    278     }
    279   }
    280 }
    281