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.Assert;
62  import org.junit.Before;
63  import org.junit.Test;
64  import org.junit.runner.RunWith;
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.junit.MockitoJUnitRunner;
71  
72  @RunWith(MockitoJUnitRunner.class)
73  public class TestRedirectExec {
74  
75      @Mock
76      private HttpRoutePlanner httpRoutePlanner;
77      @Mock
78      private ExecChain chain;
79      @Mock
80      private ExecRuntime endpoint;
81  
82      private RedirectStrategy redirectStrategy;
83      private RedirectExec redirectExec;
84      private HttpHost target;
85  
86      @Before
87      public void setup() throws Exception {
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.<ExecChain.Scope>any())).thenReturn(response1);
117         Mockito.when(chain.proceed(
118                 HttpRequestMatcher.matchesRequestUri(redirect),
119                 ArgumentMatchers.<ExecChain.Scope>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         Assert.assertNotNull(allValues);
129         Assert.assertEquals(2, allValues.size());
130         Assert.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(expected = RedirectException.class)
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.<ClassicHttpRequest>any(), ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response1);
154 
155         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
156         redirectExec.execute(request, scope, chain);
157     }
158 
159     @Test(expected = HttpException.class)
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.<ExecChain.Scope>any())).thenReturn(response1);
171 
172         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
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 NTLMScheme());
190         context.setAuthExchange(target, targetAuthExchange);
191         context.setAuthExchange(proxy, proxyAuthExchange);
192 
193         final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
194         final URI redirect = new URI("http://otherhost:8888/redirect");
195         response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
196         final ClassicHttpResponse response2 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_OK));
197         final HttpHost otherHost = new HttpHost("otherhost", 8888);
198         Mockito.when(chain.proceed(
199                 ArgumentMatchers.same(request),
200                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response1);
201         Mockito.when(chain.proceed(
202                 HttpRequestMatcher.matchesRequestUri(redirect),
203                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response2);
204         Mockito.when(httpRoutePlanner.determineRoute(
205                 ArgumentMatchers.eq(otherHost),
206                 ArgumentMatchers.<HttpClientContext>any())).thenReturn(new HttpRoute(otherHost));
207 
208         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
209         redirectExec.execute(request, scope, chain);
210 
211         final AuthExchange authExchange1 = context.getAuthExchange(target);
212         Assert.assertNotNull(authExchange1);
213         Assert.assertEquals(AuthExchange.State.UNCHALLENGED, authExchange1.getState());
214         Assert.assertEquals(null, authExchange1.getAuthScheme());
215         final AuthExchange authExchange2 = context.getAuthExchange(proxy);
216         Assert.assertNotNull(authExchange2);
217         Assert.assertEquals(AuthExchange.State.UNCHALLENGED, authExchange2.getState());
218         Assert.assertEquals(null, authExchange2.getAuthScheme());
219     }
220 
221     @Test
222     public void testAllowCircularRedirects() throws Exception {
223         final HttpRoute route = new HttpRoute(target);
224         final HttpClientContext context = HttpClientContext.create();
225         context.setRequestConfig(RequestConfig.custom()
226                 .setCircularRedirectsAllowed(true)
227                 .build());
228 
229         final URI uri = URI.create("http://localhost/");
230         final HttpGet request = new HttpGet(uri);
231 
232         final URI uri1 = URI.create("http://localhost/stuff1");
233         final URI uri2 = URI.create("http://localhost/stuff2");
234         final ClassicHttpResponse response1 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
235         response1.addHeader("Location", uri1.toASCIIString());
236         final ClassicHttpResponse response2 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
237         response2.addHeader("Location", uri2.toASCIIString());
238         final ClassicHttpResponse response3 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
239         response3.addHeader("Location", uri1.toASCIIString());
240         final ClassicHttpResponse response4 = new BasicClassicHttpResponse(HttpStatus.SC_OK);
241 
242         Mockito.when(chain.proceed(
243                 HttpRequestMatcher.matchesRequestUri(uri),
244                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response1);
245         Mockito.when(chain.proceed(
246                 HttpRequestMatcher.matchesRequestUri(uri1),
247                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response2, response4);
248         Mockito.when(chain.proceed(
249                 HttpRequestMatcher.matchesRequestUri(uri2),
250                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response3);
251         Mockito.when(httpRoutePlanner.determineRoute(
252                 ArgumentMatchers.eq(new HttpHost("localhost")),
253                 ArgumentMatchers.<HttpClientContext>any())).thenReturn(route);
254 
255         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
256         redirectExec.execute(request, scope, chain);
257 
258         final RedirectLocations uris = context.getRedirectLocations();
259         Assert.assertNotNull(uris);
260         Assert.assertEquals(Arrays.asList(uri1, uri2, uri1), uris.getAll());
261     }
262 
263     @Test(expected=CircularRedirectException.class)
264     public void testGetLocationUriDisallowCircularRedirects() throws Exception {
265         final HttpRoute route = new HttpRoute(target);
266         final HttpClientContext context = HttpClientContext.create();
267         context.setRequestConfig(RequestConfig.custom()
268                 .setCircularRedirectsAllowed(false)
269                 .build());
270 
271         final URI uri = URI.create("http://localhost/");
272         final HttpGet request = new HttpGet(uri);
273 
274         final URI uri1 = URI.create("http://localhost/stuff1");
275         final URI uri2 = URI.create("http://localhost/stuff2");
276         final ClassicHttpResponse response1 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
277         response1.addHeader("Location", uri1.toASCIIString());
278         final ClassicHttpResponse response2 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
279         response2.addHeader("Location", uri2.toASCIIString());
280         final ClassicHttpResponse response3 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
281         response3.addHeader("Location", uri1.toASCIIString());
282         Mockito.when(httpRoutePlanner.determineRoute(
283                 ArgumentMatchers.eq(new HttpHost("localhost")),
284                 ArgumentMatchers.<HttpClientContext>any())).thenReturn(route);
285 
286         Mockito.when(chain.proceed(
287                 HttpRequestMatcher.matchesRequestUri(uri),
288                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response1);
289         Mockito.when(chain.proceed(
290                 HttpRequestMatcher.matchesRequestUri(uri1),
291                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response2);
292         Mockito.when(chain.proceed(
293                 HttpRequestMatcher.matchesRequestUri(uri2),
294                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response3);
295 
296         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
297         redirectExec.execute(request, scope, chain);
298     }
299 
300     @Test(expected = RuntimeException.class)
301     public void testRedirectRuntimeException() throws Exception {
302         final HttpRoute route = new HttpRoute(target);
303         final HttpGet request = new HttpGet("/test");
304         final HttpClientContext context = HttpClientContext.create();
305 
306         final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
307         final URI redirect = new URI("http://localhost:80/redirect");
308         response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
309         Mockito.when(chain.proceed(
310                 ArgumentMatchers.same(request),
311                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response1);
312         Mockito.doThrow(new RuntimeException("Oppsie")).when(redirectStrategy).getLocationURI(
313                 ArgumentMatchers.<ClassicHttpRequest>any(),
314                 ArgumentMatchers.<ClassicHttpResponse>any(),
315                 ArgumentMatchers.<HttpClientContext>any());
316 
317         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
318         try {
319             redirectExec.execute(request, scope, chain);
320         } catch (final Exception ex) {
321             Mockito.verify(response1).close();
322             throw ex;
323         }
324     }
325 
326     @Test(expected = ProtocolException.class)
327     public void testRedirectProtocolException() throws Exception {
328         final HttpRoute route = new HttpRoute(target);
329         final HttpGet request = new HttpGet("/test");
330         final HttpClientContext context = HttpClientContext.create();
331 
332         final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
333         final URI redirect = new URI("http://localhost:80/redirect");
334         response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
335         final InputStream inStream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
336         final HttpEntity entity1 = EntityBuilder.create()
337                 .setStream(inStream1)
338                 .build();
339         response1.setEntity(entity1);
340         Mockito.when(chain.proceed(
341                 ArgumentMatchers.same(request),
342                 ArgumentMatchers.<ExecChain.Scope>any())).thenReturn(response1);
343         Mockito.doThrow(new ProtocolException("Oppsie")).when(redirectStrategy).getLocationURI(
344                 ArgumentMatchers.<ClassicHttpRequest>any(),
345                 ArgumentMatchers.<ClassicHttpResponse>any(),
346                 ArgumentMatchers.<HttpClientContext>any());
347 
348         final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
349         try {
350             redirectExec.execute(request, scope, chain);
351         } catch (final Exception ex) {
352             Mockito.verify(inStream1, Mockito.times(2)).close();
353             Mockito.verify(response1).close();
354             throw ex;
355         }
356     }
357 
358     private static class HttpRequestMatcher implements ArgumentMatcher<ClassicHttpRequest> {
359 
360         private final URI expectedRequestUri;
361 
362         HttpRequestMatcher(final URI requestUri) {
363             super();
364             this.expectedRequestUri = requestUri;
365         }
366 
367         @Override
368         public boolean matches(final ClassicHttpRequest argument) {
369             if (argument == null) {
370                 return false;
371             }
372             try {
373                 final URI requestUri = argument.getUri();
374                 return this.expectedRequestUri.equals(requestUri);
375             } catch (final URISyntaxException ex) {
376                 return false;
377             }
378         }
379 
380         static ClassicHttpRequest matchesRequestUri(final URI requestUri) {
381             return ArgumentMatchers.argThat(new HttpRequestMatcher(requestUri));
382         }
383 
384     }
385 
386 }