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.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 }