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