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 18 package libcore.net.http; 19 20 import java.io.BufferedInputStream; 21 import java.io.IOException; 22 import java.io.InputStream; 23 import java.io.OutputStream; 24 import java.net.InetAddress; 25 import java.net.InetSocketAddress; 26 import java.net.Proxy; 27 import java.net.ProxySelector; 28 import java.net.Socket; 29 import java.net.SocketAddress; 30 import java.net.SocketException; 31 import java.net.URI; 32 import java.net.UnknownHostException; 33 import java.util.Arrays; 34 import java.util.List; 35 import javax.net.ssl.HostnameVerifier; 36 import javax.net.ssl.SSLSocket; 37 import javax.net.ssl.SSLSocketFactory; 38 import libcore.io.IoUtils; 39 import libcore.net.spdy.SpdyConnection; 40 import libcore.util.Libcore; 41 import libcore.util.Objects; 42 43 /** 44 * Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection, 45 * which may be used for multiple HTTP request/response exchanges. Connections 46 * may be direct to the origin server or via a proxy. Create an instance using 47 * the {@link Address} inner class. 48 * 49 * <p>Do not confuse this class with the misnamed {@code HttpURLConnection}, 50 * which isn't so much a connection as a single request/response pair. 51 */ 52 final class HttpConnection { 53 private static final byte[] NPN_PROTOCOLS = new byte[] { 54 6, 's', 'p', 'd', 'y', '/', '2', 55 8, 'h', 't', 't', 'p', '/', '1', '.', '1', 56 }; 57 private static final byte[] SPDY2 = new byte[] { 58 's', 'p', 'd', 'y', '/', '2', 59 }; 60 private static final byte[] HTTP_11 = new byte[] { 61 'h', 't', 't', 'p', '/', '1', '.', '1', 62 }; 63 64 private final Address address; 65 private final Socket socket; 66 private InputStream inputStream; 67 private OutputStream outputStream; 68 private SSLSocket sslSocket; 69 private InputStream sslInputStream; 70 private OutputStream sslOutputStream; 71 private boolean recycled = false; 72 private SpdyConnection spdyConnection; 73 74 /** 75 * The version this client will use. Either 0 for HTTP/1.0, or 1 for 76 * HTTP/1.1. Upon receiving a non-HTTP/1.1 response, this client 77 * automatically sets its version to HTTP/1.0. 78 */ 79 int httpMinorVersion = 1; // Assume HTTP/1.1 80 HttpConnection(Address config, int connectTimeout)81 private HttpConnection(Address config, int connectTimeout) throws IOException { 82 this.address = config; 83 84 /* 85 * Try each of the host's addresses for best behavior in mixed IPv4/IPv6 86 * environments. See http://b/2876927 87 * TODO: add a hidden method so that Socket.tryAllAddresses can does this for us 88 */ 89 Socket socketCandidate = null; 90 InetAddress[] addresses = InetAddress.getAllByName(config.socketHost); 91 for (int i = 0; i < addresses.length; i++) { 92 socketCandidate = (config.proxy != null && config.proxy.type() != Proxy.Type.HTTP) 93 ? new Socket(config.proxy) 94 : new Socket(); 95 try { 96 socketCandidate.connect( 97 new InetSocketAddress(addresses[i], config.socketPort), connectTimeout); 98 break; 99 } catch (IOException e) { 100 if (i == addresses.length - 1) { 101 throw e; 102 } 103 } 104 } 105 106 if (socketCandidate == null) { 107 throw new IOException(); 108 } 109 110 this.socket = socketCandidate; 111 112 /* 113 * Buffer the socket stream to permit efficient parsing of HTTP headers 114 * and chunk sizes. Benchmarks suggest 128 is sufficient. We cannot 115 * buffer when setting up a tunnel because we may consume bytes intended 116 * for the SSL socket. 117 */ 118 int bufferSize = 128; 119 inputStream = address.requiresTunnel 120 ? socket.getInputStream() 121 : new BufferedInputStream(socket.getInputStream(), bufferSize); 122 outputStream = socket.getOutputStream(); 123 } 124 connect(URI uri, SSLSocketFactory sslSocketFactory, Proxy proxy, boolean requiresTunnel, int connectTimeout)125 public static HttpConnection connect(URI uri, SSLSocketFactory sslSocketFactory, 126 Proxy proxy, boolean requiresTunnel, int connectTimeout) throws IOException { 127 /* 128 * Try an explicitly-specified proxy. 129 */ 130 if (proxy != null) { 131 Address address = (proxy.type() == Proxy.Type.DIRECT) 132 ? new Address(uri, sslSocketFactory) 133 : new Address(uri, sslSocketFactory, proxy, requiresTunnel); 134 return HttpConnectionPool.INSTANCE.get(address, connectTimeout); 135 } 136 137 /* 138 * Try connecting to each of the proxies provided by the ProxySelector 139 * until a connection succeeds. 140 */ 141 ProxySelector selector = ProxySelector.getDefault(); 142 List<Proxy> proxyList = selector.select(uri); 143 if (proxyList != null) { 144 for (Proxy selectedProxy : proxyList) { 145 if (selectedProxy.type() == Proxy.Type.DIRECT) { 146 // the same as NO_PROXY 147 // TODO: if the selector recommends a direct connection, attempt that? 148 continue; 149 } 150 try { 151 Address address = new Address(uri, sslSocketFactory, 152 selectedProxy, requiresTunnel); 153 return HttpConnectionPool.INSTANCE.get(address, connectTimeout); 154 } catch (IOException e) { 155 // failed to connect, tell it to the selector 156 selector.connectFailed(uri, selectedProxy.address(), e); 157 } 158 } 159 } 160 161 /* 162 * Try a direct connection. If this fails, this method will throw. 163 */ 164 return HttpConnectionPool.INSTANCE.get(new Address(uri, sslSocketFactory), connectTimeout); 165 } 166 closeSocketAndStreams()167 public void closeSocketAndStreams() { 168 IoUtils.closeQuietly(sslOutputStream); 169 IoUtils.closeQuietly(sslInputStream); 170 IoUtils.closeQuietly(sslSocket); 171 IoUtils.closeQuietly(outputStream); 172 IoUtils.closeQuietly(inputStream); 173 IoUtils.closeQuietly(socket); 174 } 175 setSoTimeout(int readTimeout)176 public void setSoTimeout(int readTimeout) throws SocketException { 177 socket.setSoTimeout(readTimeout); 178 } 179 getSocket()180 Socket getSocket() { 181 return sslSocket != null ? sslSocket : socket; 182 } 183 getAddress()184 public Address getAddress() { 185 return address; 186 } 187 188 /** 189 * Create an {@code SSLSocket} and perform the SSL handshake 190 * (performing certificate validation. 191 * 192 * @param sslSocketFactory Source of new {@code SSLSocket} instances. 193 * @param tlsTolerant If true, assume server can handle common 194 */ setupSecureSocket(SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier, boolean tlsTolerant)195 public SSLSocket setupSecureSocket(SSLSocketFactory sslSocketFactory, 196 HostnameVerifier hostnameVerifier, boolean tlsTolerant) throws IOException { 197 if (spdyConnection != null || sslOutputStream != null || sslInputStream != null) { 198 throw new IllegalStateException(); 199 } 200 201 // Create the wrapper over connected socket. 202 sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, 203 address.uriHost, address.uriPort, true /* autoClose */); 204 Libcore.makeTlsTolerant(sslSocket, address.socketHost, tlsTolerant); 205 206 if (tlsTolerant) { 207 Libcore.setNpnProtocols(sslSocket, NPN_PROTOCOLS); 208 } 209 210 // Force handshake. This can throw! 211 sslSocket.startHandshake(); 212 213 // Verify that the socket's certificates are acceptable for the target host. 214 if (!hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) { 215 throw new IOException("Hostname '" + address.uriHost + "' was not verified"); 216 } 217 218 // SSL success. Prepare to hand out Transport instances. 219 sslOutputStream = sslSocket.getOutputStream(); 220 sslInputStream = sslSocket.getInputStream(); 221 222 byte[] selectedProtocol; 223 if (tlsTolerant 224 && (selectedProtocol = Libcore.getNpnSelectedProtocol(sslSocket)) != null) { 225 if (Arrays.equals(selectedProtocol, SPDY2)) { 226 spdyConnection = new SpdyConnection.Builder( 227 true, sslInputStream, sslOutputStream).build(); 228 HttpConnectionPool.INSTANCE.share(this); 229 } else if (!Arrays.equals(selectedProtocol, HTTP_11)) { 230 throw new IOException("Unexpected NPN transport " 231 + new String(selectedProtocol, "ISO-8859-1")); 232 } 233 } 234 235 return sslSocket; 236 } 237 238 /** 239 * Return an {@code SSLSocket} if already connected, otherwise null. 240 */ getSecureSocketIfConnected()241 public SSLSocket getSecureSocketIfConnected() { 242 return sslSocket; 243 } 244 245 /** 246 * Returns true if this connection has been used to satisfy an earlier 247 * HTTP request/response pair. 248 */ isRecycled()249 public boolean isRecycled() { 250 return recycled; 251 } 252 setRecycled()253 public void setRecycled() { 254 this.recycled = true; 255 } 256 257 /** 258 * Returns true if this connection is eligible to be reused for another 259 * request/response pair. 260 */ isEligibleForRecycling()261 protected boolean isEligibleForRecycling() { 262 return !socket.isClosed() 263 && !socket.isInputShutdown() 264 && !socket.isOutputShutdown(); 265 } 266 267 /** 268 * Returns the transport appropriate for this connection. 269 */ newTransport(HttpEngine httpEngine)270 public Transport newTransport(HttpEngine httpEngine) throws IOException { 271 if (spdyConnection != null) { 272 return new SpdyTransport(httpEngine, spdyConnection); 273 } else if (sslSocket != null) { 274 return new HttpTransport(httpEngine, sslOutputStream, sslInputStream); 275 } else { 276 return new HttpTransport(httpEngine, outputStream, inputStream); 277 } 278 } 279 280 /** 281 * Returns true if this is a SPDY connection. Such connections can be used 282 * in multiple HTTP requests simultaneously. 283 */ isSpdy()284 public boolean isSpdy() { 285 return spdyConnection != null; 286 } 287 288 /** 289 * This address has two parts: the address we connect to directly and the 290 * origin address of the resource. These are the same unless a proxy is 291 * being used. It also includes the SSL socket factory so that a socket will 292 * not be reused if its SSL configuration is different. 293 */ 294 public static final class Address { 295 private final Proxy proxy; 296 private final boolean requiresTunnel; 297 private final String uriHost; 298 private final int uriPort; 299 private final String socketHost; 300 private final int socketPort; 301 private final SSLSocketFactory sslSocketFactory; 302 Address(URI uri, SSLSocketFactory sslSocketFactory)303 public Address(URI uri, SSLSocketFactory sslSocketFactory) throws UnknownHostException { 304 this.proxy = null; 305 this.requiresTunnel = false; 306 this.uriHost = uri.getHost(); 307 this.uriPort = Libcore.getEffectivePort(uri); 308 this.sslSocketFactory = sslSocketFactory; 309 this.socketHost = uriHost; 310 this.socketPort = uriPort; 311 if (uriHost == null) { 312 throw new UnknownHostException(uri.toString()); 313 } 314 } 315 316 /** 317 * @param requiresTunnel true if the HTTP connection needs to tunnel one 318 * protocol over another, such as when using HTTPS through an HTTP 319 * proxy. When doing so, we must avoid buffering bytes intended for 320 * the higher-level protocol. 321 */ Address(URI uri, SSLSocketFactory sslSocketFactory, Proxy proxy, boolean requiresTunnel)322 public Address(URI uri, SSLSocketFactory sslSocketFactory, 323 Proxy proxy, boolean requiresTunnel) throws UnknownHostException { 324 this.proxy = proxy; 325 this.requiresTunnel = requiresTunnel; 326 this.uriHost = uri.getHost(); 327 this.uriPort = Libcore.getEffectivePort(uri); 328 this.sslSocketFactory = sslSocketFactory; 329 330 SocketAddress proxyAddress = proxy.address(); 331 if (!(proxyAddress instanceof InetSocketAddress)) { 332 throw new IllegalArgumentException("Proxy.address() is not an InetSocketAddress: " 333 + proxyAddress.getClass()); 334 } 335 InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress; 336 this.socketHost = proxySocketAddress.getHostName(); 337 this.socketPort = proxySocketAddress.getPort(); 338 if (uriHost == null) { 339 throw new UnknownHostException(uri.toString()); 340 } 341 } 342 getProxy()343 public Proxy getProxy() { 344 return proxy; 345 } 346 equals(Object other)347 @Override public boolean equals(Object other) { 348 if (other instanceof Address) { 349 Address that = (Address) other; 350 return Objects.equal(this.proxy, that.proxy) 351 && this.uriHost.equals(that.uriHost) 352 && this.uriPort == that.uriPort 353 && Objects.equal(this.sslSocketFactory, that.sslSocketFactory) 354 && this.requiresTunnel == that.requiresTunnel; 355 } 356 return false; 357 } 358 hashCode()359 @Override public int hashCode() { 360 int result = 17; 361 result = 31 * result + uriHost.hashCode(); 362 result = 31 * result + uriPort; 363 result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0); 364 result = 31 * result + (proxy != null ? proxy.hashCode() : 0); 365 result = 31 * result + (requiresTunnel ? 1 : 0); 366 return result; 367 } 368 connect(int connectTimeout)369 public HttpConnection connect(int connectTimeout) throws IOException { 370 return new HttpConnection(this, connectTimeout); 371 } 372 } 373 } 374