/* * 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 libcore.net.http; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.net.URI; import java.net.UnknownHostException; import java.util.List; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import libcore.io.IoUtils; import libcore.util.Objects; import org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl; /** * Holds the sockets and streams of an HTTP or HTTPS connection, which may be * used for multiple HTTP request/response exchanges. Connections may be direct * to the origin server or via a proxy. Create an instance using the {@link * Address} inner class. * * <p>Do not confuse this class with the misnamed {@code HttpURLConnection}, * which isn't so much a connection as a single request/response pair. */ final class HttpConnection { private final Address address; private final Socket socket; private InputStream inputStream; private OutputStream outputStream; private SSLSocket unverifiedSocket; private SSLSocket sslSocket; private InputStream sslInputStream; private OutputStream sslOutputStream; private boolean recycled = false; private HttpConnection(Address config, int connectTimeout) throws IOException { this.address = config; /* * Try each of the host's addresses for best behavior in mixed IPv4/IPv6 * environments. See http://b/2876927 * TODO: add a hidden method so that Socket.tryAllAddresses can does this for us */ Socket socketCandidate = null; InetAddress[] addresses = InetAddress.getAllByName(config.socketHost); for (int i = 0; i < addresses.length; i++) { socketCandidate = (config.proxy != null && config.proxy.type() != Proxy.Type.HTTP) ? new Socket(config.proxy) : new Socket(); try { socketCandidate.connect( new InetSocketAddress(addresses[i], config.socketPort), connectTimeout); break; } catch (IOException e) { if (i == addresses.length - 1) { throw e; } } } this.socket = socketCandidate; } public static HttpConnection connect(URI uri, SSLSocketFactory sslSocketFactory, Proxy proxy, boolean requiresTunnel, int connectTimeout) throws IOException { /* * Try an explicitly-specified proxy. */ if (proxy != null) { Address address = (proxy.type() == Proxy.Type.DIRECT) ? new Address(uri, sslSocketFactory) : new Address(uri, sslSocketFactory, proxy, requiresTunnel); return HttpConnectionPool.INSTANCE.get(address, connectTimeout); } /* * Try connecting to each of the proxies provided by the ProxySelector * until a connection succeeds. */ ProxySelector selector = ProxySelector.getDefault(); List<Proxy> proxyList = selector.select(uri); if (proxyList != null) { for (Proxy selectedProxy : proxyList) { if (selectedProxy.type() == Proxy.Type.DIRECT) { // the same as NO_PROXY // TODO: if the selector recommends a direct connection, attempt that? continue; } try { Address address = new Address(uri, sslSocketFactory, selectedProxy, requiresTunnel); return HttpConnectionPool.INSTANCE.get(address, connectTimeout); } catch (IOException e) { // failed to connect, tell it to the selector selector.connectFailed(uri, selectedProxy.address(), e); } } } /* * Try a direct connection. If this fails, this method will throw. */ return HttpConnectionPool.INSTANCE.get(new Address(uri, sslSocketFactory), connectTimeout); } public void closeSocketAndStreams() { IoUtils.closeQuietly(sslOutputStream); IoUtils.closeQuietly(sslInputStream); IoUtils.closeQuietly(sslSocket); IoUtils.closeQuietly(outputStream); IoUtils.closeQuietly(inputStream); IoUtils.closeQuietly(socket); } public void setSoTimeout(int readTimeout) throws SocketException { socket.setSoTimeout(readTimeout); } public OutputStream getOutputStream() throws IOException { if (sslSocket != null) { if (sslOutputStream == null) { sslOutputStream = sslSocket.getOutputStream(); } return sslOutputStream; } else if(outputStream == null) { outputStream = socket.getOutputStream(); } return outputStream; } public InputStream getInputStream() throws IOException { if (sslSocket != null) { if (sslInputStream == null) { sslInputStream = sslSocket.getInputStream(); } return sslInputStream; } else if (inputStream == null) { /* * Buffer the socket stream to permit efficient parsing of HTTP * headers and chunk sizes. Benchmarks suggest 128 is sufficient. * We cannot buffer when setting up a tunnel because we may consume * bytes intended for the SSL socket. */ int bufferSize = 128; inputStream = address.requiresTunnel ? socket.getInputStream() : new BufferedInputStream(socket.getInputStream(), bufferSize); } return inputStream; } protected Socket getSocket() { return sslSocket != null ? sslSocket : socket; } public Address getAddress() { return address; } /** * Create an {@code SSLSocket} and perform the SSL handshake * (performing certificate validation. * * @param sslSocketFactory Source of new {@code SSLSocket} instances. * @param tlsTolerant If true, assume server can handle common * TLS extensions and SSL deflate compression. If false, use * an SSL3 only fallback mode without compression. */ public void setupSecureSocket(SSLSocketFactory sslSocketFactory, boolean tlsTolerant) throws IOException { // create the wrapper over connected socket unverifiedSocket = (SSLSocket) sslSocketFactory.createSocket(socket, address.uriHost, address.uriPort, true /* autoClose */); // tlsTolerant mimics Chrome's behavior if (tlsTolerant && unverifiedSocket instanceof OpenSSLSocketImpl) { OpenSSLSocketImpl openSslSocket = (OpenSSLSocketImpl) unverifiedSocket; openSslSocket.setUseSessionTickets(true); openSslSocket.setHostname(address.uriHost); // use SSLSocketFactory default enabled protocols } else { unverifiedSocket.setEnabledProtocols(new String [] { "SSLv3" }); } // force handshake, which can throw unverifiedSocket.startHandshake(); } /** * Return an {@code SSLSocket} that is not only connected but has * also passed hostname verification. * * @param hostnameVerifier Used to verify the hostname we * connected to is an acceptable match for the peer certificate * chain of the SSLSession. */ public SSLSocket verifySecureSocketHostname(HostnameVerifier hostnameVerifier) throws IOException { if (!hostnameVerifier.verify(address.uriHost, unverifiedSocket.getSession())) { throw new IOException("Hostname '" + address.uriHost + "' was not verified"); } sslSocket = unverifiedSocket; return sslSocket; } /** * Return an {@code SSLSocket} if already connected, otherwise null. */ public SSLSocket getSecureSocketIfConnected() { return sslSocket; } /** * Returns true if this connection has been used to satisfy an earlier * HTTP request/response pair. */ public boolean isRecycled() { return recycled; } public void setRecycled() { this.recycled = true; } /** * Returns true if this connection is eligible to be reused for another * request/response pair. */ protected boolean isEligibleForRecycling() { return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown(); } /** * This address has two parts: the address we connect to directly and the * origin address of the resource. These are the same unless a proxy is * being used. It also includes the SSL socket factory so that a socket will * not be reused if its SSL configuration is different. */ public static final class Address { private final Proxy proxy; private final boolean requiresTunnel; private final String uriHost; private final int uriPort; private final String socketHost; private final int socketPort; private final SSLSocketFactory sslSocketFactory; public Address(URI uri, SSLSocketFactory sslSocketFactory) throws UnknownHostException { this.proxy = null; this.requiresTunnel = false; this.uriHost = uri.getHost(); this.uriPort = uri.getEffectivePort(); this.sslSocketFactory = sslSocketFactory; this.socketHost = uriHost; this.socketPort = uriPort; if (uriHost == null) { throw new UnknownHostException(uri.toString()); } } /** * @param requiresTunnel 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 Address(URI uri, SSLSocketFactory sslSocketFactory, Proxy proxy, boolean requiresTunnel) throws UnknownHostException { this.proxy = proxy; this.requiresTunnel = requiresTunnel; this.uriHost = uri.getHost(); this.uriPort = uri.getEffectivePort(); this.sslSocketFactory = sslSocketFactory; SocketAddress proxyAddress = proxy.address(); if (!(proxyAddress instanceof InetSocketAddress)) { throw new IllegalArgumentException("Proxy.address() is not an InetSocketAddress: " + proxyAddress.getClass()); } InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress; this.socketHost = proxySocketAddress.getHostName(); this.socketPort = proxySocketAddress.getPort(); if (uriHost == null) { throw new UnknownHostException(uri.toString()); } } public Proxy getProxy() { return proxy; } @Override public boolean equals(Object other) { if (other instanceof Address) { Address that = (Address) other; return Objects.equal(this.proxy, that.proxy) && this.uriHost.equals(that.uriHost) && this.uriPort == that.uriPort && Objects.equal(this.sslSocketFactory, that.sslSocketFactory) && this.requiresTunnel == that.requiresTunnel; } return false; } @Override public int hashCode() { int result = 17; result = 31 * result + uriHost.hashCode(); result = 31 * result + uriPort; result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0); result = 31 * result + (proxy != null ? proxy.hashCode() : 0); result = 31 * result + (requiresTunnel ? 1 : 0); return result; } public HttpConnection connect(int connectTimeout) throws IOException { return new HttpConnection(this, connectTimeout); } } }