View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.impl.io;
29  
30  import java.net.ConnectException;
31  import java.net.InetAddress;
32  import java.net.InetSocketAddress;
33  import java.net.Socket;
34  import java.net.SocketTimeoutException;
35  import java.util.Arrays;
36  import java.util.Collections;
37  import java.util.List;
38  import java.util.concurrent.TimeUnit;
39  
40  import javax.net.ssl.SSLSocket;
41  
42  import org.apache.hc.client5.http.ConnectTimeoutException;
43  import org.apache.hc.client5.http.DnsResolver;
44  import org.apache.hc.client5.http.HttpHostConnectException;
45  import org.apache.hc.client5.http.SchemePortResolver;
46  import org.apache.hc.client5.http.UnsupportedSchemeException;
47  import org.apache.hc.client5.http.config.TlsConfig;
48  import org.apache.hc.client5.http.io.DetachedSocketFactory;
49  import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
50  import org.apache.hc.client5.http.protocol.HttpClientContext;
51  import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
52  import org.apache.hc.core5.http.HttpHost;
53  import org.apache.hc.core5.http.config.Lookup;
54  import org.apache.hc.core5.http.io.SocketConfig;
55  import org.apache.hc.core5.http2.HttpVersionPolicy;
56  import org.apache.hc.core5.util.TimeValue;
57  import org.apache.hc.core5.util.Timeout;
58  import org.junit.jupiter.api.Assertions;
59  import org.junit.jupiter.api.BeforeEach;
60  import org.junit.jupiter.api.Test;
61  import org.mockito.Mockito;
62  
63  class TestHttpClientConnectionOperator {
64  
65      private ManagedHttpClientConnection conn;
66      private Socket socket;
67      private DetachedSocketFactory detachedSocketFactory;
68      private TlsSocketStrategy tlsSocketStrategy;
69      private Lookup<TlsSocketStrategy> tlsSocketStrategyLookup;
70      private SchemePortResolver schemePortResolver;
71      private DnsResolver dnsResolver;
72      private DefaultHttpClientConnectionOperator connectionOperator;
73  
74      @BeforeEach
75      void setup() {
76          conn = Mockito.mock(ManagedHttpClientConnection.class);
77          socket = Mockito.mock(Socket.class);
78          detachedSocketFactory = Mockito.mock(DetachedSocketFactory.class);
79          tlsSocketStrategy = Mockito.mock(TlsSocketStrategy.class);
80          tlsSocketStrategyLookup = Mockito.mock(Lookup.class);
81          schemePortResolver = Mockito.mock(SchemePortResolver.class);
82          dnsResolver = Mockito.mock(DnsResolver.class);
83          connectionOperator = new DefaultHttpClientConnectionOperator(
84                  detachedSocketFactory, schemePortResolver, dnsResolver, tlsSocketStrategyLookup);
85      }
86  
87      @Test
88      void testConnect() throws Exception {
89          final HttpClientContext context = HttpClientContext.create();
90          final HttpHost host = new HttpHost("somehost");
91          final InetAddress local = InetAddress.getByAddress(new byte[] {127, 0, 0, 0});
92          final InetAddress ip1 = InetAddress.getByAddress(new byte[] {127, 0, 0, 1});
93          final InetAddress ip2 = InetAddress.getByAddress(new byte[] {127, 0, 0, 2});
94          final int port = 80;
95          final List<InetSocketAddress> resolvedAddresses = Arrays.asList(
96                  new InetSocketAddress(ip1, port),
97                  new InetSocketAddress(ip2, port)
98          );
99          Mockito.when(dnsResolver.resolve("somehost", port)).thenReturn(resolvedAddresses);
100 
101         Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(port);
102         Mockito.when(detachedSocketFactory.create(Mockito.any(), Mockito.any())).thenReturn(socket);
103 
104         final SocketConfig socketConfig = SocketConfig.custom()
105             .setSoKeepAlive(true)
106             .setSoReuseAddress(true)
107             .setSoTimeout(5000, TimeUnit.MILLISECONDS)
108             .setTcpNoDelay(true)
109             .setSoLinger(50, TimeUnit.MILLISECONDS)
110             .build();
111         final InetSocketAddress localAddress = new InetSocketAddress(local, 0);
112         connectionOperator.connect(conn, host, null, localAddress, Timeout.ofMilliseconds(123), socketConfig, null, context);
113 
114         Mockito.verify(socket).setKeepAlive(true);
115         Mockito.verify(socket).setReuseAddress(true);
116         Mockito.verify(socket).setSoTimeout(5000);
117         Mockito.verify(socket).setSoLinger(true, 50);
118         Mockito.verify(socket).setTcpNoDelay(true);
119         Mockito.verify(socket).bind(localAddress);
120 
121         Mockito.verify(socket).connect(new InetSocketAddress(ip1, port), 123);
122         Mockito.verify(conn, Mockito.times(2)).bind(socket);
123     }
124 
125     @Test
126     void testConnectWithTLSUpgrade() throws Exception {
127         final HttpClientContext context = HttpClientContext.create();
128         final HttpHost host = new HttpHost("https", "somehost");
129         final InetAddress local = InetAddress.getByAddress(new byte[] {127, 0, 0, 0});
130         final InetAddress ip1 = InetAddress.getByAddress(new byte[] {127, 0, 0, 1});
131         final InetAddress ip2 = InetAddress.getByAddress(new byte[] {127, 0, 0, 2});
132         final int port = 443;
133 
134         final TlsConfig tlsConfig = TlsConfig.custom()
135                 .setHandshakeTimeout(Timeout.ofMilliseconds(345))
136                 .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1)
137                 .build();
138 
139         final List<InetSocketAddress> resolvedAddresses = Arrays.asList(
140                 new InetSocketAddress(ip1, port),
141                 new InetSocketAddress(ip2, port)
142         );
143         Mockito.when(dnsResolver.resolve("somehost", port)).thenReturn(resolvedAddresses);
144 
145         Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(port);
146         Mockito.when(detachedSocketFactory.create(Mockito.any(), Mockito.any())).thenReturn(socket);
147 
148         Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy);
149         final SSLSocket upgradedSocket = Mockito.mock(SSLSocket.class);
150         Mockito.when(tlsSocketStrategy.upgrade(
151                 Mockito.same(socket),
152                 Mockito.eq("somehost"),
153                 Mockito.anyInt(),
154                 Mockito.any(),
155                 Mockito.any())).thenReturn(upgradedSocket);
156 
157         final InetSocketAddress localAddress = new InetSocketAddress(local, 0);
158         connectionOperator.connect(conn, host, null, localAddress,
159                 Timeout.ofMilliseconds(123), SocketConfig.DEFAULT, tlsConfig, context);
160 
161         Mockito.verify(socket).connect(new InetSocketAddress(ip1, port), 123);
162         Mockito.verify(conn, Mockito.times(2)).bind(socket);
163         Mockito.verify(tlsSocketStrategy).upgrade(socket, "somehost", -1, tlsConfig, context);
164         Mockito.verify(conn, Mockito.times(1)).bind(upgradedSocket, socket);
165     }
166 
167 
168     @Test
169     void testConnectTimeout() throws Exception {
170         final HttpClientContext context = HttpClientContext.create();
171         final HttpHost host = new HttpHost("somehost");
172         final int port = 80;
173         final InetAddress ip1 = InetAddress.getByAddress(new byte[] {10, 0, 0, 1});
174         final InetAddress ip2 = InetAddress.getByAddress(new byte[] {10, 0, 0, 2});
175         final List<InetSocketAddress> resolvedAddresses = Arrays.asList(
176                 new InetSocketAddress(ip1, port),
177                 new InetSocketAddress(ip2, port)
178         );
179         Mockito.when(dnsResolver.resolve("somehost", port)).thenReturn(resolvedAddresses);
180         Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(port);
181         Mockito.when(detachedSocketFactory.create(Mockito.any(), Mockito.any())).thenReturn(socket);
182         Mockito.doThrow(new SocketTimeoutException()).when(socket).connect(Mockito.any(), Mockito.anyInt());
183         Assertions.assertThrows(ConnectTimeoutException.class, () ->
184                 connectionOperator.connect(
185                         conn, host, null, new InetSocketAddress(InetAddress.getLoopbackAddress(), 0),
186                         Timeout.ofMilliseconds(1000), SocketConfig.DEFAULT, null, context));
187     }
188 
189     @Test
190     void testConnectFailure() throws Exception {
191         final HttpClientContext context = HttpClientContext.create();
192         final HttpHost host = new HttpHost("somehost");
193         final InetAddress ip1 = InetAddress.getByAddress(new byte[] {10, 0, 0, 1});
194         final InetAddress ip2 = InetAddress.getByAddress(new byte[] {10, 0, 0, 2});
195         final int port = 80;
196         final List<InetSocketAddress> resolvedAddresses = Arrays.asList(
197                 new InetSocketAddress(ip1, port),
198                 new InetSocketAddress(ip2, port)
199         );
200         Mockito.when(dnsResolver.resolve("somehost", port)).thenReturn(resolvedAddresses);
201 
202         Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(port);
203         Mockito.when(detachedSocketFactory.create(Mockito.any(), Mockito.any())).thenReturn(socket);
204         Mockito.doThrow(new ConnectException()).when(socket).connect(Mockito.any(), Mockito.anyInt());
205 
206         Assertions.assertThrows(HttpHostConnectException.class, () ->
207                 connectionOperator.connect(
208                         conn, host, null, TimeValue.ofMilliseconds(1000), SocketConfig.DEFAULT, context));
209     }
210 
211     @Test
212     void testConnectFailover() throws Exception {
213         final HttpClientContext context = HttpClientContext.create();
214         final HttpHost host = new HttpHost("somehost");
215         final InetAddress local = InetAddress.getByAddress(new byte[] {127, 0, 0, 0});
216         final InetSocketAddress ipAddress1 = new InetSocketAddress(InetAddress.getByAddress(new byte[] {10, 0, 0, 1}), 80);
217         final InetSocketAddress ipAddress2 = new InetSocketAddress(InetAddress.getByAddress(new byte[] {10, 0, 0, 2}), 80);
218 
219         Mockito.when(dnsResolver.resolve("somehost", 80)).thenReturn(Arrays.asList(ipAddress1, ipAddress2));
220         Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(80);
221         Mockito.when(detachedSocketFactory.create(Mockito.any(), Mockito.any())).thenReturn(socket);
222         Mockito.doThrow(new ConnectException()).when(socket).connect(
223                 Mockito.eq(ipAddress1),
224                 Mockito.anyInt());
225 
226         final InetSocketAddress localAddress = new InetSocketAddress(local, 0);
227         final TlsConfig tlsConfig = TlsConfig.custom()
228                 .build();
229         connectionOperator.connect(conn, host, null, localAddress,
230                 Timeout.ofMilliseconds(123), SocketConfig.DEFAULT, tlsConfig, context);
231 
232         Mockito.verify(socket, Mockito.times(2)).bind(localAddress);
233         Mockito.verify(socket).connect(ipAddress2, 123);
234         Mockito.verify(conn, Mockito.times(3)).bind(socket);
235 
236     }
237 
238     @Test
239     void testConnectExplicitAddress() throws Exception {
240         final HttpClientContext context = HttpClientContext.create();
241         final InetAddress local = InetAddress.getByAddress(new byte[] {127, 0, 0, 0});
242         final InetAddress ip = InetAddress.getByAddress(new byte[] {127, 0, 0, 23});
243         final HttpHost host = new HttpHost(ip);
244 
245         Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(80);
246         Mockito.when(detachedSocketFactory.create(Mockito.any(), Mockito.any())).thenReturn(socket);
247 
248         final InetSocketAddress localAddress = new InetSocketAddress(local, 0);
249         final TlsConfig tlsConfig = TlsConfig.custom()
250                 .build();
251         connectionOperator.connect(conn, host, null, localAddress,
252                 Timeout.ofMilliseconds(123), SocketConfig.DEFAULT, tlsConfig, context);
253 
254         Mockito.verify(socket).bind(localAddress);
255         Mockito.verify(socket).connect(new InetSocketAddress(ip, 80), 123);
256         Mockito.verify(dnsResolver, Mockito.never()).resolve(Mockito.anyString(), Mockito.anyInt());
257         Mockito.verify(conn, Mockito.times(2)).bind(socket);
258     }
259 
260     @Test
261     void testUpgrade() throws Exception {
262         final HttpClientContext context = HttpClientContext.create();
263         final HttpHost host = new HttpHost("https", "somehost", -1);
264 
265         Mockito.when(conn.isOpen()).thenReturn(true);
266         Mockito.when(conn.getSocket()).thenReturn(socket);
267         Mockito.when(tlsSocketStrategyLookup.lookup("https")).thenReturn(tlsSocketStrategy);
268 
269         final SSLSocket upgradedSocket = Mockito.mock(SSLSocket.class);
270         Mockito.when(tlsSocketStrategy.upgrade(
271                 Mockito.any(),
272                 Mockito.eq("somehost"),
273                 Mockito.anyInt(),
274                 Mockito.eq(Timeout.ofMilliseconds(345)),
275                 Mockito.any())).thenReturn(upgradedSocket);
276 
277         connectionOperator.upgrade(conn, host, null, Timeout.ofMilliseconds(345), context);
278 
279         Mockito.verify(conn).bind(Mockito.eq(upgradedSocket), Mockito.any());
280     }
281 
282     @Test
283     void testUpgradeUpsupportedScheme() {
284         final HttpClientContext context = HttpClientContext.create();
285         final HttpHost host = new HttpHost("httpsssss", "somehost", -1);
286 
287         Mockito.when(conn.isOpen()).thenReturn(true);
288         Mockito.when(conn.getSocket()).thenReturn(socket);
289 
290         Assertions.assertThrows(UnsupportedSchemeException.class, () ->
291                 connectionOperator.upgrade(conn, host, context));
292     }
293 
294     @Test
295     void testUpgradeNonLayeringScheme() {
296         final HttpClientContext context = HttpClientContext.create();
297         final HttpHost host = new HttpHost("http", "somehost", -1);
298 
299         Mockito.when(conn.isOpen()).thenReturn(true);
300         Mockito.when(conn.getSocket()).thenReturn(socket);
301 
302         Assertions.assertThrows(UnsupportedSchemeException.class, () ->
303                 connectionOperator.upgrade(conn, host, context));
304     }
305 
306     @Test
307     void testConnectWithDisableDnsResolution() throws Exception {
308         final HttpClientContext context = HttpClientContext.create();
309         final HttpHost host = new HttpHost("someonion.onion");
310         final InetAddress local = InetAddress.getByAddress(new byte[]{127, 0, 0, 0});
311         final int port = 80;
312 
313         final List<InetSocketAddress> resolvedAddresses = Collections.singletonList(
314                 InetSocketAddress.createUnresolved(host.getHostName(), port)
315         );
316         Mockito.when(dnsResolver.resolve(host.getHostName(), port)).thenReturn(resolvedAddresses);
317 
318         Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(port);
319         Mockito.when(detachedSocketFactory.create(Mockito.any(), Mockito.any())).thenReturn(socket);
320 
321         final SocketConfig socketConfig = SocketConfig.custom()
322                 .setSoKeepAlive(true)
323                 .setSoReuseAddress(true)
324                 .setSoTimeout(5000, TimeUnit.MILLISECONDS)
325                 .setTcpNoDelay(true)
326                 .setSoLinger(50, TimeUnit.MILLISECONDS)
327                 .build();
328         final InetSocketAddress localAddress = new InetSocketAddress(local, 0);
329         final InetSocketAddress remoteAddress = InetSocketAddress.createUnresolved(host.getHostName(), port);
330 
331         connectionOperator.connect(conn, host, null, localAddress, Timeout.ofMilliseconds(123), socketConfig, null, context);
332 
333         // Verify that the socket was created and attempted to connect without DNS resolution
334         Mockito.verify(socket).setKeepAlive(true);
335         Mockito.verify(socket).setReuseAddress(true);
336         Mockito.verify(socket).setSoTimeout(5000);
337         Mockito.verify(socket).setSoLinger(true, 50);
338         Mockito.verify(socket).setTcpNoDelay(true);
339         Mockito.verify(socket).bind(localAddress);
340 
341         Mockito.verify(socket).connect(remoteAddress, 123);
342         Mockito.verify(conn, Mockito.times(2)).bind(socket);
343         Mockito.verify(dnsResolver, Mockito.never()).resolve(Mockito.anyString());
344     }
345 
346     @Test
347     void testConnectWithDnsResolutionAndFallback() throws Exception {
348         final HttpClientContext context = HttpClientContext.create();
349         final HttpHost host = new HttpHost("fallbackhost.com");
350         final InetAddress local = InetAddress.getByAddress(new byte[] {127, 0, 0, 0});
351         final int port = 8080;
352         final InetAddress ip1 = InetAddress.getByAddress(new byte[] {10, 0, 0, 1});
353         final InetAddress ip2 = InetAddress.getByAddress(new byte[] {10, 0, 0, 2});
354 
355         // Update to match the new `resolve` implementation that returns a list of SocketAddress
356         final List<InetSocketAddress> resolvedAddresses = Arrays.asList(
357                 new InetSocketAddress(ip1, port),
358                 new InetSocketAddress(ip2, port)
359         );
360         Mockito.when(dnsResolver.resolve("fallbackhost.com", port)).thenReturn(resolvedAddresses);
361         Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(port);
362         Mockito.when(detachedSocketFactory.create(Mockito.any(), Mockito.any())).thenReturn(socket);
363 
364         // Simulate failure to connect to the first resolved address
365         Mockito.doThrow(new ConnectException()).when(socket).connect(Mockito.eq(new InetSocketAddress(ip1, port)), Mockito.anyInt());
366 
367         final InetSocketAddress localAddress = new InetSocketAddress(local, 0);
368         final SocketConfig socketConfig = SocketConfig.custom()
369                 .setSoKeepAlive(true)
370                 .setSoReuseAddress(true)
371                 .setSoTimeout(5000, TimeUnit.MILLISECONDS)
372                 .setTcpNoDelay(true)
373                 .setSoLinger(50, TimeUnit.MILLISECONDS)
374                 .build();
375 
376         // Connect using the updated connection operator
377         connectionOperator.connect(conn, host, null, localAddress, Timeout.ofMilliseconds(123), socketConfig, null, context);
378 
379         // Verify fallback behavior after connection failure to the first address
380         Mockito.verify(socket, Mockito.times(2)).bind(localAddress);
381         Mockito.verify(socket).connect(new InetSocketAddress(ip2, port), 123);
382         Mockito.verify(conn, Mockito.times(3)).bind(socket);
383     }
384 }