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