diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java index 593dc14e6..31679eee7 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java @@ -20,6 +20,7 @@ import com.squareup.okhttp.internal.Internal; import com.squareup.okhttp.internal.RecordingOkAuthenticator; import com.squareup.okhttp.internal.SingleInetAddressNetwork; import com.squareup.okhttp.internal.SslContextBuilder; +import com.squareup.okhttp.internal.Util; import com.squareup.okhttp.internal.Version; import com.squareup.okhttp.internal.io.FileSystem; import com.squareup.okhttp.internal.io.InMemoryFileSystem; @@ -36,7 +37,10 @@ import java.io.InterruptedIOException; import java.net.CookieManager; import java.net.HttpCookie; import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.ProtocolException; +import java.net.ServerSocket; import java.net.UnknownServiceException; import java.security.cert.Certificate; import java.util.ArrayList; @@ -52,6 +56,7 @@ import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import javax.net.ServerSocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; @@ -89,19 +94,16 @@ public final class CallTest { private OkHttpClient client = new OkHttpClient(); private RecordingCallback callback = new RecordingCallback(); private TestLogHandler logHandler = new TestLogHandler(); - private Cache cache; + private Cache cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem); + private ServerSocket nullServer; @Before public void setUp() throws Exception { - client = new OkHttpClient(); - callback = new RecordingCallback(); - logHandler = new TestLogHandler(); - - cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem); logger.addHandler(logHandler); } @After public void tearDown() throws Exception { cache.delete(); + Util.closeQuietly(nullServer); logger.removeHandler(logHandler); } @@ -1469,6 +1471,45 @@ public final class CallTest { assertEquals(0, server.getRequestCount()); } + @Test public void cancelDuringHttpConnect() throws Exception { + cancelDuringConnect("http"); + } + + @Test public void cancelDuringHttpsConnect() throws Exception { + cancelDuringConnect("https"); + } + + /** Cancel a call that's waiting for connect to complete. */ + private void cancelDuringConnect(String scheme) throws Exception { + InetSocketAddress socketAddress = startNullServer(); + + HttpUrl url = new HttpUrl.Builder() + .scheme(scheme) + .host(socketAddress.getHostName()) + .port(socketAddress.getPort()) + .build(); + + long cancelDelayMillis = 300L; + Call call = client.newCall(new Request.Builder().url(url).build()); + cancelLater(call, cancelDelayMillis); + + long startNanos = System.nanoTime(); + try { + call.execute(); + fail(); + } catch (IOException expected) { + } + long elapsedNanos = System.nanoTime() - startNanos; + assertEquals(cancelDelayMillis, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 100f); + } + + private InetSocketAddress startNullServer() throws IOException { + InetSocketAddress address = new InetSocketAddress(InetAddress.getByName("localhost"), 0); + nullServer = ServerSocketFactory.getDefault().createServerSocket(); + nullServer.bind(address); + return new InetSocketAddress(address.getAddress(), nullServer.getLocalPort()); + } + @Test public void cancelTagImmediatelyAfterEnqueue() throws Exception { Call call = client.newCall(new Request.Builder() .url(server.url("/a")) @@ -1806,6 +1847,19 @@ public final class CallTest { return result; } + private void cancelLater(final Call call, final long delay) { + new Thread("canceler") { + @Override public void run() { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + throw new AssertionError(); + } + call.cancel(); + } + }.start(); + } + private static class RecordingSSLSocketFactory extends DelegatingSSLSocketFactory { private List socketsCreated = new ArrayList<>(); diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java index d04a311f1..e807ef06f 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java @@ -16,20 +16,38 @@ */ package com.squareup.okhttp; +import com.squareup.okhttp.internal.ConnectionSpecSelector; +import com.squareup.okhttp.internal.Platform; +import com.squareup.okhttp.internal.Util; +import com.squareup.okhttp.internal.framed.FramedConnection; +import com.squareup.okhttp.internal.http.FramedTransport; import com.squareup.okhttp.internal.http.HttpConnection; import com.squareup.okhttp.internal.http.HttpEngine; import com.squareup.okhttp.internal.http.HttpTransport; +import com.squareup.okhttp.internal.http.OkHeaders; import com.squareup.okhttp.internal.http.RouteException; -import com.squareup.okhttp.internal.http.SocketConnector; -import com.squareup.okhttp.internal.http.FramedTransport; import com.squareup.okhttp.internal.http.Transport; -import com.squareup.okhttp.internal.framed.FramedConnection; +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.net.UnknownServiceException; +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.BufferedSink; import okio.BufferedSource; +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; /** * The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be @@ -136,40 +154,200 @@ public final class Connection { List connectionSpecs, boolean connectionRetryEnabled) throws RouteException { if (connected) throw new IllegalStateException("already connected"); - SocketConnector socketConnector = new SocketConnector(this, pool); - SocketConnector.ConnectedSocket connectedSocket; + RouteException routeException = null; + ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs); + Proxy proxy = route.getProxy(); + Address address = route.getAddress(); + + if (route.address.getSslSocketFactory() == null + && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) { + throw new RouteException(new UnknownServiceException( + "CLEARTEXT communication not supported: " + connectionSpecs)); + } + + while (!connected) { + try { + socket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP + ? address.getSocketFactory().createSocket() + : new Socket(proxy); + connectSocket(connectTimeout, readTimeout, writeTimeout, request, + connectionSpecSelector); + connected = true; // Success! + } catch (IOException e) { + Util.closeQuietly(socket); + socket = null; + + if (routeException == null) { + routeException = new RouteException(e); + } else { + routeException.addConnectException(e); + } + + if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) { + throw routeException; + } + } + } + } + + /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */ + private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout, + Request request, ConnectionSpecSelector connectionSpecSelector) throws IOException { + socket.setSoTimeout(readTimeout); + Platform.get().connectSocket(socket, route.getSocketAddress(), connectTimeout); + if (route.address.getSslSocketFactory() != null) { - // https:// communication - connectedSocket = socketConnector.connectTls(connectTimeout, readTimeout, writeTimeout, - request, route, connectionSpecs, connectionRetryEnabled); + connectTls(readTimeout, writeTimeout, request, connectionSpecSelector); + } + + if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) { + socket.setSoTimeout(0); // Framed connection timeouts are set per-stream. + framedConnection = new FramedConnection.Builder(route.address.uriHost, true, socket) + .protocol(protocol).build(); + framedConnection.sendConnectionPreface(); } else { - // http:// communication. - if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) { - throw new RouteException( - new UnknownServiceException( - "CLEARTEXT communication not supported: " + connectionSpecs)); - } - connectedSocket = socketConnector.connectCleartext(connectTimeout, readTimeout, route); + httpConnection = new HttpConnection(pool, this, socket); + } + } + + private void connectTls(int readTimeout, int writeTimeout, Request request, + ConnectionSpecSelector connectionSpecSelector) throws IOException { + if (route.requiresTunnel()) { + createTunnel(readTimeout, writeTimeout, request); } - socket = connectedSocket.socket; - handshake = connectedSocket.handshake; - protocol = connectedSocket.alpnProtocol == null - ? Protocol.HTTP_1_1 : connectedSocket.alpnProtocol; - + Address address = route.getAddress(); + SSLSocketFactory sslSocketFactory = address.getSslSocketFactory(); + boolean success = false; + SSLSocket sslSocket = null; try { - if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) { - socket.setSoTimeout(0); // Framed connection timeouts are set per-stream. - framedConnection = new FramedConnection.Builder(route.address.uriHost, true, socket) - .protocol(protocol).build(); - framedConnection.sendConnectionPreface(); - } else { - httpConnection = new HttpConnection(pool, this, socket); + // 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); + if (connectionSpec.supportsTlsExtensions()) { + Platform.get().configureTlsExtensions( + sslSocket, address.getUriHost(), address.getProtocols()); + } + + // Force handshake. This can throw! + sslSocket.startHandshake(); + Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession()); + + // Verify that the socket's certificates are acceptable for the target host. + if (!address.getHostnameVerifier().verify(address.getUriHost(), sslSocket.getSession())) { + X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(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(), + unverifiedHandshake.peerCertificates()); + + // Success! Save the handshake and the ALPN protocol. + String maybeProtocol = connectionSpec.supportsTlsExtensions() + ? Platform.get().getSelectedProtocol(sslSocket) + : null; + protocol = maybeProtocol != null + ? Protocol.get(maybeProtocol) + : Protocol.HTTP_1_1; + handshake = unverifiedHandshake; + socket = sslSocket; + success = true; + } finally { + if (sslSocket != null) { + Platform.get().afterHandshake(sslSocket); + } + if (!success) { + closeQuietly(sslSocket); } - } catch (IOException e) { - throw new RouteException(e); } - connected = true; + } + + /** + * 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) throws IOException { + // Make an SSL Tunnel on the first message pair of each SSL + proxy connection. + Request tunnelRequest = createTunnelRequest(request); + HttpConnection tunnelConnection = new HttpConnection(pool, this, socket); + tunnelConnection.setTimeouts(readTimeout, writeTimeout); + URL url = tunnelRequest.url(); + String requestLine = "CONNECT " + url.getHost() + ":" + getEffectivePort(url) + " 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()); + } + } + } + + /** + * 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(); } /** diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java index 42522c805..ea6c1527f 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java @@ -327,19 +327,9 @@ public final class HttpEngine { } } - connection = nextConnection(); - route = connection.getRoute(); - } - - /** - * Returns the next connection to attempt. - * - * @throws java.util.NoSuchElementException if there are no more routes to attempt. - */ - private Connection nextConnection() throws RouteException { - Connection connection = createNextConnection(); + connection = createNextConnection(); Internal.instance.connectAndSetOwner(client, connection, this, networkRequest); - return connection; + route = connection.getRoute(); } private Connection createNextConnection() throws RouteException { @@ -572,17 +562,25 @@ public final class HttpEngine { } /** - * Immediately closes the socket connection if it's currently held by this - * engine. Use this to interrupt an in-flight request from any thread. It's - * the caller's responsibility to close the request body and response body - * streams; otherwise resources may be leaked. + * Immediately closes the socket connection if it's currently held by this engine. Use this to + * interrupt an in-flight request from any thread. It's the caller's responsibility to close the + * request body and response body streams; otherwise resources may be leaked. + * + *

This method is safe to be called concurrently, but provides limited guarantees. If a + * transport layer connection has been established (such as a HTTP/2 stream) that is terminated. + * Otherwise if a socket connection is being established, that is terminated. */ public void disconnect() { - if (transport != null) { - try { + try { + if (transport != null) { transport.disconnect(this); - } catch (IOException ignored) { + } else { + Connection connection = this.connection; + if (connection != null) { + Internal.instance.closeIfOwnedBy(connection, this); + } } + } catch (IOException ignored) { } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SocketConnector.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SocketConnector.java deleted file mode 100644 index c1159bc19..000000000 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SocketConnector.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * 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 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() + ":" + getEffectivePort(url) + " 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; - } - } -}