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
18package libcore.net.http;
19
20import java.io.BufferedInputStream;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.OutputStream;
24import java.net.InetAddress;
25import java.net.InetSocketAddress;
26import java.net.Proxy;
27import java.net.ProxySelector;
28import java.net.Socket;
29import java.net.SocketAddress;
30import java.net.SocketException;
31import java.net.URI;
32import java.net.UnknownHostException;
33import java.util.Arrays;
34import java.util.List;
35import javax.net.ssl.HostnameVerifier;
36import javax.net.ssl.SSLSocket;
37import javax.net.ssl.SSLSocketFactory;
38import libcore.io.IoUtils;
39import libcore.net.spdy.SpdyConnection;
40import libcore.util.Libcore;
41import 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 */
52final 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
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
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
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
176    public void setSoTimeout(int readTimeout) throws SocketException {
177        socket.setSoTimeout(readTimeout);
178    }
179
180    Socket getSocket() {
181        return sslSocket != null ? sslSocket : socket;
182    }
183
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     */
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     */
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     */
249    public boolean isRecycled() {
250        return recycled;
251    }
252
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     */
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     */
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     */
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
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         */
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
343        public Proxy getProxy() {
344            return proxy;
345        }
346
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
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
369        public HttpConnection connect(int connectTimeout) throws IOException {
370            return new HttpConnection(this, connectTimeout);
371        }
372    }
373}
374