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  package org.apache.hc.client5.http.impl.classic;
28  
29  import java.io.ByteArrayInputStream;
30  import java.io.InputStream;
31  import java.util.Collections;
32  
33  import org.apache.hc.client5.http.AuthenticationStrategy;
34  import org.apache.hc.client5.http.HttpRoute;
35  import org.apache.hc.client5.http.RouteInfo;
36  import org.apache.hc.client5.http.auth.AuthScope;
37  import org.apache.hc.client5.http.auth.ChallengeType;
38  import org.apache.hc.client5.http.auth.CredentialsProvider;
39  import org.apache.hc.client5.http.auth.StandardAuthScheme;
40  import org.apache.hc.client5.http.classic.ExecChain;
41  import org.apache.hc.client5.http.classic.ExecRuntime;
42  import org.apache.hc.client5.http.classic.methods.HttpGet;
43  import org.apache.hc.client5.http.entity.EntityBuilder;
44  import org.apache.hc.client5.http.impl.TunnelRefusedException;
45  import org.apache.hc.client5.http.impl.auth.BasicScheme;
46  import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
47  import org.apache.hc.client5.http.protocol.HttpClientContext;
48  import org.apache.hc.core5.http.ClassicHttpRequest;
49  import org.apache.hc.core5.http.ClassicHttpResponse;
50  import org.apache.hc.core5.http.ConnectionReuseStrategy;
51  import org.apache.hc.core5.http.HttpException;
52  import org.apache.hc.core5.http.HttpHeaders;
53  import org.apache.hc.core5.http.HttpHost;
54  import org.apache.hc.core5.http.HttpRequest;
55  import org.apache.hc.core5.http.HttpVersion;
56  import org.apache.hc.core5.http.io.entity.StringEntity;
57  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
58  import org.apache.hc.core5.http.protocol.HttpProcessor;
59  import org.junit.jupiter.api.Assertions;
60  import org.junit.jupiter.api.BeforeEach;
61  import org.junit.jupiter.api.Test;
62  import org.mockito.ArgumentCaptor;
63  import org.mockito.Mock;
64  import org.mockito.Mockito;
65  import org.mockito.MockitoAnnotations;
66  import org.mockito.stubbing.Answer;
67  
68  @SuppressWarnings({"boxing","static-access"}) // test code
69  public class TestConnectExec {
70  
71      @Mock
72      private ConnectionReuseStrategy reuseStrategy;
73      @Mock
74      private HttpProcessor proxyHttpProcessor;
75      @Mock
76      private AuthenticationStrategy proxyAuthStrategy;
77      @Mock
78      private ExecRuntime execRuntime;
79      @Mock
80      private ExecChain execChain;
81  
82      private ConnectExec exec;
83      private HttpHost target;
84      private HttpHost proxy;
85  
86      @BeforeEach
87      public void setup() throws Exception {
88          MockitoAnnotations.openMocks(this);
89          exec = new ConnectExec(reuseStrategy, proxyHttpProcessor, proxyAuthStrategy, null, true);
90          target = new HttpHost("foo", 80);
91          proxy = new HttpHost("bar", 8888);
92      }
93  
94      @Test
95      public void testExecAcquireConnection() throws Exception {
96          final HttpRoute route = new HttpRoute(target);
97          final HttpClientContext context = new HttpClientContext();
98          final ClassicHttpRequest request = new HttpGet("http://bar/test");
99          try (final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK")) {
100             response.setEntity(EntityBuilder.create()
101                     .setStream(new ByteArrayInputStream(new byte[]{}))
102                     .build());
103         }
104         context.setUserToken("Blah");
105 
106         Mockito.when(execRuntime.isEndpointAcquired()).thenReturn(false);
107         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
108         exec.execute(request, scope, execChain);
109         Mockito.verify(execRuntime).acquireEndpoint("test", route, "Blah", context);
110         Mockito.verify(execRuntime).connectEndpoint(context);
111     }
112 
113     @Test
114     public void testEstablishDirectRoute() throws Exception {
115         final HttpRoute route = new HttpRoute(target);
116         final HttpClientContext context = new HttpClientContext();
117         final ClassicHttpRequest request = new HttpGet("http://bar/test");
118 
119         final ConnectionState connectionState = new ConnectionState();
120         Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connectEndpoint(Mockito.any());
121         Mockito.when(execRuntime.isEndpointConnected()).thenAnswer(connectionState.isConnectedAnswer());
122 
123         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
124         exec.execute(request, scope, execChain);
125 
126         Mockito.verify(execRuntime).connectEndpoint(context);
127         Mockito.verify(execRuntime, Mockito.never()).execute(
128                 Mockito.anyString(),
129                 Mockito.any(),
130                 Mockito.any());
131     }
132 
133     @Test
134     public void testEstablishRouteDirectProxy() throws Exception {
135         final HttpRoute route = new HttpRoute(target, null, proxy, false);
136         final HttpClientContext context = new HttpClientContext();
137         final ClassicHttpRequest request = new HttpGet("http://bar/test");
138 
139         final ConnectionState connectionState = new ConnectionState();
140         Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connectEndpoint(Mockito.any());
141         Mockito.when(execRuntime.isEndpointConnected()).thenAnswer(connectionState.isConnectedAnswer());
142 
143         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
144         exec.execute(request, scope, execChain);
145 
146         Mockito.verify(execRuntime).connectEndpoint(context);
147         Mockito.verify(execRuntime, Mockito.never()).execute(
148                 Mockito.anyString(),
149                 Mockito.any(),
150                 Mockito.any());
151     }
152 
153     @Test
154     public void testEstablishRouteViaProxyTunnel() throws Exception {
155         final HttpRoute route = new HttpRoute(target, null, proxy, true);
156         final HttpClientContext context = new HttpClientContext();
157         final ClassicHttpRequest request = new HttpGet("http://bar/test");
158         final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
159 
160         final ConnectionState connectionState = new ConnectionState();
161         Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connectEndpoint(Mockito.any());
162         Mockito.when(execRuntime.isEndpointConnected()).thenAnswer(connectionState.isConnectedAnswer());
163         Mockito.when(execRuntime.execute(
164                 Mockito.anyString(),
165                 Mockito.any(),
166                 Mockito.any())).thenReturn(response);
167 
168         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
169         exec.execute(request, scope, execChain);
170 
171         Mockito.verify(execRuntime).connectEndpoint(context);
172         final ArgumentCaptor<ClassicHttpRequest> reqCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class);
173         Mockito.verify(execRuntime).execute(
174                 Mockito.anyString(),
175                 reqCaptor.capture(),
176                 Mockito.same(context));
177         final HttpRequest connect = reqCaptor.getValue();
178         Assertions.assertNotNull(connect);
179         Assertions.assertEquals("CONNECT", connect.getMethod());
180         Assertions.assertEquals(HttpVersion.HTTP_1_1, connect.getVersion());
181         Assertions.assertEquals("foo:80", connect.getRequestUri());
182     }
183 
184     @Test
185     public void testEstablishRouteViaProxyTunnelUnexpectedResponse() throws Exception {
186         final HttpRoute route = new HttpRoute(target, null, proxy, true);
187         final HttpClientContext context = new HttpClientContext();
188         final ClassicHttpRequest request = new HttpGet("http://bar/test");
189         final ClassicHttpResponse response = new BasicClassicHttpResponse(101, "Lost");
190 
191         final ConnectionState connectionState = new ConnectionState();
192         Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connectEndpoint(Mockito.any());
193         Mockito.when(execRuntime.isEndpointConnected()).thenAnswer(connectionState.isConnectedAnswer());
194         Mockito.when(execRuntime.execute(
195                 Mockito.anyString(),
196                 Mockito.any(),
197                 Mockito.any())).thenReturn(response);
198 
199         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
200         Assertions.assertThrows(HttpException.class, () ->
201                 exec.execute(request, scope, execChain));
202     }
203 
204     @Test
205     public void testEstablishRouteViaProxyTunnelFailure() throws Exception {
206         final HttpRoute route = new HttpRoute(target, null, proxy, true);
207         final HttpClientContext context = new HttpClientContext();
208         final ClassicHttpRequest request = new HttpGet("http://bar/test");
209         final ClassicHttpResponse response = new BasicClassicHttpResponse(500, "Boom");
210         response.setEntity(new StringEntity("Ka-boom"));
211 
212         final ConnectionState connectionState = new ConnectionState();
213         Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connectEndpoint(Mockito.any());
214         Mockito.when(execRuntime.isEndpointConnected()).thenAnswer(connectionState.isConnectedAnswer());
215         Mockito.when(execRuntime.execute(
216                 Mockito.anyString(),
217                 Mockito.any(),
218                 Mockito.any())).thenReturn(response);
219 
220         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
221         final TunnelRefusedException exception = Assertions.assertThrows(TunnelRefusedException.class, () ->
222                 exec.execute(request, scope, execChain));
223         Assertions.assertEquals("Ka-boom", exception.getResponseMessage());
224         Mockito.verify(execRuntime, Mockito.atLeastOnce()).disconnectEndpoint();
225         Mockito.verify(execRuntime).discardEndpoint();
226     }
227 
228     @Test
229     public void testEstablishRouteViaProxyTunnelRetryOnAuthChallengePersistentConnection() throws Exception {
230         final HttpRoute route = new HttpRoute(target, null, proxy, true);
231         final HttpClientContext context = new HttpClientContext();
232         final ClassicHttpRequest request = new HttpGet("http://bar/test");
233         final ClassicHttpResponse response1 = new BasicClassicHttpResponse(407, "Huh?");
234         response1.setHeader(HttpHeaders.PROXY_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=test");
235         final InputStream inStream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
236         response1.setEntity(EntityBuilder.create()
237                 .setStream(inStream1)
238                 .build());
239         final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
240 
241         context.setCredentialsProvider(CredentialsProviderBuilder.create()
242                 .add(new AuthScope(proxy), "user", "pass".toCharArray())
243                 .build());
244 
245         final ConnectionState connectionState = new ConnectionState();
246         Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connectEndpoint(Mockito.any());
247         Mockito.when(execRuntime.isEndpointConnected()).thenAnswer(connectionState.isConnectedAnswer());
248         Mockito.when(reuseStrategy.keepAlive(
249                 Mockito.any(),
250                 Mockito.any(),
251                 Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
252         Mockito.when(execRuntime.execute(
253                 Mockito.anyString(),
254                 Mockito.any(),
255                 Mockito.any())).thenReturn(response1, response2);
256 
257         Mockito.when(proxyAuthStrategy.select(
258                 Mockito.eq(ChallengeType.PROXY),
259                 Mockito.any(),
260                 Mockito.any())).thenReturn(Collections.singletonList(new BasicScheme()));
261 
262         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
263         exec.execute(request, scope, execChain);
264 
265         Mockito.verify(execRuntime).connectEndpoint(context);
266         Mockito.verify(inStream1).close();
267     }
268 
269     @Test
270     public void testEstablishRouteViaProxyTunnelRetryOnAuthChallengeNonPersistentConnection() throws Exception {
271         final HttpRoute route = new HttpRoute(target, null, proxy, true);
272         final HttpClientContext context = new HttpClientContext();
273         final ClassicHttpRequest request = new HttpGet("http://bar/test");
274         final ClassicHttpResponse response1 = new BasicClassicHttpResponse(407, "Huh?");
275         response1.setHeader(HttpHeaders.PROXY_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=test");
276         final InputStream inStream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
277         response1.setEntity(EntityBuilder.create()
278                 .setStream(inStream1)
279                 .build());
280         final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
281 
282         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
283                 .add(new AuthScope(proxy), "user", "pass".toCharArray())
284                 .build();
285         context.setCredentialsProvider(credentialsProvider);
286 
287         final ConnectionState connectionState = new ConnectionState();
288         Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connectEndpoint(Mockito.any());
289         Mockito.when(execRuntime.isEndpointConnected()).thenAnswer(connectionState.isConnectedAnswer());
290         Mockito.when(execRuntime.execute(
291                 Mockito.anyString(),
292                 Mockito.any(),
293                 Mockito.any())).thenReturn(response1, response2);
294 
295         Mockito.when(proxyAuthStrategy.select(
296                 Mockito.eq(ChallengeType.PROXY),
297                 Mockito.any(),
298                 Mockito.any())).thenReturn(Collections.singletonList(new BasicScheme()));
299 
300         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
301         exec.execute(request, scope, execChain);
302 
303         Mockito.verify(execRuntime).connectEndpoint(context);
304         Mockito.verify(inStream1, Mockito.never()).close();
305         Mockito.verify(execRuntime, Mockito.atLeastOnce()).disconnectEndpoint();
306     }
307 
308     @Test
309     public void testEstablishRouteViaProxyTunnelMultipleHops() throws Exception {
310         final HttpHost proxy1 = new HttpHost("this", 8888);
311         final HttpHost proxy2 = new HttpHost("that", 8888);
312         final HttpRoute route = new HttpRoute(target, null, new HttpHost[] {proxy1, proxy2},
313                 true, RouteInfo.TunnelType.TUNNELLED, RouteInfo.LayerType.LAYERED);
314         final HttpClientContext context = new HttpClientContext();
315         final ClassicHttpRequest request = new HttpGet("http://bar/test");
316 
317         final ConnectionState connectionState = new ConnectionState();
318         Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connectEndpoint(Mockito.any());
319         Mockito.when(execRuntime.isEndpointConnected()).thenAnswer(connectionState.isConnectedAnswer());
320 
321         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context);
322         Assertions.assertThrows(HttpException.class, () ->
323                 exec.execute(request, scope, execChain));
324     }
325 
326     static class ConnectionState {
327 
328         private boolean connected;
329 
330         public Answer<?> connectAnswer() {
331 
332             return invocationOnMock -> {
333                 connected = true;
334                 return null;
335             };
336         }
337 
338         public Answer<Boolean> isConnectedAnswer() {
339 
340             return invocationOnMock -> connected;
341 
342         }
343     }
344 
345 }