1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package com.squareup.okhttp; 18 19 import com.squareup.okhttp.internal.Platform; 20 import com.squareup.okhttp.internal.http.HttpAuthenticator; 21 import com.squareup.okhttp.internal.http.HttpEngine; 22 import com.squareup.okhttp.internal.http.HttpTransport; 23 import com.squareup.okhttp.internal.http.RawHeaders; 24 import com.squareup.okhttp.internal.http.SpdyTransport; 25 import com.squareup.okhttp.internal.spdy.SpdyConnection; 26 import java.io.BufferedInputStream; 27 import java.io.BufferedOutputStream; 28 import java.io.Closeable; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.OutputStream; 32 import java.net.Proxy; 33 import java.net.Socket; 34 import java.net.SocketTimeoutException; 35 import java.net.URL; 36 import java.util.Arrays; 37 import javax.net.ssl.SSLSocket; 38 39 import static java.net.HttpURLConnection.HTTP_OK; 40 import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; 41 42 /** 43 * Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection, 44 * which may be used for multiple HTTP request/response exchanges. Connections 45 * may be direct to the origin server or via a proxy. 46 * 47 * <p>Typically instances of this class are created, connected and exercised 48 * automatically by the HTTP client. Applications may use this class to monitor 49 * HTTP connections as members of a {@link ConnectionPool connection pool}. 50 * 51 * <p>Do not confuse this class with the misnamed {@code HttpURLConnection}, 52 * which isn't so much a connection as a single request/response exchange. 53 * 54 * <h3>Modern TLS</h3> 55 * There are tradeoffs when selecting which options to include when negotiating 56 * a secure connection to a remote host. Newer TLS options are quite useful: 57 * <ul> 58 * <li>Server Name Indication (SNI) enables one IP address to negotiate secure 59 * connections for multiple domain names. 60 * <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used 61 * for both HTTP and SPDY transports. 62 * </ul> 63 * Unfortunately, older HTTPS servers refuse to connect when such options are 64 * presented. Rather than avoiding these options entirely, this class allows a 65 * connection to be attempted with modern options and then retried without them 66 * should the attempt fail. 67 */ 68 public final class Connection implements Closeable { 69 private static final byte[] NPN_PROTOCOLS = new byte[] { 70 6, 's', 'p', 'd', 'y', '/', '3', 71 8, 'h', 't', 't', 'p', '/', '1', '.', '1' 72 }; 73 private static final byte[] SPDY3 = new byte[] { 74 's', 'p', 'd', 'y', '/', '3' 75 }; 76 private static final byte[] HTTP_11 = new byte[] { 77 'h', 't', 't', 'p', '/', '1', '.', '1' 78 }; 79 80 private final Route route; 81 82 private Socket socket; 83 private InputStream in; 84 private OutputStream out; 85 private boolean connected = false; 86 private SpdyConnection spdyConnection; 87 private int httpMinorVersion = 1; // Assume HTTP/1.1 88 private long idleStartTimeNs; 89 90 public Connection(Route route) { 91 this.route = route; 92 } 93 94 public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest) 95 throws IOException { 96 if (connected) { 97 throw new IllegalStateException("already connected"); 98 } 99 connected = true; 100 socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket(); 101 Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout); 102 socket.setSoTimeout(readTimeout); 103 in = socket.getInputStream(); 104 out = socket.getOutputStream(); 105 106 if (route.address.sslSocketFactory != null) { 107 upgradeToTls(tunnelRequest); 108 } 109 110 // Use MTU-sized buffers to send fewer packets. 111 int mtu = Platform.get().getMtu(socket); 112 if (mtu < 1024) mtu = 1024; 113 if (mtu > 8192) mtu = 8192; 114 in = new BufferedInputStream(in, mtu); 115 out = new BufferedOutputStream(out, mtu); 116 } 117 118 /** 119 * Create an {@code SSLSocket} and perform the TLS handshake and certificate 120 * validation. 121 */ 122 private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException { 123 Platform platform = Platform.get(); 124 125 // Make an SSL Tunnel on the first message pair of each SSL + proxy connection. 126 if (requiresTunnel()) { 127 makeTunnel(tunnelRequest); 128 } 129 130 // Create the wrapper over connected socket. 131 socket = route.address.sslSocketFactory 132 .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */); 133 SSLSocket sslSocket = (SSLSocket) socket; 134 if (route.modernTls) { 135 platform.enableTlsExtensions(sslSocket, route.address.uriHost); 136 } else { 137 platform.supportTlsIntolerantServer(sslSocket); 138 } 139 140 boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3"); 141 if (useNpn) { 142 platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS); 143 } 144 145 // Force handshake. This can throw! 146 sslSocket.startHandshake(); 147 148 // Verify that the socket's certificates are acceptable for the target host. 149 if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) { 150 throw new IOException("Hostname '" + route.address.uriHost + "' was not verified"); 151 } 152 153 out = sslSocket.getOutputStream(); 154 in = sslSocket.getInputStream(); 155 156 byte[] selectedProtocol; 157 if (useNpn && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) { 158 if (Arrays.equals(selectedProtocol, SPDY3)) { 159 sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream. 160 spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out) 161 .build(); 162 } else if (!Arrays.equals(selectedProtocol, HTTP_11)) { 163 throw new IOException( 164 "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1")); 165 } 166 } 167 } 168 169 /** Returns true if {@link #connect} has been attempted on this connection. */ 170 public boolean isConnected() { 171 return connected; 172 } 173 174 @Override public void close() throws IOException { 175 socket.close(); 176 } 177 178 /** Returns the route used by this connection. */ 179 public Route getRoute() { 180 return route; 181 } 182 183 /** 184 * Returns the socket that this connection uses, or null if the connection 185 * is not currently connected. 186 */ 187 public Socket getSocket() { 188 return socket; 189 } 190 191 /** Returns true if this connection is alive. */ 192 public boolean isAlive() { 193 return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown(); 194 } 195 196 /** 197 * Returns true if we are confident that we can read data from this 198 * connection. This is more expensive and more accurate than {@link 199 * #isAlive()}; callers should check {@link #isAlive()} first. 200 */ 201 public boolean isReadable() { 202 if (!(in instanceof BufferedInputStream)) { 203 return true; // Optimistic. 204 } 205 if (isSpdy()) { 206 return true; // Optimistic. We can't test SPDY because its streams are in use. 207 } 208 BufferedInputStream bufferedInputStream = (BufferedInputStream) in; 209 try { 210 int readTimeout = socket.getSoTimeout(); 211 try { 212 socket.setSoTimeout(1); 213 bufferedInputStream.mark(1); 214 if (bufferedInputStream.read() == -1) { 215 return false; // Stream is exhausted; socket is closed. 216 } 217 bufferedInputStream.reset(); 218 return true; 219 } finally { 220 socket.setSoTimeout(readTimeout); 221 } 222 } catch (SocketTimeoutException ignored) { 223 return true; // Read timed out; socket is good. 224 } catch (IOException e) { 225 return false; // Couldn't read; socket is closed. 226 } 227 } 228 229 public void resetIdleStartTime() { 230 if (spdyConnection != null) { 231 throw new IllegalStateException("spdyConnection != null"); 232 } 233 this.idleStartTimeNs = System.nanoTime(); 234 } 235 236 /** Returns true if this connection is idle. */ 237 public boolean isIdle() { 238 return spdyConnection == null || spdyConnection.isIdle(); 239 } 240 241 /** 242 * Returns true if this connection has been idle for longer than 243 * {@code keepAliveDurationNs}. 244 */ 245 public boolean isExpired(long keepAliveDurationNs) { 246 return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs; 247 } 248 249 /** 250 * Returns the time in ns when this connection became idle. Undefined if 251 * this connection is not idle. 252 */ 253 public long getIdleStartTimeNs() { 254 return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs(); 255 } 256 257 /** Returns the transport appropriate for this connection. */ 258 public Object newTransport(HttpEngine httpEngine) throws IOException { 259 return (spdyConnection != null) ? new SpdyTransport(httpEngine, spdyConnection) 260 : new HttpTransport(httpEngine, out, in); 261 } 262 263 /** 264 * Returns true if this is a SPDY connection. Such connections can be used 265 * in multiple HTTP requests simultaneously. 266 */ 267 public boolean isSpdy() { 268 return spdyConnection != null; 269 } 270 271 public SpdyConnection getSpdyConnection() { 272 return spdyConnection; 273 } 274 275 /** 276 * Returns the minor HTTP version that should be used for future requests on 277 * this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default 278 * value is 1 for new connections. 279 */ 280 public int getHttpMinorVersion() { 281 return httpMinorVersion; 282 } 283 284 public void setHttpMinorVersion(int httpMinorVersion) { 285 this.httpMinorVersion = httpMinorVersion; 286 } 287 288 /** 289 * Returns true if the HTTP connection needs to tunnel one protocol over 290 * another, such as when using HTTPS through an HTTP proxy. When doing so, 291 * we must avoid buffering bytes intended for the higher-level protocol. 292 */ 293 public boolean requiresTunnel() { 294 return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP; 295 } 296 297 public final void updateReadTimeout(int newTimeout) throws IOException { 298 if (!connected) throw new IllegalStateException("updateReadTimeout - not connected"); 299 socket.setSoTimeout(newTimeout); 300 } 301 302 /** 303 * To make an HTTPS connection over an HTTP proxy, send an unencrypted 304 * CONNECT request to create the proxy connection. This may need to be 305 * retried if the proxy requires authorization. 306 */ 307 private void makeTunnel(TunnelRequest tunnelRequest) throws IOException { 308 RawHeaders requestHeaders = tunnelRequest.getRequestHeaders(); 309 while (true) { 310 out.write(requestHeaders.toBytes()); 311 RawHeaders responseHeaders = RawHeaders.fromBytes(in); 312 313 switch (responseHeaders.getResponseCode()) { 314 case HTTP_OK: 315 return; 316 case HTTP_PROXY_AUTH: 317 requestHeaders = new RawHeaders(requestHeaders); 318 URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/"); 319 boolean credentialsFound = HttpAuthenticator.processAuthHeader( 320 route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders, 321 route.proxy, url); 322 if (credentialsFound) { 323 continue; 324 } else { 325 throw new IOException("Failed to authenticate with proxy"); 326 } 327 default: 328 throw new IOException( 329 "Unexpected response code for CONNECT: " + responseHeaders.getResponseCode()); 330 } 331 } 332 } 333 } 334