| /* |
| * Copyright (C) 2015 Square, Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.squareup.okhttp.internal.http; |
| |
| import com.squareup.okhttp.Address; |
| import com.squareup.okhttp.CertificatePinner; |
| import com.squareup.okhttp.Connection; |
| import com.squareup.okhttp.ConnectionPool; |
| import com.squareup.okhttp.ConnectionSpec; |
| import com.squareup.okhttp.Handshake; |
| import com.squareup.okhttp.Protocol; |
| import com.squareup.okhttp.Request; |
| import com.squareup.okhttp.Response; |
| import com.squareup.okhttp.Route; |
| import com.squareup.okhttp.internal.Platform; |
| import com.squareup.okhttp.internal.ConnectionSpecSelector; |
| import com.squareup.okhttp.internal.Util; |
| import com.squareup.okhttp.internal.tls.OkHostnameVerifier; |
| |
| import java.io.IOException; |
| import java.net.Proxy; |
| import java.net.Socket; |
| import java.net.URL; |
| import java.security.cert.X509Certificate; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| import javax.net.ssl.SSLPeerUnverifiedException; |
| import javax.net.ssl.SSLSocket; |
| import javax.net.ssl.SSLSocketFactory; |
| |
| import okio.Source; |
| |
| import static com.squareup.okhttp.internal.Util.closeQuietly; |
| import static com.squareup.okhttp.internal.Util.getDefaultPort; |
| import static com.squareup.okhttp.internal.Util.getEffectivePort; |
| import static java.net.HttpURLConnection.HTTP_OK; |
| import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; |
| |
| /** |
| * Helper that can establish a socket connection to a {@link com.squareup.okhttp.Route} using the |
| * specified {@link ConnectionSpec} set. A {@link SocketConnector} can be used multiple times. |
| */ |
| public class SocketConnector { |
| private final Connection connection; |
| private final ConnectionPool connectionPool; |
| |
| public SocketConnector(Connection connection, ConnectionPool connectionPool) { |
| this.connection = connection; |
| this.connectionPool = connectionPool; |
| } |
| |
| public ConnectedSocket connectCleartext(int connectTimeout, int readTimeout, Route route) |
| throws RouteException { |
| Socket socket = connectRawSocket(readTimeout, connectTimeout, route); |
| return new ConnectedSocket(route, socket); |
| } |
| |
| public ConnectedSocket connectTls(int connectTimeout, int readTimeout, |
| int writeTimeout, Request request, Route route, List<ConnectionSpec> connectionSpecs, |
| boolean connectionRetryEnabled) throws RouteException { |
| |
| Address address = route.getAddress(); |
| ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs); |
| RouteException routeException = null; |
| do { |
| Socket socket = connectRawSocket(readTimeout, connectTimeout, route); |
| if (route.requiresTunnel()) { |
| createTunnel(readTimeout, writeTimeout, request, route, socket); |
| } |
| |
| SSLSocket sslSocket = null; |
| try { |
| SSLSocketFactory sslSocketFactory = address.getSslSocketFactory(); |
| |
| // Create the wrapper over the connected socket. |
| sslSocket = (SSLSocket) sslSocketFactory |
| .createSocket(socket, address.getUriHost(), address.getUriPort(), true /* autoClose */); |
| |
| // Configure the socket's ciphers, TLS versions, and extensions. |
| ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket); |
| Platform platform = Platform.get(); |
| Handshake handshake = null; |
| Protocol alpnProtocol = null; |
| try { |
| if (connectionSpec.supportsTlsExtensions()) { |
| platform.configureTlsExtensions( |
| sslSocket, address.getUriHost(), address.getProtocols()); |
| } |
| // Force handshake. This can throw! |
| sslSocket.startHandshake(); |
| |
| handshake = Handshake.get(sslSocket.getSession()); |
| |
| String maybeProtocol; |
| if (connectionSpec.supportsTlsExtensions() |
| && (maybeProtocol = platform.getSelectedProtocol(sslSocket)) != null) { |
| alpnProtocol = Protocol.get(maybeProtocol); // Throws IOE on unknown. |
| } |
| } finally { |
| platform.afterHandshake(sslSocket); |
| } |
| |
| // Verify that the socket's certificates are acceptable for the target host. |
| if (!address.getHostnameVerifier().verify(address.getUriHost(), sslSocket.getSession())) { |
| X509Certificate cert = (X509Certificate) sslSocket.getSession() |
| .getPeerCertificates()[0]; |
| throw new SSLPeerUnverifiedException( |
| "Hostname " + address.getUriHost() + " not verified:" |
| + "\n certificate: " + CertificatePinner.pin(cert) |
| + "\n DN: " + cert.getSubjectDN().getName() |
| + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert)); |
| } |
| |
| // Check that the certificate pinner is satisfied by the certificates presented. |
| address.getCertificatePinner().check(address.getUriHost(), handshake.peerCertificates()); |
| |
| return new ConnectedSocket(route, sslSocket, alpnProtocol, handshake); |
| } catch (IOException e) { |
| boolean canRetry = connectionRetryEnabled && connectionSpecSelector.connectionFailed(e); |
| closeQuietly(sslSocket); |
| closeQuietly(socket); |
| if (routeException == null) { |
| routeException = new RouteException(e); |
| } else { |
| routeException.addConnectException(e); |
| } |
| if (!canRetry) { |
| throw routeException; |
| } |
| } |
| } while (true); |
| } |
| |
| private Socket connectRawSocket(int soTimeout, int connectTimeout, Route route) |
| throws RouteException { |
| Platform platform = Platform.get(); |
| try { |
| Proxy proxy = route.getProxy(); |
| Address address = route.getAddress(); |
| Socket socket; |
| if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP) { |
| socket = address.getSocketFactory().createSocket(); |
| } else { |
| socket = new Socket(proxy); |
| } |
| socket.setSoTimeout(soTimeout); |
| platform.connectSocket(socket, route.getSocketAddress(), connectTimeout); |
| |
| return socket; |
| } catch (IOException e) { |
| throw new RouteException(e); |
| } |
| } |
| |
| /** |
| * To make an HTTPS connection over an HTTP proxy, send an unencrypted |
| * CONNECT request to create the proxy connection. This may need to be |
| * retried if the proxy requires authorization. |
| */ |
| private void createTunnel(int readTimeout, int writeTimeout, Request request, Route route, |
| Socket socket) throws RouteException { |
| // Make an SSL Tunnel on the first message pair of each SSL + proxy connection. |
| try { |
| Request tunnelRequest = createTunnelRequest(request); |
| HttpConnection tunnelConnection = new HttpConnection(connectionPool, connection, socket); |
| tunnelConnection.setTimeouts(readTimeout, writeTimeout); |
| URL url = tunnelRequest.url(); |
| String requestLine = "CONNECT " + url.getHost() + ":" + url.getPort() + " HTTP/1.1"; |
| while (true) { |
| tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine); |
| tunnelConnection.flush(); |
| Response response = tunnelConnection.readResponse().request(tunnelRequest).build(); |
| // The response body from a CONNECT should be empty, but if it is not then we should consume |
| // it before proceeding. |
| long contentLength = OkHeaders.contentLength(response); |
| if (contentLength == -1L) { |
| contentLength = 0L; |
| } |
| Source body = tunnelConnection.newFixedLengthSource(contentLength); |
| Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS); |
| body.close(); |
| |
| switch (response.code()) { |
| case HTTP_OK: |
| // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If |
| // that happens, then we will have buffered bytes that are needed by the SSLSocket! |
| // This check is imperfect: it doesn't tell us whether a handshake will succeed, just |
| // that it will almost certainly fail because the proxy has sent unexpected data. |
| if (tunnelConnection.bufferSize() > 0) { |
| throw new IOException("TLS tunnel buffered too many bytes!"); |
| } |
| return; |
| |
| case HTTP_PROXY_AUTH: |
| tunnelRequest = OkHeaders.processAuthHeader( |
| route.getAddress().getAuthenticator(), response, route.getProxy()); |
| if (tunnelRequest != null) continue; |
| throw new IOException("Failed to authenticate with proxy"); |
| |
| default: |
| throw new IOException( |
| "Unexpected response code for CONNECT: " + response.code()); |
| } |
| } |
| } catch (IOException e) { |
| throw new RouteException(e); |
| } |
| } |
| |
| /** |
| * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if |
| * no tunnel is necessary. Everything in the tunnel request is sent |
| * unencrypted to the proxy server, so tunnels include only the minimum set of |
| * headers. This avoids sending potentially sensitive data like HTTP cookies |
| * to the proxy unencrypted. |
| */ |
| private Request createTunnelRequest(Request request) throws IOException { |
| String host = request.url().getHost(); |
| int port = getEffectivePort(request.url()); |
| String authority = (port == getDefaultPort("https")) ? host : (host + ":" + port); |
| Request.Builder result = new Request.Builder() |
| .url(new URL("https", host, port, "/")) |
| .header("Host", authority) |
| .header("Proxy-Connection", "Keep-Alive"); // For HTTP/1.0 proxies like Squid. |
| |
| // Copy over the User-Agent header if it exists. |
| String userAgent = request.header("User-Agent"); |
| if (userAgent != null) { |
| result.header("User-Agent", userAgent); |
| } |
| |
| // Copy over the Proxy-Authorization header if it exists. |
| String proxyAuthorization = request.header("Proxy-Authorization"); |
| if (proxyAuthorization != null) { |
| result.header("Proxy-Authorization", proxyAuthorization); |
| } |
| |
| return result.build(); |
| } |
| |
| /** |
| * A connected socket with metadata. |
| */ |
| public static class ConnectedSocket { |
| public final Route route; |
| public final Socket socket; |
| public final Protocol alpnProtocol; |
| public final Handshake handshake; |
| |
| /** A connected plain / raw (i.e. unencrypted communication) socket. */ |
| public ConnectedSocket(Route route, Socket socket) { |
| this.route = route; |
| this.socket = socket; |
| alpnProtocol = null; |
| handshake = null; |
| } |
| |
| /** A connected {@link SSLSocket}. */ |
| public ConnectedSocket(Route route, SSLSocket socket, Protocol alpnProtocol, |
| Handshake handshake) { |
| this.route = route; |
| this.socket = socket; |
| this.alpnProtocol = alpnProtocol; |
| this.handshake = handshake; |
| } |
| } |
| } |