1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 }