• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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