HTTPCLIENT-1893: Moved HttpContext state logic out of DefaultRedirectStrategy to the redirect execution interceptors; redesigned RedirectLocations class; refactored classic redirect execution interceptor unit tests

This commit is contained in:
Oleg Kalnichevski 2018-01-03 19:36:04 +01:00
parent 71060c7ee1
commit 47a0eb8b65
10 changed files with 228 additions and 379 deletions

View File

@ -29,12 +29,12 @@ package org.apache.hc.client5.testing.sync;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import org.apache.hc.client5.http.HttpRequestRetryHandler;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.client5.http.utils.URIUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
@ -252,8 +252,8 @@ public class TestClientRequestExecution extends LocalServerTestBase {
final HttpRequest request = context.getRequest();
Assert.assertEquals("/stuff", request.getRequestUri());
final List<URI> redirectLocations = context.getRedirectLocations();
final URI location = URIUtils.resolve(uri, target, redirectLocations);
final RedirectLocations redirectLocations = context.getRedirectLocations();
final URI location = URIUtils.resolve(uri, target, redirectLocations.getAll());
Assert.assertEquals(uri, location);
}

View File

@ -30,7 +30,6 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import org.apache.hc.client5.http.CircularRedirectException;
import org.apache.hc.client5.http.ClientProtocolException;
@ -42,6 +41,7 @@ import org.apache.hc.client5.http.cookie.BasicCookieStore;
import org.apache.hc.client5.http.cookie.CookieStore;
import org.apache.hc.client5.http.impl.cookie.BasicClientCookie;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.client5.http.utils.URIUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
@ -282,8 +282,9 @@ public class TestRedirects extends LocalServerTestBase {
Assert.assertEquals(HttpStatus.SC_MULTIPLE_CHOICES, response.getCode());
Assert.assertEquals(URIUtils.create(target, "/oldlocation/"), reqWrapper.getUri());
final List<URI> redirects = context.getRedirectLocations();
Assert.assertNull(redirects);
final RedirectLocations redirects = context.getRedirectLocations();
Assert.assertNotNull(redirects);
Assert.assertEquals(0, redirects.size());
}
@Test
@ -304,7 +305,7 @@ public class TestRedirects extends LocalServerTestBase {
Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
Assert.assertEquals(URIUtils.create(target, "/newlocation/"), reqWrapper.getUri());
final List<URI> redirects = context.getRedirectLocations();
final RedirectLocations redirects = context.getRedirectLocations();
Assert.assertNotNull(redirects);
Assert.assertEquals(1, redirects.size());

View File

@ -30,13 +30,7 @@ package org.apache.hc.client5.http.impl;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.hc.client5.http.CircularRedirectException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.client5.http.protocol.RedirectStrategy;
import org.apache.hc.client5.http.utils.URIUtils;
import org.apache.hc.core5.annotation.Contract;
@ -52,8 +46,6 @@ import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.URIBuilder;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.TextUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Default implementation of {@link RedirectStrategy}. This strategy honors the restrictions
@ -64,27 +56,11 @@ import org.apache.logging.log4j.Logger;
*
* @since 4.1
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE)
@Contract(threading = ThreadingBehavior.STATELESS)
public class DefaultRedirectStrategy implements RedirectStrategy {
private final Logger log = LogManager.getLogger(getClass());
public static final DefaultRedirectStrategy INSTANCE = new DefaultRedirectStrategy();
private final ConcurrentMap<String, Boolean> safeMethods;
public DefaultRedirectStrategy(final String... safeMethods) {
super();
this.safeMethods = new ConcurrentHashMap<>();
for (final String safeMethod: safeMethods) {
this.safeMethods.put(safeMethod.toUpperCase(Locale.ROOT), Boolean.TRUE);
}
}
public DefaultRedirectStrategy() {
this("GET", "HEAD", "OPTIONS", "TRACE");
}
@Override
public boolean isRedirected(
final HttpRequest request,
@ -117,20 +93,12 @@ public class DefaultRedirectStrategy implements RedirectStrategy {
Args.notNull(response, "HTTP response");
Args.notNull(context, "HTTP context");
final HttpClientContext clientContext = HttpClientContext.adapt(context);
//get the location header to find out where to redirect to
final Header locationHeader = response.getFirstHeader("location");
if (locationHeader == null) {
throw new HttpException("Redirect location is missing");
}
final String location = locationHeader.getValue();
if (this.log.isDebugEnabled()) {
this.log.debug("Redirect requested to location '" + location + "'");
}
final RequestConfig config = clientContext.getRequestConfig();
URI uri = createLocationURI(location);
try {
if (!uri.isAbsolute()) {
@ -141,18 +109,6 @@ public class DefaultRedirectStrategy implements RedirectStrategy {
throw new ProtocolException(ex.getMessage(), ex);
}
RedirectLocations redirectLocations = (RedirectLocations) clientContext.getAttribute(
HttpClientContext.REDIRECT_LOCATIONS);
if (redirectLocations == null) {
redirectLocations = new RedirectLocations();
context.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
}
if (!config.isCircularRedirectsAllowed()) {
if (redirectLocations.contains(uri)) {
throw new CircularRedirectException("Circular redirect to '" + uri + "'");
}
}
redirectLocations.add(uri);
return uri;
}

View File

@ -28,8 +28,8 @@ package org.apache.hc.client5.http.impl.async;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import org.apache.hc.client5.http.CircularRedirectException;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.RedirectException;
import org.apache.hc.client5.http.StandardMethods;
@ -40,6 +40,7 @@ import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.AuthScheme;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.client5.http.protocol.RedirectStrategy;
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
import org.apache.hc.client5.http.utils.URIUtils;
@ -53,6 +54,7 @@ import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.message.BasicHttpRequest;
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
import org.apache.hc.core5.util.LangUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -75,6 +77,7 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
volatile int redirectCount;
volatile HttpRequest currentRequest;
volatile AsyncEntityProducer currentEntityProducer;
volatile RedirectLocations redirectLocations;
volatile AsyncExecChain.Scope currentScope;
volatile boolean reroute;
@ -107,6 +110,16 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
state.redirectCount++;
final URI redirectUri = redirectStrategy.getLocationURI(request, response, clientContext);
if (log.isDebugEnabled()) {
log.debug("Redirect requested to location '" + redirectUri + "'");
}
if (!config.isCircularRedirectsAllowed()) {
if (state.redirectLocations.contains(redirectUri)) {
throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'");
}
}
state.redirectLocations.add(redirectUri);
final int statusCode = response.getCode();
switch (statusCode) {
@ -128,27 +141,27 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
throw new ProtocolException("Redirect URI does not specify a valid host name: " + redirectUri);
}
if (!currentRoute.getTargetHost().equals(newTarget)) {
log.debug("New route required");
state.reroute = true;
final AuthExchange targetAuthExchange = clientContext.getAuthExchange(currentRoute.getTargetHost());
log.debug("Resetting target auth state");
targetAuthExchange.reset();
if (currentRoute.getProxyHost() != null) {
final AuthExchange proxyAuthExchange = clientContext.getAuthExchange(currentRoute.getProxyHost());
final AuthScheme authScheme = proxyAuthExchange.getAuthScheme();
if (authScheme != null && authScheme.isConnectionBased()) {
log.debug("Resetting proxy auth state");
proxyAuthExchange.reset();
}
}
state.reroute = false;
state.redirectURI = redirectUri;
if (!LangUtils.equals(currentRoute.getTargetHost(), newTarget)) {
final HttpRoute newRoute = routePlanner.determineRoute(newTarget, clientContext);
state.currentScope = new AsyncExecChain.Scope(scope.exchangeId, newRoute,
scope.originalRequest, scope.future, clientContext, scope.execRuntime);
state.redirectURI = redirectUri;
} else {
state.reroute = false;
state.redirectURI = redirectUri;
if (!LangUtils.equals(currentRoute, newRoute)) {
state.reroute = true;
final AuthExchange targetAuthExchange = clientContext.getAuthExchange(currentRoute.getTargetHost());
log.debug("Resetting target auth state");
targetAuthExchange.reset();
if (currentRoute.getProxyHost() != null) {
final AuthExchange proxyAuthExchange = clientContext.getAuthExchange(currentRoute.getProxyHost());
final AuthScheme authScheme = proxyAuthExchange.getAuthScheme();
if (authScheme != null && authScheme.isConnectionBased()) {
log.debug("Resetting proxy auth state");
proxyAuthExchange.reset();
}
}
state.currentScope = new AsyncExecChain.Scope(scope.exchangeId, newRoute,
scope.originalRequest, scope.future, clientContext, scope.execRuntime);
}
}
}
if (state.redirectURI != null) {
@ -200,10 +213,13 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
final AsyncExecChain chain,
final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
final HttpClientContext clientContext = scope.clientContext;
final List<URI> redirectLocations = clientContext.getRedirectLocations();
if (redirectLocations != null) {
redirectLocations.clear();
RedirectLocations redirectLocations = clientContext.getRedirectLocations();
if (redirectLocations == null) {
redirectLocations = new RedirectLocations();
clientContext.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
}
redirectLocations.clear();
final RequestConfig config = clientContext.getRequestConfig();
final State state = new State();
@ -211,6 +227,7 @@ class AsyncRedirectExec implements AsyncExecChainHandler {
state.redirectCount = 0;
state.currentRequest = request;
state.currentEntityProducer = entityProducer;
state.redirectLocations = redirectLocations;
state.currentScope = scope;
internalExecute(state, chain, asyncExecCallback);

View File

@ -29,8 +29,8 @@ package org.apache.hc.client5.http.impl.classic;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import org.apache.hc.client5.http.CircularRedirectException;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.RedirectException;
import org.apache.hc.client5.http.StandardMethods;
@ -42,6 +42,7 @@ import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.RequestBuilder;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.client5.http.protocol.RedirectStrategy;
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
import org.apache.hc.client5.http.utils.URIUtils;
@ -56,6 +57,7 @@ import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.LangUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -97,11 +99,12 @@ final class RedirectExec implements ExecChainHandler {
Args.notNull(scope, "Scope");
final HttpClientContext context = scope.clientContext;
final List<URI> redirectLocations = context.getRedirectLocations();
if (redirectLocations != null) {
redirectLocations.clear();
RedirectLocations redirectLocations = context.getRedirectLocations();
if (redirectLocations == null) {
redirectLocations = new RedirectLocations();
context.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
}
redirectLocations.clear();
final RequestConfig config = context.getRequestConfig();
final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
@ -122,6 +125,16 @@ final class RedirectExec implements ExecChainHandler {
redirectCount++;
final URI redirectUri = this.redirectStrategy.getLocationURI(currentRequest, response, context);
if (this.log.isDebugEnabled()) {
this.log.debug("Redirect requested to location '" + redirectUri + "'");
}
if (!config.isCircularRedirectsAllowed()) {
if (redirectLocations.contains(redirectUri)) {
throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'");
}
}
redirectLocations.add(redirectUri);
final ClassicHttpRequest originalRequest = scope.originalRequest;
ClassicHttpRequest redirect = null;
final int statusCode = response.getCode();
@ -147,28 +160,29 @@ final class RedirectExec implements ExecChainHandler {
redirectUri);
}
HttpRoute currentRoute = currentScope.route;
final boolean crossSiteRedirect = !currentRoute.getTargetHost().equals(newTarget);
if (crossSiteRedirect) {
final AuthExchange targetAuthExchange = context.getAuthExchange(currentRoute.getTargetHost());
this.log.debug("Resetting target auth state");
targetAuthExchange.reset();
if (currentRoute.getProxyHost() != null) {
final AuthExchange proxyAuthExchange = context.getAuthExchange(currentRoute.getProxyHost());
final AuthScheme authScheme = proxyAuthExchange.getAuthScheme();
if (authScheme != null && authScheme.isConnectionBased()) {
this.log.debug("Resetting proxy auth state");
proxyAuthExchange.reset();
final HttpRoute currentRoute = currentScope.route;
if (!LangUtils.equals(currentRoute.getTargetHost(), newTarget)) {
final HttpRoute newRoute = this.routePlanner.determineRoute(newTarget, context);
if (!LangUtils.equals(currentRoute, newRoute)) {
log.debug("New route required");
final AuthExchange targetAuthExchange = context.getAuthExchange(currentRoute.getTargetHost());
this.log.debug("Resetting target auth state");
targetAuthExchange.reset();
if (currentRoute.getProxyHost() != null) {
final AuthExchange proxyAuthExchange = context.getAuthExchange(currentRoute.getProxyHost());
final AuthScheme authScheme = proxyAuthExchange.getAuthScheme();
if (authScheme != null && authScheme.isConnectionBased()) {
this.log.debug("Resetting proxy auth state");
proxyAuthExchange.reset();
}
}
currentScope = new ExecChain.Scope(
currentScope.exchangeId,
newRoute,
currentScope.originalRequest,
currentScope.execRuntime,
currentScope.clientContext);
}
currentRoute = this.routePlanner.determineRoute(newTarget, context);
currentScope = new ExecChain.Scope(
currentScope.exchangeId,
currentRoute,
currentScope.originalRequest,
currentScope.execRuntime,
currentScope.clientContext);
}
if (this.log.isDebugEnabled()) {

View File

@ -27,9 +27,7 @@
package org.apache.hc.client5.http.protocol;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hc.client5.http.HttpRoute;
@ -67,7 +65,7 @@ public class HttpClientContext extends HttpCoreContext {
public static final String HTTP_ROUTE = "http.route";
/**
* Attribute name of a {@link List} object that represents a collection of all
* Attribute name of a {@link RedirectLocations} object that represents a collection of all
* redirect locations received in the process of request execution.
*/
public static final String REDIRECT_LOCATIONS = "http.protocol.redirect-locations";
@ -156,9 +154,8 @@ public class HttpClientContext extends HttpCoreContext {
return getAttribute(HTTP_ROUTE, HttpRoute.class);
}
@SuppressWarnings("unchecked")
public List<URI> getRedirectLocations() {
return getAttribute(REDIRECT_LOCATIONS, List.class);
public RedirectLocations getRedirectLocations() {
return getAttribute(REDIRECT_LOCATIONS, RedirectLocations.class);
}
public CookieStore getCookieStore() {

View File

@ -28,10 +28,8 @@
package org.apache.hc.client5.http.protocol;
import java.net.URI;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@ -41,7 +39,7 @@ import java.util.Set;
*
* @since 4.0
*/
public class RedirectLocations extends AbstractList<Object> {
public final class RedirectLocations {
private final Set<URI> unique;
private final List<URI> all;
@ -67,23 +65,6 @@ public class RedirectLocations extends AbstractList<Object> {
this.all.add(uri);
}
/**
* Removes a URI from the collection.
*/
public boolean remove(final URI uri) {
final boolean removed = this.unique.remove(uri);
if (removed) {
final Iterator<URI> it = this.all.iterator();
while (it.hasNext()) {
final URI current = it.next();
if (current.equals(uri)) {
it.remove();
}
}
}
return removed;
}
/**
* Returns all redirect {@link URI}s in the order they were added to the collection.
*
@ -106,7 +87,6 @@ public class RedirectLocations extends AbstractList<Object> {
* {@code index &lt; 0 || index &gt;= size()})
* @since 4.3
*/
@Override
public URI get(final int index) {
return this.all.get(index);
}
@ -119,106 +99,13 @@ public class RedirectLocations extends AbstractList<Object> {
* @return the number of elements in this list
* @since 4.3
*/
@Override
public int size() {
return this.all.size();
}
/**
* Replaces the URI at the specified position in this list with the
* specified element (must be a URI).
*
* @param index
* index of the element to replace
* @param element
* URI to be stored at the specified position
* @return the URI previously at the specified position
* @throws UnsupportedOperationException
* if the {@code set} operation is not supported by this list
* @throws ClassCastException
* if the element is not a {@link URI}
* @throws NullPointerException
* if the specified element is null and this list does not
* permit null elements
* @throws IndexOutOfBoundsException
* if the index is out of range (
* {@code index &lt; 0 || index &gt;= size()})
* @since 4.3
*/
@Override
public Object set(final int index, final Object element) {
final URI removed = this.all.set(index, (URI) element);
this.unique.remove(removed);
this.unique.add((URI) element);
if (this.all.size() != this.unique.size()) {
this.unique.addAll(this.all);
}
return removed;
}
/**
* Inserts the specified element at the specified position in this list
* (must be a URI). Shifts the URI currently at that position (if any) and
* any subsequent URIs to the right (adds one to their indices).
*
* @param index
* index at which the specified element is to be inserted
* @param element
* URI to be inserted
* @throws UnsupportedOperationException
* if the {@code add} operation is not supported by this list
* @throws ClassCastException
* if the element is not a {@link URI}
* @throws NullPointerException
* if the specified element is null and this list does not
* permit null elements
* @throws IndexOutOfBoundsException
* if the index is out of range (
* {@code index &lt; 0 || index &gt; size()})
* @since 4.3
*/
@Override
public void add(final int index, final Object element) {
this.all.add(index, (URI) element);
this.unique.add((URI) element);
}
/**
* Removes the URI at the specified position in this list. Shifts any
* subsequent URIs to the left (subtracts one from their indices). Returns
* the URI that was removed from the list.
*
* @param index
* the index of the URI to be removed
* @return the URI previously at the specified position
* @throws IndexOutOfBoundsException
* if the index is out of range (
* {@code index &lt; 0 || index &gt;= size()})
* @since 4.3
*/
@Override
public URI remove(final int index) {
final URI removed = this.all.remove(index);
this.unique.remove(removed);
if (this.all.size() != this.unique.size()) {
this.unique.addAll(this.all);
}
return removed;
}
/**
* Returns {@code true} if this collection contains the specified element.
* More formally, returns {@code true} if and only if this collection
* contains at least one element {@code e} such that
* {@code (o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))}.
*
* @param o element whose presence in this collection is to be tested
* @return {@code true} if this collection contains the specified
* element
*/
@Override
public boolean contains(final Object o) {
return this.unique.contains(o);
public void clear() {
unique.clear();
all.clear();
}
}

View File

@ -27,11 +27,9 @@
package org.apache.hc.client5.http.impl;
import java.net.URI;
import java.util.List;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
@ -200,53 +198,6 @@ public class TestDefaultRedirectStrategy {
Assert.assertEquals(URI.create("http://localhost/morestuff"), uri);
}
@Test
public void testGetLocationUriAllowCircularRedirects() throws Exception {
final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
final HttpClientContext context = HttpClientContext.create();
final RequestConfig config = RequestConfig.custom().setCircularRedirectsAllowed(true).build();
context.setRequestConfig(config);
final URI uri1 = URI.create("http://localhost/stuff1");
final URI uri2 = URI.create("http://localhost/stuff2");
final URI uri3 = URI.create("http://localhost/stuff3");
final HttpGet httpget1 = new HttpGet("http://localhost/");
final HttpResponse response1 = new BasicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY, "Redirect");
response1.addHeader("Location", uri1.toASCIIString());
final HttpGet httpget2 = new HttpGet(uri1.toASCIIString());
final HttpResponse response2 = new BasicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY, "Redirect");
response2.addHeader("Location", uri2.toASCIIString());
final HttpGet httpget3 = new HttpGet(uri2.toASCIIString());
final HttpResponse response3 = new BasicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY, "Redirect");
response3.addHeader("Location", uri3.toASCIIString());
Assert.assertEquals(uri1, redirectStrategy.getLocationURI(httpget1, response1, context));
Assert.assertEquals(uri2, redirectStrategy.getLocationURI(httpget2, response2, context));
Assert.assertEquals(uri3, redirectStrategy.getLocationURI(httpget3, response3, context));
final List<URI> uris = context.getRedirectLocations();
Assert.assertNotNull(uris);
Assert.assertTrue(uris.contains(uri1));
Assert.assertTrue(uris.contains(uri2));
Assert.assertTrue(uris.contains(uri3));
Assert.assertEquals(3, uris.size());
Assert.assertEquals(uri1, uris.get(0));
Assert.assertEquals(uri2, uris.get(1));
Assert.assertEquals(uri3, uris.get(2));
}
@Test(expected=ProtocolException.class)
public void testGetLocationUriDisallowCircularRedirects() throws Exception {
final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
final HttpClientContext context = HttpClientContext.create();
final HttpGet httpget = new HttpGet("http://localhost/stuff");
final RequestConfig config = RequestConfig.custom().setCircularRedirectsAllowed(false).build();
context.setRequestConfig(config);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY, "Redirect");
response.addHeader("Location", "http://localhost/stuff");
final URI uri = URI.create("http://localhost/stuff");
Assert.assertEquals(uri, redirectStrategy.getLocationURI(httpget, response, context));
redirectStrategy.getLocationURI(httpget, response, context);
}
@Test
public void testGetLocationUriInvalidInput() throws Exception {
final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

View File

@ -30,8 +30,10 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import org.apache.hc.client5.http.CircularRedirectException;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.RedirectException;
import org.apache.hc.client5.http.auth.AuthExchange;
@ -40,48 +42,52 @@ import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.auth.BasicScheme;
import org.apache.hc.client5.http.impl.auth.NTLMScheme;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.client5.http.protocol.RedirectStrategy;
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
@SuppressWarnings({"boxing","static-access"}) // test code
@RunWith(MockitoJUnitRunner.class)
public class TestRedirectExec {
@Mock
private HttpRoutePlanner httpRoutePlanner;
@Mock
private RedirectStrategy redirectStrategy;
@Mock
private ExecChain chain;
@Mock
private ExecRuntime endpoint;
private RedirectStrategy redirectStrategy;
private RedirectExec redirectExec;
private HttpHost target;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
redirectExec = new RedirectExec(httpRoutePlanner, redirectStrategy);
target = new HttpHost("localhost", 80);
redirectStrategy = Mockito.spy(new DefaultRedirectStrategy());
redirectExec = new RedirectExec(httpRoutePlanner, redirectStrategy);
}
@Test
@ -90,19 +96,20 @@ public class TestRedirectExec {
final HttpGet request = new HttpGet("/test");
final HttpClientContext context = HttpClientContext.create();
final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
final URI redirect = new URI("http://localhost:80/redirect");
response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
final HttpEntity entity1 = EntityBuilder.create()
.setStream(instream1)
.build();
Mockito.when(response1.getEntity()).thenReturn(entity1);
final ClassicHttpResponse response2 = Mockito.mock(ClassicHttpResponse.class);
response1.setEntity(entity1);
final ClassicHttpResponse response2 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_OK));
final InputStream instream2 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
final HttpEntity entity2 = EntityBuilder.create()
.setStream(instream2)
.build();
Mockito.when(response2.getEntity()).thenReturn(entity2);
final URI redirect = new URI("http://localhost:80/redirect");
response2.setEntity(entity2);
Mockito.when(chain.proceed(
Mockito.same(request),
@ -110,26 +117,12 @@ public class TestRedirectExec {
Mockito.when(chain.proceed(
HttpRequestMatcher.matchesRequestUri(redirect),
Mockito.<ExecChain.Scope>any())).thenReturn(response2);
Mockito.when(redirectStrategy.isRedirected(
Mockito.same(request),
Mockito.same(response1),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.when(redirectStrategy.getLocationURI(
Mockito.same(request),
Mockito.same(response1),
Mockito.<HttpClientContext>any())).thenReturn(redirect);
Mockito.when(httpRoutePlanner.determineRoute(
Mockito.eq(target),
Mockito.<HttpClientContext>any())).thenReturn(route);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
redirectExec.execute(request, scope, chain);
final ArgumentCaptor<ClassicHttpRequest> reqCaptor = ArgumentCaptor.forClass(
ClassicHttpRequest.class);
Mockito.verify(chain, Mockito.times(2)).proceed(
reqCaptor.capture(),
Mockito.same(scope));
final ArgumentCaptor<ClassicHttpRequest> reqCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(chain, Mockito.times(2)).proceed(reqCaptor.capture(), Mockito.same(scope));
final List<ClassicHttpRequest> allValues = reqCaptor.getAllValues();
Assert.assertNotNull(allValues);
@ -137,7 +130,7 @@ public class TestRedirectExec {
Assert.assertSame(request, allValues.get(0));
Mockito.verify(response1, Mockito.times(1)).close();
Mockito.verify(instream1, Mockito.times(1)).close();
Mockito.verify(instream1, Mockito.times(2)).close();
Mockito.verify(response2, Mockito.never()).close();
Mockito.verify(instream2, Mockito.never()).close();
}
@ -153,23 +146,11 @@ public class TestRedirectExec {
.build();
context.setRequestConfig(config);
final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
final URI redirect = new URI("http://localhost:80/redirect");
response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
Mockito.when(chain.proceed(
Mockito.<ClassicHttpRequest>any(),
Mockito.<ExecChain.Scope>any())).thenReturn(response1);
Mockito.when(redirectStrategy.isRedirected(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpResponse>any(),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.when(redirectStrategy.getLocationURI(
Mockito.<ClassicHttpRequest>any(),
Mockito.<HttpResponse>any(),
Mockito.<HttpClientContext>any())).thenReturn(redirect);
Mockito.when(httpRoutePlanner.determineRoute(
Mockito.eq(target),
Mockito.<HttpClientContext>any())).thenReturn(route);
Mockito.when(chain.proceed(Mockito.<ClassicHttpRequest>any(), Mockito.<ExecChain.Scope>any())).thenReturn(response1);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
redirectExec.execute(request, scope, chain);
@ -181,26 +162,12 @@ public class TestRedirectExec {
final HttpGet request = new HttpGet("/test");
final HttpClientContext context = HttpClientContext.create();
final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
final ClassicHttpResponse response2 = Mockito.mock(ClassicHttpResponse.class);
final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
final URI redirect = new URI("/redirect");
response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
Mockito.when(chain.proceed(
Mockito.same(request),
Mockito.<ExecChain.Scope>any())).thenReturn(response1);
Mockito.when(chain.proceed(
HttpRequestMatcher.matchesRequestUri(redirect),
Mockito.<ExecChain.Scope>any())).thenReturn(response2);
Mockito.when(redirectStrategy.isRedirected(
Mockito.same(request),
Mockito.same(response1),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.when(redirectStrategy.getLocationURI(
Mockito.same(request),
Mockito.same(response1),
Mockito.<HttpClientContext>any())).thenReturn(redirect);
Mockito.when(httpRoutePlanner.determineRoute(
Mockito.eq(target),
Mockito.<HttpClientContext>any())).thenReturn(route);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
redirectExec.execute(request, scope, chain);
@ -223,27 +190,17 @@ public class TestRedirectExec {
context.setAuthExchange(target, targetAuthExchange);
context.setAuthExchange(proxy, proxyAuthExchange);
final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
final ClassicHttpResponse response2 = Mockito.mock(ClassicHttpResponse.class);
final HttpHost otherHost = new HttpHost("otherhost", 8888);
final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
final URI redirect = new URI("http://otherhost:8888/redirect");
response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
final ClassicHttpResponse response2 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_OK));
final HttpHost otherHost = new HttpHost("otherhost", 8888);
Mockito.when(chain.proceed(
Mockito.same(request),
Mockito.<ExecChain.Scope>any())).thenReturn(response1);
Mockito.when(chain.proceed(
HttpRequestMatcher.matchesRequestUri(redirect),
Mockito.<ExecChain.Scope>any())).thenReturn(response2);
Mockito.when(redirectStrategy.isRedirected(
Mockito.same(request),
Mockito.same(response1),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.when(redirectStrategy.getLocationURI(
Mockito.same(request),
Mockito.same(response1),
Mockito.<HttpClientContext>any())).thenReturn(redirect);
Mockito.when(httpRoutePlanner.determineRoute(
Mockito.eq(target),
Mockito.<HttpClientContext>any())).thenReturn(new HttpRoute(target));
Mockito.when(httpRoutePlanner.determineRoute(
Mockito.eq(otherHost),
Mockito.<HttpClientContext>any())).thenReturn(new HttpRoute(otherHost));
@ -261,24 +218,101 @@ public class TestRedirectExec {
Assert.assertEquals(null, authExchange2.getAuthScheme());
}
@Test
public void testAllowCircularRedirects() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = HttpClientContext.create();
context.setRequestConfig(RequestConfig.custom()
.setCircularRedirectsAllowed(true)
.build());
final URI uri = URI.create("http://localhost/");
final HttpGet request = new HttpGet(uri);
final URI uri1 = URI.create("http://localhost/stuff1");
final URI uri2 = URI.create("http://localhost/stuff2");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
response1.addHeader("Location", uri1.toASCIIString());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
response2.addHeader("Location", uri2.toASCIIString());
final ClassicHttpResponse response3 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
response3.addHeader("Location", uri1.toASCIIString());
final ClassicHttpResponse response4 = new BasicClassicHttpResponse(HttpStatus.SC_OK);
Mockito.when(chain.proceed(
HttpRequestMatcher.matchesRequestUri(uri),
Mockito.<ExecChain.Scope>any())).thenReturn(response1);
Mockito.when(chain.proceed(
HttpRequestMatcher.matchesRequestUri(uri1),
Mockito.<ExecChain.Scope>any())).thenReturn(response2, response4);
Mockito.when(chain.proceed(
HttpRequestMatcher.matchesRequestUri(uri2),
Mockito.<ExecChain.Scope>any())).thenReturn(response3);
Mockito.when(httpRoutePlanner.determineRoute(
Mockito.eq(new HttpHost("localhost")),
Mockito.<HttpClientContext>any())).thenReturn(route);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
redirectExec.execute(request, scope, chain);
final RedirectLocations uris = context.getRedirectLocations();
Assert.assertNotNull(uris);
Assert.assertEquals(Arrays.asList(uri1, uri2, uri1), uris.getAll());
}
@Test(expected=CircularRedirectException.class)
public void testGetLocationUriDisallowCircularRedirects() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpClientContext context = HttpClientContext.create();
context.setRequestConfig(RequestConfig.custom()
.setCircularRedirectsAllowed(false)
.build());
final URI uri = URI.create("http://localhost/");
final HttpGet request = new HttpGet(uri);
final URI uri1 = URI.create("http://localhost/stuff1");
final URI uri2 = URI.create("http://localhost/stuff2");
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
response1.addHeader("Location", uri1.toASCIIString());
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
response2.addHeader("Location", uri2.toASCIIString());
final ClassicHttpResponse response3 = new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY);
response3.addHeader("Location", uri1.toASCIIString());
Mockito.when(httpRoutePlanner.determineRoute(
Mockito.eq(new HttpHost("localhost")),
Mockito.<HttpClientContext>any())).thenReturn(route);
Mockito.when(chain.proceed(
HttpRequestMatcher.matchesRequestUri(uri),
Mockito.<ExecChain.Scope>any())).thenReturn(response1);
Mockito.when(chain.proceed(
HttpRequestMatcher.matchesRequestUri(uri1),
Mockito.<ExecChain.Scope>any())).thenReturn(response2);
Mockito.when(chain.proceed(
HttpRequestMatcher.matchesRequestUri(uri2),
Mockito.<ExecChain.Scope>any())).thenReturn(response3);
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
redirectExec.execute(request, scope, chain);
}
@Test(expected = RuntimeException.class)
public void testRedirectRuntimeException() throws Exception {
final HttpRoute route = new HttpRoute(target);
final HttpGet request = new HttpGet("/test");
final HttpClientContext context = HttpClientContext.create();
final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
final URI redirect = new URI("http://localhost:80/redirect");
response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
Mockito.when(chain.proceed(
Mockito.same(request),
Mockito.<ExecChain.Scope>any())).thenReturn(response1);
Mockito.when(redirectStrategy.isRedirected(
Mockito.same(request),
Mockito.same(response1),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.doThrow(new RuntimeException("Oppsie")).when(redirectStrategy.getLocationURI(
Mockito.same(request),
Mockito.same(response1),
Mockito.<HttpClientContext>any()));
Mockito.doThrow(new RuntimeException("Oppsie")).when(redirectStrategy).getLocationURI(
Mockito.<ClassicHttpRequest>any(),
Mockito.<ClassicHttpResponse>any(),
Mockito.<HttpClientContext>any());
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
try {
@ -295,29 +329,27 @@ public class TestRedirectExec {
final HttpGet request = new HttpGet("/test");
final HttpClientContext context = HttpClientContext.create();
final ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class);
final ClassicHttpResponse response1 = Mockito.spy(new BasicClassicHttpResponse(HttpStatus.SC_MOVED_TEMPORARILY));
final URI redirect = new URI("http://localhost:80/redirect");
response1.setHeader(HttpHeaders.LOCATION, redirect.toASCIIString());
final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
final HttpEntity entity1 = EntityBuilder.create()
.setStream(instream1)
.build();
Mockito.when(response1.getEntity()).thenReturn(entity1);
response1.setEntity(entity1);
Mockito.when(chain.proceed(
Mockito.same(request),
Mockito.<ExecChain.Scope>any())).thenReturn(response1);
Mockito.when(redirectStrategy.isRedirected(
Mockito.same(request),
Mockito.same(response1),
Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
Mockito.doThrow(new ProtocolException("Oppsie")).when(redirectStrategy).getLocationURI(
Mockito.same(request),
Mockito.same(response1),
Mockito.<ClassicHttpRequest>any(),
Mockito.<ClassicHttpResponse>any(),
Mockito.<HttpClientContext>any());
final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, endpoint, context);
try {
redirectExec.execute(request, scope, chain);
} catch (final Exception ex) {
Mockito.verify(instream1).close();
Mockito.verify(instream1, Mockito.times(2)).close();
Mockito.verify(response1).close();
throw ex;
}
@ -325,17 +357,21 @@ public class TestRedirectExec {
private static class HttpRequestMatcher implements ArgumentMatcher<ClassicHttpRequest> {
private final URI requestUri;
private final URI expectedRequestUri;
HttpRequestMatcher(final URI requestUri) {
super();
this.requestUri = requestUri;
this.expectedRequestUri = requestUri;
}
@Override
public boolean matches(final ClassicHttpRequest argument) {
if (argument == null) {
return false;
}
try {
return requestUri.equals(argument.getUri());
final URI requestUri = argument.getUri();
return this.expectedRequestUri.equals(requestUri);
} catch (final URISyntaxException ex) {
return false;
}

View File

@ -56,7 +56,7 @@ public class TestRedirectLocation {
Assert.assertTrue(locations.contains(uri3));
Assert.assertFalse(locations.contains(new URI("/")));
List<URI> list = locations.getAll();
final List<URI> list = locations.getAll();
Assert.assertNotNull(list);
Assert.assertEquals(5, list.size());
Assert.assertEquals(uri1, list.get(0));
@ -64,16 +64,6 @@ public class TestRedirectLocation {
Assert.assertEquals(uri2, list.get(2));
Assert.assertEquals(uri3, list.get(3));
Assert.assertEquals(uri3, list.get(4));
Assert.assertTrue(locations.remove(uri3));
Assert.assertTrue(locations.remove(uri1));
Assert.assertFalse(locations.remove(new URI("/")));
list = locations.getAll();
Assert.assertNotNull(list);
Assert.assertEquals(2, list.size());
Assert.assertEquals(uri2, list.get(0));
Assert.assertEquals(uri2, list.get(1));
}
}