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.net.URI;
32  import java.net.URISyntaxException;
33  import java.util.Arrays;
34  import java.util.List;
35  
36  import org.apache.hc.client5.http.CircularRedirectException;
37  import org.apache.hc.client5.http.HttpRoute;
38  import org.apache.hc.client5.http.RedirectException;
39  import org.apache.hc.client5.http.auth.AuthExchange;
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.config.RequestConfig;
44  import org.apache.hc.client5.http.entity.EntityBuilder;
45  import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
46  import org.apache.hc.client5.http.impl.auth.BasicScheme;
47  import org.apache.hc.client5.http.impl.auth.NTLMScheme;
48  import org.apache.hc.client5.http.protocol.HttpClientContext;
49  import org.apache.hc.client5.http.protocol.RedirectLocations;
50  import org.apache.hc.client5.http.protocol.RedirectStrategy;
51  import org.apache.hc.client5.http.routing.HttpRoutePlanner;
52  import org.apache.hc.core5.http.ClassicHttpRequest;
53  import org.apache.hc.core5.http.ClassicHttpResponse;
54  import org.apache.hc.core5.http.HttpEntity;
55  import org.apache.hc.core5.http.HttpException;
56  import org.apache.hc.core5.http.HttpHeaders;
57  import org.apache.hc.core5.http.HttpHost;
58  import org.apache.hc.core5.http.HttpStatus;
59  import org.apache.hc.core5.http.ProtocolException;
60  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
61  import org.junit.jupiter.api.Assertions;
62  import org.junit.jupiter.api.BeforeEach;
63  import org.junit.jupiter.api.Test;
64  import org.mockito.ArgumentCaptor;
65  import org.mockito.ArgumentMatcher;
66  import org.mockito.ArgumentMatchers;
67  import org.mockito.Mock;
68  import org.mockito.Mockito;
69  import org.mockito.MockitoAnnotations;
70  
71  public class TestRedirectExec {
72  
73      @Mock
74      private HttpRoutePlanner httpRoutePlanner;
75      @Mock
76      private ExecChain chain;
77      @Mock
78      private ExecRuntime endpoint;
79  
80      private RedirectStrategy redirectStrategy;
81      private RedirectExec redirectExec;
82      private HttpHost target;
83  
84      @BeforeEach
85      public void setup() throws Exception {
86          MockitoAnnotations.openMocks(this);
87          target = new HttpHost("localhost", 80);
88          redirectStrategy = Mockito.spy(new DefaultRedirectStrategy());
89          redirectExec = new RedirectExec(httpRoutePlanner, redirectStrategy);
90      }
91  
92      @Test
93      public void testFundamentals() throws Exception {
94          final HttpRoute route = new HttpRoute(target);
95          final HttpGet request = new HttpGet("/test");
96          final HttpClientContext context = HttpClientContext.create();
97  
98          final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
99          final URI redirect = new URI("http://localhost:80/redirect");
100         response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
101         final InputStream inStream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
102         final HttpEntity entity1 = EntityBuilder.create()
103                 .setStream(inStream1)
104                 .build();
105         response1.setEntity(entity1);
106         final ClassicHttpResponse response2 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_OK));
107         final InputStream inStream2 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
108         final HttpEntity entity2 = EntityBuilder.create()
109                 .setStream(inStream2)
110                 .build();
111         response2.setEntity(entity2);
112 
113         Mockito.when(chain.proceed(
114                 ArgumentMatchers.same(request),
115                 ArgumentMatchers.any())).thenReturn(response1);
116         Mockito.when(chain.proceed(
117                 HttpRequestMatcher.matchesRequestUri(redirect),
118                 ArgumentMatchers.any())).thenReturn(response2);
119 
120         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
121         redirectExec.execute(request, scope, chain);
122 
123         final ArgumentCaptor<ClassicHttpRequest> reqCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class);
124         Mockito.verify(chain, Mockito.times(2)).proceed(reqCaptor.capture(), ArgumentMatchers.same(scope));
125 
126         final List<ClassicHttpRequest> allValues = reqCaptor.getAllValues();
127         Assertions.assertNotNull(allValues);
128         Assertions.assertEquals(2, allValues.size());
129         Assertions.assertSame(request, allValues.get(0));
130 
131         Mockito.verify(response1, Mockito.times(1)).close();
132         Mockito.verify(inStream1, Mockito.times(2)).close();
133         Mockito.verify(response2, Mockito.never()).close();
134         Mockito.verify(inStream2, Mockito.never()).close();
135     }
136 
137     @Test
138     public void testMaxRedirect() throws Exception {
139         final HttpRoute route = new HttpRoute(target);
140         final HttpGet request = new HttpGet("/test");
141         final HttpClientContext context = HttpClientContext.create();
142         final RequestConfig config = RequestConfig.custom()
143                 .setRedirectsEnabled(true)
144                 .setMaxRedirects(3)
145                 .build();
146         context.setRequestConfig(config);
147 
148         final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
149         final URI redirect = new URI("http://localhost:80/redirect");
150         response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
151 
152         Mockito.when(chain.proceed(ArgumentMatchers.any(), ArgumentMatchers.any())).thenReturn(response1);
153 
154         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
155         Assertions.assertThrows(RedirectException.class, () ->
156                 redirectExec.execute(request, scope, chain));
157     }
158 
159     @Test
160     public void testRelativeRedirect() throws Exception {
161         final HttpRoute route = new HttpRoute(target);
162         final HttpGet request = new HttpGet("/test");
163         final HttpClientContext context = HttpClientContext.create();
164 
165         final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
166         final URI redirect = new URI("/redirect");
167         response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
168         Mockito.when(chain.proceed(
169                 ArgumentMatchers.same(request),
170                 ArgumentMatchers.any())).thenReturn(response1);
171 
172         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
173         Assertions.assertThrows(HttpException.class, () ->
174                 redirectExec.execute(request, scope, chain));
175     }
176 
177     @Test
178     public void testCrossSiteRedirect() throws Exception {
179 
180         final HttpHost proxy = new HttpHost("proxy");
181         final HttpRoute route = new HttpRoute(target, proxy);
182         final HttpGet request = new HttpGet("/test");
183         final HttpClientContext context = HttpClientContext.create();
184 
185         final AuthExchange targetAuthExchange = new AuthExchange();
186         targetAuthExchange.setState(AuthExchange.State.SUCCESS);
187         targetAuthExchange.select(new BasicScheme());
188         final AuthExchange proxyAuthExchange = new AuthExchange();
189         proxyAuthExchange.setState(AuthExchange.State.SUCCESS);
190         proxyAuthExchange.select(new NTLMScheme());
191         context.setAuthExchange(target, targetAuthExchange);
192         context.setAuthExchange(proxy, proxyAuthExchange);
193 
194         final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
195         final URI redirect = new URI("http://otherhost:8888/redirect");
196         response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
197         final ClassicHttpResponse response2 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_OK));
198         final HttpHost otherHost = new HttpHost("otherhost", 8888);
199         Mockito.when(chain.proceed(
200                 ArgumentMatchers.same(request),
201                 ArgumentMatchers.any())).thenReturn(response1);
202         Mockito.when(chain.proceed(
203                 HttpRequestMatcher.matchesRequestUri(redirect),
204                 ArgumentMatchers.any())).thenReturn(response2);
205         Mockito.when(httpRoutePlanner.determineRoute(
206                 ArgumentMatchers.eq(otherHost),
207                 ArgumentMatchers.<HttpClientContext>any())).thenReturn(new HttpRoute(otherHost));
208 
209         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
210         redirectExec.execute(request, scope, chain);
211 
212         final AuthExchange authExchange1 = context.getAuthExchange(target);
213         Assertions.assertNotNull(authExchange1);
214         Assertions.assertEquals(AuthExchange.State.UNCHALLENGED, authExchange1.getState());
215         Assertions.assertNull(authExchange1.getAuthScheme());
216         final AuthExchange authExchange2 = context.getAuthExchange(proxy);
217         Assertions.assertNotNull(authExchange2);
218         Assertions.assertEquals(AuthExchange.State.UNCHALLENGED, authExchange2.getState());
219         Assertions.assertNull(authExchange2.getAuthScheme());
220     }
221 
222     @Test
223     public void testAllowCircularRedirects() throws Exception {
224         final HttpRoute route = new HttpRoute(target);
225         final HttpClientContext context = HttpClientContext.create();
226         context.setRequestConfig(RequestConfig.custom()
227                 .setCircularRedirectsAllowed(true)
228                 .build());
229 
230         final URI uri = URI.create("http://localhost/");
231         final HttpGet request = new HttpGet(uri);
232 
233         final URI uri1 = URI.create("http://localhost/stuff1");
234         final URI uri2 = URI.create("http://localhost/stuff2");
235         final ClassicHttpResponse response1 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
236         response1.addHeader("Location", uri1.toASCIIString());
237         final ClassicHttpResponse response2 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
238         response2.addHeader("Location", uri2.toASCIIString());
239         final ClassicHttpResponse response3 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
240         response3.addHeader("Location", uri1.toASCIIString());
241         final ClassicHttpResponse response4 = new BasicClassicHttpResponse(HttpStatus.SC_OK);
242 
243         Mockito.when(chain.proceed(
244                 HttpRequestMatcher.matchesRequestUri(uri),
245                 ArgumentMatchers.any())).thenReturn(response1);
246         Mockito.when(chain.proceed(
247                 HttpRequestMatcher.matchesRequestUri(uri1),
248                 ArgumentMatchers.any())).thenReturn(response2, response4);
249         Mockito.when(chain.proceed(
250                 HttpRequestMatcher.matchesRequestUri(uri2),
251                 ArgumentMatchers.any())).thenReturn(response3);
252         Mockito.when(httpRoutePlanner.determineRoute(
253                 ArgumentMatchers.eq(new HttpHost("localhost")),
254                 ArgumentMatchers.<HttpClientContext>any())).thenReturn(route);
255 
256         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
257         redirectExec.execute(request, scope, chain);
258 
259         final RedirectLocations uris = context.getRedirectLocations();
260         Assertions.assertNotNull(uris);
261         Assertions.assertEquals(Arrays.asList(uri1, uri2, uri1), uris.getAll());
262     }
263 
264     @Test
265     public void testGetLocationUriDisallowCircularRedirects() throws Exception {
266         final HttpRoute route = new HttpRoute(target);
267         final HttpClientContext context = HttpClientContext.create();
268         context.setRequestConfig(RequestConfig.custom()
269                 .setCircularRedirectsAllowed(false)
270                 .build());
271 
272         final URI uri = URI.create("http://localhost/");
273         final HttpGet request = new HttpGet(uri);
274 
275         final URI uri1 = URI.create("http://localhost/stuff1");
276         final URI uri2 = URI.create("http://localhost/stuff2");
277         final ClassicHttpResponse response1 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
278         response1.addHeader("Location", uri1.toASCIIString());
279         final ClassicHttpResponse response2 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
280         response2.addHeader("Location", uri2.toASCIIString());
281         final ClassicHttpResponse response3 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
282         response3.addHeader("Location", uri1.toASCIIString());
283         Mockito.when(httpRoutePlanner.determineRoute(
284                 ArgumentMatchers.eq(new HttpHost("localhost")),
285                 ArgumentMatchers.<HttpClientContext>any())).thenReturn(route);
286 
287         Mockito.when(chain.proceed(
288                 HttpRequestMatcher.matchesRequestUri(uri),
289                 ArgumentMatchers.any())).thenReturn(response1);
290         Mockito.when(chain.proceed(
291                 HttpRequestMatcher.matchesRequestUri(uri1),
292                 ArgumentMatchers.any())).thenReturn(response2);
293         Mockito.when(chain.proceed(
294                 HttpRequestMatcher.matchesRequestUri(uri2),
295                 ArgumentMatchers.any())).thenReturn(response3);
296 
297         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
298         Assertions.assertThrows(CircularRedirectException.class, () ->
299                 redirectExec.execute(request, scope, chain));
300     }
301 
302     @Test
303     public void testRedirectRuntimeException() throws Exception {
304         final HttpRoute route = new HttpRoute(target);
305         final HttpGet request = new HttpGet("/test");
306         final HttpClientContext context = HttpClientContext.create();
307 
308         final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
309         final URI redirect = new URI("http://localhost:80/redirect");
310         response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
311         Mockito.when(chain.proceed(
312                 ArgumentMatchers.same(request),
313                 ArgumentMatchers.any())).thenReturn(response1);
314         Mockito.doThrow(new RuntimeException("Oppsie")).when(redirectStrategy).getLocationURI(
315                 ArgumentMatchers.<ClassicHttpRequest>any(),
316                 ArgumentMatchers.<ClassicHttpResponse>any(),
317                 ArgumentMatchers.<HttpClientContext>any());
318 
319         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
320         Assertions.assertThrows(RuntimeException.class, () ->
321                 redirectExec.execute(request, scope, chain));
322         Mockito.verify(response1).close();
323     }
324 
325     @Test
326     public void testRedirectProtocolException() throws Exception {
327         final HttpRoute route = new HttpRoute(target);
328         final HttpGet request = new HttpGet("/test");
329         final HttpClientContext context = HttpClientContext.create();
330 
331         final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
332         final URI redirect = new URI("http://localhost:80/redirect");
333         response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
334         final InputStream inStream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
335         final HttpEntity entity1 = EntityBuilder.create()
336                 .setStream(inStream1)
337                 .build();
338         response1.setEntity(entity1);
339         Mockito.when(chain.proceed(
340                 ArgumentMatchers.same(request),
341                 ArgumentMatchers.any())).thenReturn(response1);
342         Mockito.doThrow(new ProtocolException("Oppsie")).when(redirectStrategy).getLocationURI(
343                 ArgumentMatchers.<ClassicHttpRequest>any(),
344                 ArgumentMatchers.<ClassicHttpResponse>any(),
345                 ArgumentMatchers.<HttpClientContext>any());
346 
347         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
348         Assertions.assertThrows(ProtocolException.class, () ->
349                 redirectExec.execute(request, scope, chain));
350         Mockito.verify(inStream1, Mockito.times(2)).close();
351         Mockito.verify(response1).close();
352     }
353 
354     private static class HttpRequestMatcher implements ArgumentMatcher<ClassicHttpRequest> {
355 
356         private final URI expectedRequestUri;
357 
358         HttpRequestMatcher(final URI requestUri) {
359             super();
360             this.expectedRequestUri = requestUri;
361         }
362 
363         @Override
364         public boolean matches(final ClassicHttpRequest argument) {
365             if (argument == null) {
366                 return false;
367             }
368             try {
369                 final URI requestUri = argument.getUri();
370                 return this.expectedRequestUri.equals(requestUri);
371             } catch (final URISyntaxException ex) {
372                 return false;
373             }
374         }
375 
376         static ClassicHttpRequest matchesRequestUri(final URI requestUri) {
377             return ArgumentMatchers.argThat(new HttpRequestMatcher(requestUri));
378         }
379 
380     }
381 
382 }