336 lines
12 KiB
Java
336 lines
12 KiB
Java
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright ownership.
|
|
* The ASF licenses this file to You 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;
|
|
|
|
import com.squareup.okhttp.internal.Platform;
|
|
import com.squareup.okhttp.internal.http.HttpAuthenticator;
|
|
import com.squareup.okhttp.internal.http.HttpEngine;
|
|
import com.squareup.okhttp.internal.http.HttpTransport;
|
|
import com.squareup.okhttp.internal.http.RawHeaders;
|
|
import com.squareup.okhttp.internal.http.SpdyTransport;
|
|
import com.squareup.okhttp.internal.spdy.SpdyConnection;
|
|
import java.io.BufferedInputStream;
|
|
import java.io.BufferedOutputStream;
|
|
import java.io.Closeable;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.net.Proxy;
|
|
import java.net.Socket;
|
|
import java.net.SocketTimeoutException;
|
|
import java.net.URL;
|
|
import java.util.Arrays;
|
|
import javax.net.ssl.SSLSocket;
|
|
|
|
import static java.net.HttpURLConnection.HTTP_OK;
|
|
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
|
|
|
|
/**
|
|
* Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection,
|
|
* which may be used for multiple HTTP request/response exchanges. Connections
|
|
* may be direct to the origin server or via a proxy.
|
|
*
|
|
* <p>Typically instances of this class are created, connected and exercised
|
|
* automatically by the HTTP client. Applications may use this class to monitor
|
|
* HTTP connections as members of a {@link ConnectionPool connection pool}.
|
|
*
|
|
* <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
|
|
* which isn't so much a connection as a single request/response exchange.
|
|
*
|
|
* <h3>Modern TLS</h3>
|
|
* There are tradeoffs when selecting which options to include when negotiating
|
|
* a secure connection to a remote host. Newer TLS options are quite useful:
|
|
* <ul>
|
|
* <li>Server Name Indication (SNI) enables one IP address to negotiate secure
|
|
* connections for multiple domain names.
|
|
* <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
|
|
* for both HTTP and SPDY transports.
|
|
* </ul>
|
|
* Unfortunately, older HTTPS servers refuse to connect when such options are
|
|
* presented. Rather than avoiding these options entirely, this class allows a
|
|
* connection to be attempted with modern options and then retried without them
|
|
* should the attempt fail.
|
|
*/
|
|
public final class Connection implements Closeable {
|
|
private static final byte[] NPN_PROTOCOLS = new byte[] {
|
|
6, 's', 'p', 'd', 'y', '/', '3',
|
|
8, 'h', 't', 't', 'p', '/', '1', '.', '1'
|
|
};
|
|
private static final byte[] SPDY3 = new byte[] {
|
|
's', 'p', 'd', 'y', '/', '3'
|
|
};
|
|
private static final byte[] HTTP_11 = new byte[] {
|
|
'h', 't', 't', 'p', '/', '1', '.', '1'
|
|
};
|
|
|
|
private final Route route;
|
|
|
|
private Socket socket;
|
|
private InputStream in;
|
|
private OutputStream out;
|
|
private boolean connected = false;
|
|
private SpdyConnection spdyConnection;
|
|
private int httpMinorVersion = 1; // Assume HTTP/1.1
|
|
private long idleStartTimeNs;
|
|
|
|
public Connection(Route route) {
|
|
this.route = route;
|
|
}
|
|
|
|
public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
|
|
throws IOException {
|
|
if (connected) throw new IllegalStateException("already connected");
|
|
|
|
socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket();
|
|
Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
|
|
socket.setSoTimeout(readTimeout);
|
|
in = socket.getInputStream();
|
|
out = socket.getOutputStream();
|
|
|
|
if (route.address.sslSocketFactory != null) {
|
|
upgradeToTls(tunnelRequest);
|
|
} else {
|
|
streamWrapper();
|
|
}
|
|
connected = true;
|
|
}
|
|
|
|
/**
|
|
* Create an {@code SSLSocket} and perform the TLS handshake and certificate
|
|
* validation.
|
|
*/
|
|
private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException {
|
|
Platform platform = Platform.get();
|
|
|
|
// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
|
|
if (requiresTunnel()) {
|
|
makeTunnel(tunnelRequest);
|
|
}
|
|
|
|
// Create the wrapper over connected socket.
|
|
socket = route.address.sslSocketFactory
|
|
.createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
|
|
SSLSocket sslSocket = (SSLSocket) socket;
|
|
if (route.modernTls) {
|
|
platform.enableTlsExtensions(sslSocket, route.address.uriHost);
|
|
} else {
|
|
platform.supportTlsIntolerantServer(sslSocket);
|
|
}
|
|
|
|
boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3");
|
|
if (useNpn) {
|
|
platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
|
|
}
|
|
|
|
// Force handshake. This can throw!
|
|
sslSocket.startHandshake();
|
|
|
|
// Verify that the socket's certificates are acceptable for the target host.
|
|
if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
|
|
throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
|
|
}
|
|
|
|
out = sslSocket.getOutputStream();
|
|
in = sslSocket.getInputStream();
|
|
streamWrapper();
|
|
|
|
byte[] selectedProtocol;
|
|
if (useNpn && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
|
|
if (Arrays.equals(selectedProtocol, SPDY3)) {
|
|
sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
|
|
spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out)
|
|
.build();
|
|
spdyConnection.sendConnectionHeader();
|
|
} else if (!Arrays.equals(selectedProtocol, HTTP_11)) {
|
|
throw new IOException(
|
|
"Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1"));
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Returns true if {@link #connect} has been attempted on this connection. */
|
|
public boolean isConnected() {
|
|
return connected;
|
|
}
|
|
|
|
@Override public void close() throws IOException {
|
|
socket.close();
|
|
}
|
|
|
|
/** Returns the route used by this connection. */
|
|
public Route getRoute() {
|
|
return route;
|
|
}
|
|
|
|
/**
|
|
* Returns the socket that this connection uses, or null if the connection
|
|
* is not currently connected.
|
|
*/
|
|
public Socket getSocket() {
|
|
return socket;
|
|
}
|
|
|
|
/** Returns true if this connection is alive. */
|
|
public boolean isAlive() {
|
|
return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
|
|
}
|
|
|
|
/**
|
|
* Returns true if we are confident that we can read data from this
|
|
* connection. This is more expensive and more accurate than {@link
|
|
* #isAlive()}; callers should check {@link #isAlive()} first.
|
|
*/
|
|
public boolean isReadable() {
|
|
if (!(in instanceof BufferedInputStream)) {
|
|
return true; // Optimistic.
|
|
}
|
|
if (isSpdy()) {
|
|
return true; // Optimistic. We can't test SPDY because its streams are in use.
|
|
}
|
|
BufferedInputStream bufferedInputStream = (BufferedInputStream) in;
|
|
try {
|
|
int readTimeout = socket.getSoTimeout();
|
|
try {
|
|
socket.setSoTimeout(1);
|
|
bufferedInputStream.mark(1);
|
|
if (bufferedInputStream.read() == -1) {
|
|
return false; // Stream is exhausted; socket is closed.
|
|
}
|
|
bufferedInputStream.reset();
|
|
return true;
|
|
} finally {
|
|
socket.setSoTimeout(readTimeout);
|
|
}
|
|
} catch (SocketTimeoutException ignored) {
|
|
return true; // Read timed out; socket is good.
|
|
} catch (IOException e) {
|
|
return false; // Couldn't read; socket is closed.
|
|
}
|
|
}
|
|
|
|
public void resetIdleStartTime() {
|
|
if (spdyConnection != null) {
|
|
throw new IllegalStateException("spdyConnection != null");
|
|
}
|
|
this.idleStartTimeNs = System.nanoTime();
|
|
}
|
|
|
|
/** Returns true if this connection is idle. */
|
|
public boolean isIdle() {
|
|
return spdyConnection == null || spdyConnection.isIdle();
|
|
}
|
|
|
|
/**
|
|
* Returns true if this connection has been idle for longer than
|
|
* {@code keepAliveDurationNs}.
|
|
*/
|
|
public boolean isExpired(long keepAliveDurationNs) {
|
|
return getIdleStartTimeNs() < System.nanoTime() - keepAliveDurationNs;
|
|
}
|
|
|
|
/**
|
|
* Returns the time in ns when this connection became idle. Undefined if
|
|
* this connection is not idle.
|
|
*/
|
|
public long getIdleStartTimeNs() {
|
|
return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs();
|
|
}
|
|
|
|
/** Returns the transport appropriate for this connection. */
|
|
public Object newTransport(HttpEngine httpEngine) throws IOException {
|
|
return (spdyConnection != null)
|
|
? new SpdyTransport(httpEngine, spdyConnection)
|
|
: new HttpTransport(httpEngine, out, in);
|
|
}
|
|
|
|
/**
|
|
* Returns true if this is a SPDY connection. Such connections can be used
|
|
* in multiple HTTP requests simultaneously.
|
|
*/
|
|
public boolean isSpdy() {
|
|
return spdyConnection != null;
|
|
}
|
|
|
|
public SpdyConnection getSpdyConnection() {
|
|
return spdyConnection;
|
|
}
|
|
|
|
/**
|
|
* Returns the minor HTTP version that should be used for future requests on
|
|
* this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default
|
|
* value is 1 for new connections.
|
|
*/
|
|
public int getHttpMinorVersion() {
|
|
return httpMinorVersion;
|
|
}
|
|
|
|
public void setHttpMinorVersion(int httpMinorVersion) {
|
|
this.httpMinorVersion = httpMinorVersion;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the HTTP connection needs to tunnel one protocol over
|
|
* another, such as when using HTTPS through an HTTP proxy. When doing so,
|
|
* we must avoid buffering bytes intended for the higher-level protocol.
|
|
*/
|
|
public boolean requiresTunnel() {
|
|
return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP;
|
|
}
|
|
|
|
public void updateReadTimeout(int newTimeout) throws IOException {
|
|
if (!connected) throw new IllegalStateException("updateReadTimeout - not connected");
|
|
socket.setSoTimeout(newTimeout);
|
|
}
|
|
|
|
/**
|
|
* 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 makeTunnel(TunnelRequest tunnelRequest) throws IOException {
|
|
RawHeaders requestHeaders = tunnelRequest.getRequestHeaders();
|
|
while (true) {
|
|
out.write(requestHeaders.toBytes());
|
|
RawHeaders responseHeaders = RawHeaders.fromBytes(in);
|
|
|
|
switch (responseHeaders.getResponseCode()) {
|
|
case HTTP_OK:
|
|
return;
|
|
case HTTP_PROXY_AUTH:
|
|
requestHeaders = new RawHeaders(requestHeaders);
|
|
URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/");
|
|
boolean credentialsFound = HttpAuthenticator.processAuthHeader(
|
|
route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders,
|
|
route.proxy, url);
|
|
if (credentialsFound) {
|
|
continue;
|
|
} else {
|
|
throw new IOException("Failed to authenticate with proxy");
|
|
}
|
|
default:
|
|
throw new IOException(
|
|
"Unexpected response code for CONNECT: " + responseHeaders.getResponseCode());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void streamWrapper() throws IOException {
|
|
in = new BufferedInputStream(in, 4096);
|
|
out = new BufferedOutputStream(out, 256);
|
|
}
|
|
}
|