mirror of
https://github.com/square/okhttp.git
synced 2026-01-17 08:42:25 +03:00
Merge pull request #1782 from square/jwilson_0801_fix_cancel
Make call canceling more reliable.
This commit is contained in:
@@ -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<SSLSocket> socketsCreated = new ArrayList<>();
|
||||
|
||||
@@ -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<ConnectionSpec> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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() + ":" + 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user