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