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