diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java index 88f73b21a..5de45111b 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java @@ -26,14 +26,19 @@ */ package org.apache.hc.client5.testing.async; +import java.util.Arrays; import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthSchemeFactory; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.CredentialsProvider; @@ -42,6 +47,7 @@ import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.auth.BasicAuthCache; import org.apache.hc.client5.http.impl.auth.BasicScheme; import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder; import org.apache.hc.client5.http.protocol.HttpClientContext; @@ -52,7 +58,9 @@ 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.HttpRequestInterceptor; import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpResponseInterceptor; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http.URIScheme; @@ -64,9 +72,12 @@ import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.http.support.BasicResponseBuilder; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.http2.impl.H2Processors; import org.apache.hc.core5.net.URIAuthority; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -104,6 +115,10 @@ public final HttpHost start( abstract void setTargetAuthenticationStrategy(AuthenticationStrategy targetAuthStrategy); + abstract void addResponseInterceptor(HttpResponseInterceptor responseInterceptor); + + abstract void addRequestInterceptor(final HttpRequestInterceptor requestInterceptor); + @Test public void testBasicAuthenticationNoCreds() throws Exception { server.register("*", AsyncEchoHandler::new); @@ -270,6 +285,69 @@ public void testBasicAuthenticationCredentialsCaching() throws Exception { Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any()); } + @Test + public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception { + server.register("*", AsyncEchoHandler::new); + + final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy()); + setTargetAuthenticationStrategy(authStrategy); + final Queue responseQueue = new ConcurrentLinkedQueue<>(); + addResponseInterceptor((response, entity, context) + -> responseQueue.add(BasicResponseBuilder.copy(response).build())); + + final HttpHost target = start(); + + final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create() + .add(target, "test", "test".toCharArray()) + .build(); + + final AuthCache authCache = new BasicAuthCache(); + + for (final String requestPath: new String[] {"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) { + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(authCache); + context.setCredentialsProvider(credentialsProvider); + final Future future = httpclient.execute(SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath(requestPath) + .build(), context, null); + final HttpResponse response = future.get(); + Assert.assertNotNull(response); + Assert.assertEquals(HttpStatus.SC_OK, response.getCode()); + } + + // There should be only single auth strategy call for all successful message exchanges + Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any()); + + MatcherAssert.assertThat( + responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()), + CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200))); + + responseQueue.clear(); + authCache.clear(); + Mockito.reset(authStrategy); + + for (final String requestPath: new String[] {"/blah/a", "/yada/a", "/blah/blah/"}) { + final HttpClientContext context = HttpClientContext.create(); + context.setCredentialsProvider(credentialsProvider); + context.setAuthCache(authCache); + final Future future = httpclient.execute(SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath(requestPath) + .build(), context, null); + final HttpResponse response = future.get(); + Assert.assertNotNull(response); + Assert.assertEquals(HttpStatus.SC_OK, response.getCode()); + } + + // There should be an auth strategy call for all successful message exchanges + Mockito.verify(authStrategy, Mockito.times(3)).select(Mockito.any(), Mockito.any(), Mockito.any()); + + MatcherAssert.assertThat( + responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()), + CoreMatchers.equalTo(Arrays.asList(401, 200, 401, 200, 401, 200))); + } + @Test public void testAuthenticationUserinfoInRequestSuccess() throws Exception { server.register("*", AsyncEchoHandler::new); diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java index 52adf5a65..94b6766bf 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java @@ -38,6 +38,8 @@ import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.client5.testing.SSLTestContexts; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.HttpResponseInterceptor; import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.config.Lookup; @@ -92,6 +94,16 @@ void setTargetAuthenticationStrategy(final AuthenticationStrategy targetAuthStra clientBuilder.setTargetAuthenticationStrategy(targetAuthStrategy); } + @Override + void addResponseInterceptor(final HttpResponseInterceptor responseInterceptor) { + clientBuilder.addResponseInterceptorLast(responseInterceptor); + } + + @Override + void addRequestInterceptor(final HttpRequestInterceptor requestInterceptor) { + clientBuilder.addRequestInterceptorLast(requestInterceptor); + } + @Override protected CloseableHttpAsyncClient createClient() throws Exception { return clientBuilder.build(); diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java index d8ff12948..145923766 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java @@ -51,7 +51,9 @@ import org.apache.hc.core5.http.HeaderElements; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequestInterceptor; import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpResponseInterceptor; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http.URIScheme; @@ -132,6 +134,16 @@ void setTargetAuthenticationStrategy(final AuthenticationStrategy targetAuthStra clientBuilder.setTargetAuthenticationStrategy(targetAuthStrategy); } + @Override + void addResponseInterceptor(final HttpResponseInterceptor responseInterceptor) { + clientBuilder.addResponseInterceptorLast(responseInterceptor); + } + + @Override + void addRequestInterceptor(final HttpRequestInterceptor requestInterceptor) { + clientBuilder.addRequestInterceptorLast(requestInterceptor); + } + @Override protected CloseableHttpAsyncClient createClient() throws Exception { return clientBuilder.build(); diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java index 1e88a46b5..0af41e11c 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java @@ -30,8 +30,12 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthScheme; @@ -63,6 +67,7 @@ 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.config.Registry; import org.apache.hc.core5.http.config.RegistryBuilder; @@ -74,7 +79,10 @@ import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.http.support.BasicResponseBuilder; import org.apache.hc.core5.net.URIAuthority; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -267,6 +275,9 @@ public void testBasicAuthenticationCredentialsCaching() throws Exception { this.server.registerHandler("*", new EchoHandler()); final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy()); this.clientBuilder.setTargetAuthenticationStrategy(authStrategy); + final Queue responseQueue = new ConcurrentLinkedQueue<>(); + this.clientBuilder.addResponseInterceptorLast((response, entity, context) + -> responseQueue.add(BasicResponseBuilder.copy(response).build())); final HttpHost target = start(); @@ -286,6 +297,69 @@ public void testBasicAuthenticationCredentialsCaching() throws Exception { } Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any()); + + MatcherAssert.assertThat( + responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()), + CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200, 200))); + } + + @Test + public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception { + this.server.registerHandler("*", new EchoHandler()); + final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy()); + this.clientBuilder.setTargetAuthenticationStrategy(authStrategy); + final Queue responseQueue = new ConcurrentLinkedQueue<>(); + this.clientBuilder.addResponseInterceptorLast((response, entity, context) + -> responseQueue.add(BasicResponseBuilder.copy(response).build())); + + final HttpHost target = start(); + + final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create() + .add(target, "test", "test".toCharArray()) + .build(); + + final AuthCache authCache = new BasicAuthCache(); + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(authCache); + context.setCredentialsProvider(credentialsProvider); + + for (final String requestPath: new String[] {"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) { + final HttpGet httpget = new HttpGet(requestPath); + try (final ClassicHttpResponse response = this.httpclient.execute(target, httpget, context)) { + final HttpEntity entity1 = response.getEntity(); + Assert.assertEquals(HttpStatus.SC_OK, response.getCode()); + Assert.assertNotNull(entity1); + EntityUtils.consume(entity1); + } + } + + // There should be only single auth strategy call for all successful message exchanges + Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any()); + + MatcherAssert.assertThat( + responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()), + CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200))); + + responseQueue.clear(); + authCache.clear(); + Mockito.reset(authStrategy); + + for (final String requestPath: new String[] {"/blah/a", "/yada/a", "/blah/blah/", "/buh/a"}) { + final HttpGet httpget = new HttpGet(requestPath); + try (final ClassicHttpResponse response = this.httpclient.execute(target, httpget, context)) { + final HttpEntity entity1 = response.getEntity(); + Assert.assertEquals(HttpStatus.SC_OK, response.getCode()); + Assert.assertNotNull(entity1); + EntityUtils.consume(entity1); + } + } + + // There should be an auth strategy call for all successful message exchanges + Mockito.verify(authStrategy, Mockito.times(2)).select(Mockito.any(), Mockito.any(), Mockito.any()); + + MatcherAssert.assertThat( + responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()), + CoreMatchers.equalTo(Arrays.asList(200, 401, 200, 200, 401, 200))); } @Test diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java b/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java index 26ddb4225..f57e019e7 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java @@ -29,6 +29,7 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.net.NamedEndpoint; /** * Strategy for default port resolution for protocol schemes. @@ -43,4 +44,13 @@ public interface SchemePortResolver { */ int resolve(HttpHost host); + /** + * Returns the actual port for the host based on the protocol scheme. + * + * @since 5.2 + */ + default int resolve(String scheme, NamedEndpoint endpoint) { + return resolve(new HttpHost(scheme, endpoint)); + } + } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java index f566b392c..e13bde7ea 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java @@ -36,12 +36,73 @@ */ public interface AuthCache { + /** + * Stores the authentication state with the given authentication scope in the cache. + * + * @param host the authentication authority. + * @param authScheme the cacheable authentication state. + */ void put(HttpHost host, AuthScheme authScheme); + /** + * Returns the authentication state with the given authentication scope from the cache + * if available. + * + * @param host the authentication authority. + * @return the authentication state ir {@code null} if not available in the cache. + */ AuthScheme get(HttpHost host); + /** + * Removes the authentication state with the given authentication scope from the cache + * if found. + * + * @param host the authentication authority. + */ void remove(HttpHost host); void clear(); + /** + * Stores the authentication state with the given authentication scope in the cache. + * + * @param host the authentication authority. + * @param pathPrefix the path prefix (the path component up to the last segment separator). + * Can be {@code null}. + * @param authScheme the cacheable authentication state. + * + * @since 5.2 + */ + default void put(HttpHost host, String pathPrefix, AuthScheme authScheme) { + put(host, authScheme); + } + + /** + * Returns the authentication state with the given authentication scope from the cache + * if available. + * @param host the authentication authority. + * @param pathPrefix the path prefix (the path component up to the last segment separator). + * Can be {@code null}. + * @return the authentication state ir {@code null} if not available in the cache. + * + * @since 5.2 + */ + default AuthScheme get(HttpHost host, String pathPrefix) { + return get(host); + } + + /** + * Removes the authentication state with the given authentication scope from the cache + * if found. + * + * @param host the authentication authority. + * @param pathPrefix the path prefix (the path component up to the last segment separator). + * Can be {@code null}. + * + * @since 5.2 + */ + default void remove(HttpHost host, String pathPrefix) { + remove(host); + } + } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java index 4709149f7..2aaf1fb66 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java @@ -47,6 +47,7 @@ public enum State { private State state; private AuthScheme authScheme; private Queue authOptions; + private String pathPrefix; public AuthExchange() { super(); @@ -57,6 +58,7 @@ public void reset() { this.state = State.UNCHALLENGED; this.authOptions = null; this.authScheme = null; + this.pathPrefix = null; } public State getState() { @@ -81,6 +83,20 @@ public boolean isConnectionBased() { return this.authScheme != null && this.authScheme.isConnectionBased(); } + /** + * @since 5.2 + */ + public String getPathPrefix() { + return pathPrefix; + } + + /** + * @since 5.2 + */ + public void setPathPrefix(final String pathPrefix) { + this.pathPrefix = pathPrefix; + } + /** * Resets the auth state with {@link AuthScheme} and clears auth options. * diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java index ca0fdd408..a25f563be 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java @@ -39,8 +39,8 @@ import org.apache.hc.client5.http.auth.AuthChallenge; import org.apache.hc.client5.http.auth.AuthScheme; import org.apache.hc.client5.http.auth.AuthSchemeFactory; -import org.apache.hc.client5.http.auth.StandardAuthScheme; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.StandardAuthScheme; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; @@ -117,7 +117,7 @@ public List select( options.add(authScheme); } else { if (LOG.isDebugEnabled()) { - LOG.debug("{}, Challenge for {} authentication scheme not available", exchangeId, schemeName); + LOG.debug("{} Challenge for {} authentication scheme not available", exchangeId, schemeName); } } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java index 144ae5f7f..c65e65d95 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java @@ -31,6 +31,7 @@ import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.net.NamedEndpoint; import org.apache.hc.core5.util.Args; /** @@ -46,14 +47,19 @@ public class DefaultSchemePortResolver implements SchemePortResolver { @Override public int resolve(final HttpHost host) { Args.notNull(host, "HTTP host"); - final int port = host.getPort(); + return resolve(host.getSchemeName(), host); + } + + @Override + public int resolve(final String scheme, final NamedEndpoint endpoint) { + Args.notNull(endpoint, "Endpoint"); + final int port = endpoint.getPort(); if (port > 0) { return port; } - final String name = host.getSchemeName(); - if (URIScheme.HTTP.same(name)) { + if (URIScheme.HTTP.same(scheme)) { return 80; - } else if (URIScheme.HTTPS.same(name)) { + } else if (URIScheme.HTTPS.same(scheme)) { return 443; } else { return -1; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java similarity index 51% rename from httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSupport.java rename to httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java index 70f820ce0..50e879f38 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSupport.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java @@ -26,45 +26,48 @@ */ package org.apache.hc.client5.http.impl; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.List; + import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.URIScheme; -import org.apache.hc.core5.net.URIAuthority; +import org.apache.hc.core5.net.PercentCodec; +import org.apache.hc.core5.net.URIBuilder; /** - * Protocol support methods. + * Protocol support methods. For internal use only. * - * @since 5.1 + * @since 5.2 */ @Internal -public final class ProtocolSupport { +public final class RequestSupport { - public static String getRequestUri(final HttpRequest request) { - final URIAuthority authority = request.getAuthority(); - if (authority != null) { - final StringBuilder buf = new StringBuilder(); - final String scheme = request.getScheme(); - buf.append(scheme != null ? scheme : URIScheme.HTTP.id); - buf.append("://"); - if (authority.getUserInfo() != null) { - buf.append(authority.getUserInfo()); - buf.append("@"); + public static String extractPathPrefix(final HttpRequest request) { + final String path = request.getPath(); + try { + final URIBuilder uriBuilder = new URIBuilder(path); + uriBuilder.setFragment(null); + uriBuilder.clearParameters(); + uriBuilder.normalizeSyntax(); + final List pathSegments = uriBuilder.getPathSegments(); + + if (!pathSegments.isEmpty()) { + pathSegments.remove(pathSegments.size() - 1); } - buf.append(authority.getHostName()); - if (authority.getPort() != -1) { - buf.append(":"); - buf.append(authority.getPort()); + if (pathSegments.isEmpty()) { + return "/"; + } else { + final StringBuilder buf = new StringBuilder(); + buf.append('/'); + for (final String pathSegment : pathSegments) { + PercentCodec.encode(buf, pathSegment, StandardCharsets.US_ASCII); + buf.append('/'); + } + return buf.toString(); } - final String path = request.getPath(); - if (path == null || !path.startsWith("/")) { - buf.append("/"); - } - if (path != null) { - buf.append(path); - } - return buf.toString(); - } else { - return request.getPath(); + } catch (final URISyntaxException ex) { + return path; } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java index 4f83962ee..17d75c632 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java @@ -379,7 +379,7 @@ private void createTunnel( final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange(); if (authCacheKeeper != null) { - authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, clientContext); + authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext); } final HttpRequest connect = new BasicHttpRequest(Method.CONNECT, nextHop, nextHop.toHostString()); @@ -444,9 +444,9 @@ private boolean needAuthentication( if (authCacheKeeper != null) { if (proxyAuthRequested) { - authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context); } } @@ -455,7 +455,7 @@ private boolean needAuthentication( proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); } return updated; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java index 4fe36378b..7e285347c 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java @@ -43,6 +43,7 @@ import org.apache.hc.client5.http.auth.CredentialsStore; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.AuthSupport; +import org.apache.hc.client5.http.impl.RequestSupport; import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; import org.apache.hc.client5.http.impl.auth.HttpAuthenticator; import org.apache.hc.client5.http.protocol.HttpClientContext; @@ -148,23 +149,36 @@ public void execute( } final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority()); + final String pathPrefix = RequestSupport.extractPathPrefix(request); final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target); final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange(); + if (!targetAuthExchange.isConnectionBased() && + targetAuthExchange.getPathPrefix() != null && + !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) { + // force re-authentication if the current path prefix does not match + // that of the previous authentication exchange. + targetAuthExchange.reset(); + } + if (targetAuthExchange.getPathPrefix() == null) { + targetAuthExchange.setPathPrefix(pathPrefix); + } + if (authCacheKeeper != null) { - authCacheKeeper.loadPreemptively(target, targetAuthExchange, clientContext); + authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, clientContext); if (proxy != null) { - authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, clientContext); + authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext); } } final AtomicBoolean challenged = new AtomicBoolean(false); - internalExecute(target, targetAuthExchange, proxyAuthExchange, + internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange, challenged, request, entityProducer, scope, chain, asyncExecCallback); } private void internalExecute( final HttpHost target, + final String pathPrefix, final AuthExchange targetAuthExchange, final AuthExchange proxyAuthExchange, final AtomicBoolean challenged, @@ -216,6 +230,7 @@ public AsyncDataConsumer handleResponse( proxyAuthExchange, proxy != null ? proxy : target, target, + pathPrefix, response, clientContext)) { challenged.set(true); @@ -267,7 +282,7 @@ public void completed() { if (entityProducer != null) { entityProducer.releaseResources(); } - internalExecute(target, targetAuthExchange, proxyAuthExchange, + internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange, challenged, request, entityProducer, scope, chain, asyncExecCallback); } catch (final HttpException | IOException ex) { asyncExecCallback.failed(ex); @@ -298,6 +313,7 @@ private boolean needAuthentication( final AuthExchange proxyAuthExchange, final HttpHost proxy, final HttpHost target, + final String pathPrefix, final HttpResponse response, final HttpClientContext context) { final RequestConfig config = context.getRequestConfig(); @@ -307,9 +323,9 @@ private boolean needAuthentication( if (authCacheKeeper != null) { if (targetAuthRequested) { - authCacheKeeper.updateOnChallenge(target, targetAuthExchange, context); + authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(target, targetAuthExchange, context); + authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context); } } @@ -318,9 +334,9 @@ private boolean needAuthentication( if (authCacheKeeper != null) { if (proxyAuthRequested) { - authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context); } } @@ -329,7 +345,7 @@ private boolean needAuthentication( targetAuthStrategy, targetAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(target, targetAuthExchange, context); + authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context); } return updated; @@ -339,7 +355,7 @@ private boolean needAuthentication( proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); } return updated; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java index 01f959a8d..10ce0b6ba 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java @@ -59,32 +59,39 @@ public AuthCacheKeeper(final SchemePortResolver schemePortResolver) { } public void updateOnChallenge(final HttpHost host, + final String pathPrefix, final AuthExchange authExchange, final HttpContext context) { - clearCache(host, HttpClientContext.adapt(context)); + clearCache(host, pathPrefix, HttpClientContext.adapt(context)); } public void updateOnNoChallenge(final HttpHost host, + final String pathPrefix, final AuthExchange authExchange, final HttpContext context) { if (authExchange.getState() == AuthExchange.State.SUCCESS) { - updateCache(host, authExchange.getAuthScheme(), HttpClientContext.adapt(context)); + updateCache(host, pathPrefix, authExchange.getAuthScheme(), HttpClientContext.adapt(context)); } } public void updateOnResponse(final HttpHost host, + final String pathPrefix, final AuthExchange authExchange, final HttpContext context) { if (authExchange.getState() == AuthExchange.State.FAILURE) { - clearCache(host, HttpClientContext.adapt(context)); + clearCache(host, pathPrefix, HttpClientContext.adapt(context)); } } public void loadPreemptively(final HttpHost host, + final String pathPrefix, final AuthExchange authExchange, final HttpContext context) { if (authExchange.getState() == AuthExchange.State.UNCHALLENGED) { - final AuthScheme authScheme = loadFromCache(host, HttpClientContext.adapt(context)); + AuthScheme authScheme = loadFromCache(host, pathPrefix, HttpClientContext.adapt(context)); + if (authScheme == null && pathPrefix != null) { + authScheme = loadFromCache(host, null, HttpClientContext.adapt(context)); + } if (authScheme != null) { authExchange.select(authScheme); } @@ -92,14 +99,16 @@ public void loadPreemptively(final HttpHost host, } private AuthScheme loadFromCache(final HttpHost host, + final String pathPrefix, final HttpClientContext clientContext) { final AuthCache authCache = clientContext.getAuthCache(); if (authCache != null) { - final AuthScheme authScheme = authCache.get(host); + final AuthScheme authScheme = authCache.get(host, pathPrefix); if (authScheme != null) { if (LOG.isDebugEnabled()) { final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} Re-using cached '{}' auth scheme for {}", exchangeId, authScheme.getName(), host); + LOG.debug("{} Re-using cached '{}' auth scheme for {}{}", exchangeId, authScheme.getName(), host, + pathPrefix != null ? pathPrefix : ""); } return authScheme; } @@ -108,6 +117,7 @@ private AuthScheme loadFromCache(final HttpHost host, } private void updateCache(final HttpHost host, + final String pathPrefix, final AuthScheme authScheme, final HttpClientContext clientContext) { final boolean cacheable = authScheme.getClass().getAnnotation(AuthStateCacheable.class) != null; @@ -119,21 +129,24 @@ private void updateCache(final HttpHost host, } if (LOG.isDebugEnabled()) { final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} Caching '{}' auth scheme for {}", exchangeId, authScheme.getName(), host); + LOG.debug("{} Caching '{}' auth scheme for {}{}", exchangeId, authScheme.getName(), host, + pathPrefix != null ? pathPrefix : ""); } - authCache.put(host, authScheme); + authCache.put(host, pathPrefix, authScheme); } } private void clearCache(final HttpHost host, + final String pathPrefix, final HttpClientContext clientContext) { final AuthCache authCache = clientContext.getAuthCache(); if (authCache != null) { if (LOG.isDebugEnabled()) { final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} Clearing cached auth scheme for {}", exchangeId, host); + LOG.debug("{} Clearing cached auth scheme for {}{}", exchangeId, host, + pathPrefix != null ? pathPrefix : ""); } - authCache.remove(host); + authCache.remove(host, pathPrefix); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java index c22c57b30..c0c92c7de 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java @@ -32,6 +32,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -39,11 +40,12 @@ import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthScheme; import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; -import org.apache.hc.client5.http.routing.RoutingSupport; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.net.NamedEndpoint; import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.LangUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,7 +64,65 @@ public class BasicAuthCache implements AuthCache { private static final Logger LOG = LoggerFactory.getLogger(BasicAuthCache.class); - private final Map map; + static class Key { + + final String scheme; + final String host; + final int port; + final String pathPrefix; + + Key(final String scheme, final String host, final int port, final String pathPrefix) { + Args.notBlank(scheme, "Scheme"); + Args.notBlank(host, "Scheme"); + this.scheme = scheme.toLowerCase(Locale.ROOT); + this.host = host.toLowerCase(Locale.ROOT); + this.port = port; + this.pathPrefix = pathPrefix; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof Key) { + final Key that = (Key) obj; + return this.scheme.equals(that.scheme) && + this.host.equals(that.host) && + this.port == that.port && + LangUtils.equals(this.pathPrefix, that.pathPrefix); + } + return false; + } + + @Override + public int hashCode() { + int hash = LangUtils.HASH_SEED; + hash = LangUtils.hashCode(hash, this.scheme); + hash = LangUtils.hashCode(hash, this.host); + hash = LangUtils.hashCode(hash, this.port); + hash = LangUtils.hashCode(hash, this.pathPrefix); + return hash; + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append(scheme).append("://").append(host); + if (port >= 0) { + buf.append(":").append(port); + } + if (pathPrefix != null) { + if (!pathPrefix.startsWith("/")) { + buf.append("/"); + } + buf.append(pathPrefix); + } + return buf.toString(); + } + } + + private final Map map; private final SchemePortResolver schemePortResolver; /** @@ -80,8 +140,27 @@ public BasicAuthCache() { this(null); } + private Key key(final String scheme, final NamedEndpoint authority, final String pathPrefix) { + return new Key(scheme, authority.getHostName(), schemePortResolver.resolve(scheme, authority), pathPrefix); + } + @Override public void put(final HttpHost host, final AuthScheme authScheme) { + put(host, null, authScheme); + } + + @Override + public AuthScheme get(final HttpHost host) { + return get(host, null); + } + + @Override + public void remove(final HttpHost host) { + remove(host, null); + } + + @Override + public void put(final HttpHost host, final String pathPrefix, final AuthScheme authScheme) { Args.notNull(host, "HTTP host"); if (authScheme == null) { return; @@ -92,8 +171,7 @@ public void put(final HttpHost host, final AuthScheme authScheme) { try (final ObjectOutputStream out = new ObjectOutputStream(buf)) { out.writeObject(authScheme); } - final HttpHost key = RoutingSupport.normalize(host, schemePortResolver); - this.map.put(key, buf.toByteArray()); + this.map.put(key(host.getSchemeName(), host, pathPrefix), buf.toByteArray()); } catch (final IOException ex) { if (LOG.isWarnEnabled()) { LOG.warn("Unexpected I/O error while serializing auth scheme", ex); @@ -107,10 +185,9 @@ public void put(final HttpHost host, final AuthScheme authScheme) { } @Override - public AuthScheme get(final HttpHost host) { + public AuthScheme get(final HttpHost host, final String pathPrefix) { Args.notNull(host, "HTTP host"); - final HttpHost key = RoutingSupport.normalize(host, schemePortResolver); - final byte[] bytes = this.map.get(key); + final byte[] bytes = this.map.get(key(host.getSchemeName(), host, pathPrefix)); if (bytes != null) { try { final ByteArrayInputStream buf = new ByteArrayInputStream(bytes); @@ -131,10 +208,9 @@ public AuthScheme get(final HttpHost host) { } @Override - public void remove(final HttpHost host) { + public void remove(final HttpHost host, final String pathPrefix) { Args.notNull(host, "HTTP host"); - final HttpHost key = RoutingSupport.normalize(host, schemePortResolver); - this.map.remove(key); + this.map.remove(key(host.getSchemeName(), host, pathPrefix)); } @Override diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java index 5b80e21e8..45c0c5ec8 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java @@ -215,7 +215,7 @@ private boolean createTunnelToTarget( final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy); if (authCacheKeeper != null) { - authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, context); + authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context); } ClassicHttpResponse response = null; @@ -254,9 +254,9 @@ private boolean createTunnelToTarget( if (authCacheKeeper != null) { if (proxyAuthRequested) { - authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context); } } @@ -265,7 +265,7 @@ private boolean createTunnelToTarget( proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); } if (updated) { // Retry request diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java index 556f7a6ed..95f954567 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java @@ -42,6 +42,7 @@ import org.apache.hc.client5.http.classic.ExecRuntime; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.AuthSupport; +import org.apache.hc.client5.http.impl.RequestSupport; import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; import org.apache.hc.client5.http.impl.auth.HttpAuthenticator; import org.apache.hc.client5.http.protocol.HttpClientContext; @@ -151,14 +152,26 @@ public ClassicHttpResponse execute( } final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority()); + final String pathPrefix = RequestSupport.extractPathPrefix(request); final AuthExchange targetAuthExchange = context.getAuthExchange(target); final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange(); + if (!targetAuthExchange.isConnectionBased() && + targetAuthExchange.getPathPrefix() != null && + !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) { + // force re-authentication if the current path prefix does not match + // that of the previous authentication exchange. + targetAuthExchange.reset(); + } + if (targetAuthExchange.getPathPrefix() == null) { + targetAuthExchange.setPathPrefix(pathPrefix); + } + if (authCacheKeeper != null) { - authCacheKeeper.loadPreemptively(target, targetAuthExchange, context); + authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, context); if (proxy != null) { - authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, context); + authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context); } } @@ -206,6 +219,7 @@ public ClassicHttpResponse execute( proxyAuthExchange, proxy != null ? proxy : target, target, + pathPrefix, response, context)) { // Make sure the response body is fully consumed, if present @@ -259,6 +273,7 @@ private boolean needAuthentication( final AuthExchange proxyAuthExchange, final HttpHost proxy, final HttpHost target, + final String pathPrefix, final HttpResponse response, final HttpClientContext context) { final RequestConfig config = context.getRequestConfig(); @@ -268,9 +283,9 @@ private boolean needAuthentication( if (authCacheKeeper != null) { if (targetAuthRequested) { - authCacheKeeper.updateOnChallenge(target, targetAuthExchange, context); + authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(target, targetAuthExchange, context); + authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context); } } @@ -279,9 +294,9 @@ private boolean needAuthentication( if (authCacheKeeper != null) { if (proxyAuthRequested) { - authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context); } } @@ -290,7 +305,7 @@ private boolean needAuthentication( targetAuthStrategy, targetAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(target, targetAuthExchange, context); + authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context); } return updated; @@ -300,7 +315,7 @@ private boolean needAuthentication( proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context); + authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); } return updated; diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSupport.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSupport.java deleted file mode 100644 index 0af8432f9..000000000 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSupport.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ -package org.apache.hc.client5.http.impl; - -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.Method; -import org.apache.hc.core5.http.message.BasicHttpRequest; -import org.apache.hc.core5.net.URIAuthority; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.Test; - -/** - * Simple tests for {@link ProtocolSupport}. - */ -public class TestProtocolSupport { - - @Test - public void testGetRequestUri() { - final HttpRequest request = new BasicHttpRequest(Method.GET, ""); - MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("/")); - request.setAuthority(new URIAuthority("testUser", "localhost", 8080)); - MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("http://testUser@localhost:8080/")); - request.setScheme("https"); - MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("https://testUser@localhost:8080/")); - request.setPath("blah"); - MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("https://testUser@localhost:8080/blah")); - request.setPath("/blah/blah"); - MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("https://testUser@localhost:8080/blah/blah")); - } -} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java new file mode 100644 index 000000000..35e787bd9 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java @@ -0,0 +1,53 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.impl; + +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.junit.Assert; +import org.junit.Test; + +/** + * Simple tests for {@link RequestSupport}. + */ +public class TestRequestSupport { + + @Test + public void testPathPrefixExtraction() { + Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb"))); + Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/"))); + Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/../aaaa/"))); + Assert.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/cccc"))); + Assert.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/"))); + Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb?////"))); + Assert.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aa%2faa/bbbb"))); + Assert.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/a%61%2fa%61/bbbb"))); + Assert.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/"))); + Assert.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa"))); + Assert.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", ""))); + } + +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java index b57fa6d63..34860657c 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java @@ -67,7 +67,7 @@ public void testNullAuthScheme() throws Exception { } @Test - public void testStoreNonserializable() throws Exception { + public void testStoreNonSerializable() throws Exception { final BasicAuthCache cache = new BasicAuthCache(); final AuthScheme authScheme = new NTLMScheme(); cache.put(new HttpHost("localhost", 80), authScheme); diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestRequestAuthCache.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestRequestAuthCache.java deleted file mode 100644 index d6959c14b..000000000 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestRequestAuthCache.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ -package org.apache.hc.client5.http.impl.auth; - -import org.apache.hc.client5.http.HttpRoute; -import org.apache.hc.client5.http.auth.AuthCache; -import org.apache.hc.client5.http.auth.AuthExchange; -import org.apache.hc.client5.http.auth.AuthScope; -import org.apache.hc.client5.http.auth.Credentials; -import org.apache.hc.client5.http.auth.CredentialsProvider; -import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; -import org.apache.hc.client5.http.protocol.HttpClientContext; -import org.apache.hc.client5.http.protocol.RequestAuthCache; -import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpRequestInterceptor; -import org.apache.hc.core5.http.message.BasicHttpRequest; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -public class TestRequestAuthCache { - - private HttpHost target; - private HttpHost proxy; - private Credentials creds1; - private Credentials creds2; - private AuthScope authscope1; - private AuthScope authscope2; - private BasicScheme authscheme1; - private BasicScheme authscheme2; - private CredentialsProvider credProvider; - - @Before - public void setUp() { - this.target = new HttpHost("localhost", 80); - this.proxy = new HttpHost("localhost", 8080); - - this.creds1 = new UsernamePasswordCredentials("user1", "secret1".toCharArray()); - this.creds2 = new UsernamePasswordCredentials("user2", "secret2".toCharArray()); - this.authscope1 = new AuthScope(this.target); - this.authscope2 = new AuthScope(this.proxy); - this.authscheme1 = new BasicScheme(); - this.authscheme2 = new BasicScheme(); - - this.credProvider = CredentialsProviderBuilder.create() - .add(this.authscope1, this.creds1) - .add(this.authscope2, this.creds2) - .build(); - } - - @Test - public void testRequestParameterCheck() throws Exception { - final HttpClientContext context = HttpClientContext.create(); - final HttpRequestInterceptor interceptor = new RequestAuthCache(); - Assert.assertThrows(NullPointerException.class, () -> - interceptor.process(null, null, context)); - } - - @Test - public void testContextParameterCheck() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET", "/"); - final HttpRequestInterceptor interceptor = new RequestAuthCache(); - Assert.assertThrows(NullPointerException.class, () -> - interceptor.process(request, null, null)); - } - - @Test - public void testPreemptiveTargetAndProxyAuth() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET", "/"); - - final HttpClientContext context = HttpClientContext.create(); - context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credProvider); - context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false)); - - final AuthCache authCache = new BasicAuthCache(); - authCache.put(this.target, this.authscheme1); - authCache.put(this.proxy, this.authscheme2); - - context.setAttribute(HttpClientContext.AUTH_CACHE, authCache); - - final HttpRequestInterceptor interceptor = new RequestAuthCache(); - interceptor.process(request, null, context); - - final AuthExchange targetAuthExchange = context.getAuthExchange(this.target); - final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy); - - Assert.assertNotNull(targetAuthExchange); - Assert.assertNotNull(targetAuthExchange.getAuthScheme()); - Assert.assertNotNull(proxyAuthExchange); - Assert.assertNotNull(proxyAuthExchange.getAuthScheme()); - } - - @Test - public void testCredentialsProviderNotSet() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET", "/"); - - final HttpClientContext context = HttpClientContext.create(); - context.setAttribute(HttpClientContext.CREDS_PROVIDER, null); - context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false)); - - final AuthCache authCache = new BasicAuthCache(); - authCache.put(this.target, this.authscheme1); - authCache.put(this.proxy, this.authscheme2); - - context.setAttribute(HttpClientContext.AUTH_CACHE, authCache); - - final HttpRequestInterceptor interceptor = new RequestAuthCache(); - interceptor.process(request, null, context); - - final AuthExchange targetAuthExchange = context.getAuthExchange(this.target); - final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy); - - Assert.assertNotNull(targetAuthExchange); - Assert.assertNull(targetAuthExchange.getAuthScheme()); - Assert.assertNotNull(proxyAuthExchange); - Assert.assertNull(proxyAuthExchange.getAuthScheme()); - } - - @Test - public void testAuthCacheNotSet() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET", "/"); - - final HttpClientContext context = HttpClientContext.create(); - context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credProvider); - context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false)); - context.setAttribute(HttpClientContext.AUTH_CACHE, null); - - final HttpRequestInterceptor interceptor = new RequestAuthCache(); - interceptor.process(request, null, context); - - final AuthExchange targetAuthExchange = context.getAuthExchange(this.target); - final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy); - - Assert.assertNotNull(targetAuthExchange); - Assert.assertNull(targetAuthExchange.getAuthScheme()); - Assert.assertNotNull(proxyAuthExchange); - Assert.assertNull(proxyAuthExchange.getAuthScheme()); - } - - @Test - public void testAuthCacheEmpty() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET", "/"); - - final HttpClientContext context = HttpClientContext.create(); - context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credProvider); - context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false)); - - final AuthCache authCache = new BasicAuthCache(); - context.setAttribute(HttpClientContext.AUTH_CACHE, authCache); - - final HttpRequestInterceptor interceptor = new RequestAuthCache(); - interceptor.process(request, null, context); - - final AuthExchange targetAuthExchange = context.getAuthExchange(this.target); - final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy); - - Assert.assertNotNull(targetAuthExchange); - Assert.assertNull(targetAuthExchange.getAuthScheme()); - Assert.assertNotNull(proxyAuthExchange); - Assert.assertNull(proxyAuthExchange.getAuthScheme()); - } - -}