diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java index bce1873ea9..1df164a8c6 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java @@ -33,6 +33,7 @@ import org.apache.hc.client5.http.HttpRequestRetryStrategy; import org.apache.hc.client5.http.UserTokenHandler; import org.apache.hc.client5.http.auth.AuthSchemeFactory; +import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; @@ -159,6 +160,12 @@ public TestAsyncClientBuilder setDefaultAuthSchemeRegistry(final Lookup + * Please note auth schemes that perform mutual authentication must implement + * {@link #processChallenge(HttpHost, AuthChallenge, HttpContext, boolean)} and + * {@link #isChallengeExpected()} instead. * * @param authChallenge the auth challenge * @param context HTTP context * @throws MalformedChallengeException in case the auth challenge is incomplete, * malformed or otherwise invalid. + * + * @see #processChallenge(HttpHost, AuthChallenge, HttpContext, boolean) + * * @since 5.0 */ void processChallenge( AuthChallenge authChallenge, HttpContext context) throws MalformedChallengeException; + /** + * Indicates that the even authorized (i.e. not 401 or 407) responses must be processed + * by this scheme. + * + * @return true if responses with non 401/407 response codes must be processed by the scheme. + * + * @since 5.5 + */ + default boolean isChallengeExpected() { + return false; + } + + /** + * Processes the given auth challenge. Some authentication schemes may involve multiple + * challenge-response exchanges. Such schemes must be able to maintain internal state + * when dealing with sequential challenges. + *

+ * When {@link #isChallengeExpected()} returns true, but no challenge was sent, this method + * must be called with a null {@link AuthChallenge} so that the scheme can handle this situation. + * + * @param host HTTP host + * @param authChallenge the auth challenge or null if no challenge was received + * @param context HTTP context + * @param challenged true if the response was unauthorised (401/407) + * + * @throws MalformedChallengeException in case the auth challenge is incomplete, + * @throws AuthenticationException in case the authentication process is unsuccessful. + * + * @since 5.5 + */ + default void processChallenge( + HttpHost host, + AuthChallenge authChallenge, + HttpContext context, + boolean challenged) throws MalformedChallengeException, AuthenticationException { + processChallenge(authChallenge, context); + } + /** * Authentication process may involve a series of challenge-response exchanges. * This method tests if the authorization process has been fully completed (either * successfully or unsuccessfully), that is, all the required authorization * challenges have been processed in their entirety. + *

+ * Please note if the scheme returns {@code true} from this method in response + * to a challenge, it effectively implies a failure to respond to this challenge + * and termination of the authentication process. * * @return {@code true} if the authentication process has been completed, * {@code false} otherwise. 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 0440a1322f..9409d6cfa4 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 @@ -72,6 +72,10 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy { StandardAuthScheme.DIGEST, StandardAuthScheme.BASIC)); + protected List getSchemePriority() { + return DEFAULT_SCHEME_PRIORITY; + } + @Override public List select( final ChallengeType challengeType, @@ -95,7 +99,7 @@ public List select( Collection authPrefs = challengeType == ChallengeType.TARGET ? config.getTargetPreferredAuthSchemes() : config.getProxyPreferredAuthSchemes(); if (authPrefs == null) { - authPrefs = DEFAULT_SCHEME_PRIORITY; + authPrefs = getSchemePriority(); } if (LOG.isDebugEnabled()) { LOG.debug("{} Authentication schemes in the order of preference: {}", exchangeId, authPrefs); 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 65fac24067..d8dd016339 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 @@ -43,10 +43,12 @@ import org.apache.hc.client5.http.async.AsyncExecChainHandler; import org.apache.hc.client5.http.async.AsyncExecRuntime; import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; -import org.apache.hc.client5.http.impl.auth.HttpAuthenticator; +import org.apache.hc.client5.http.impl.auth.AuthenticationHandler; import org.apache.hc.client5.http.impl.routing.BasicRouteDirector; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.routing.HttpRouteDirector; @@ -93,7 +95,7 @@ public final class AsyncConnectExec implements AsyncExecChainHandler { private final HttpProcessor proxyHttpProcessor; private final AuthenticationStrategy proxyAuthStrategy; - private final HttpAuthenticator authenticator; + private final AuthenticationHandler authenticator; private final AuthCacheKeeper authCacheKeeper; private final HttpRouteDirector routeDirector; @@ -106,7 +108,7 @@ public AsyncConnectExec( Args.notNull(proxyAuthStrategy, "Proxy authentication strategy"); this.proxyHttpProcessor = proxyHttpProcessor; this.proxyAuthStrategy = proxyAuthStrategy; - this.authenticator = new HttpAuthenticator(); + this.authenticator = new AuthenticationHandler(); this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(schemePortResolver); this.routeDirector = BasicRouteDirector.INSTANCE; } @@ -516,10 +518,11 @@ private boolean needAuthentication( final AuthExchange proxyAuthExchange, final HttpHost proxy, final HttpResponse response, - final HttpClientContext context) { + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { final RequestConfig config = context.getRequestConfigOrDefault(); if (config.isAuthenticationEnabled()) { final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -529,8 +532,8 @@ private boolean needAuthentication( } } - if (proxyAuthRequested) { - final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, + if (proxyAuthRequested || proxyMutualAuthRequired) { + final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { 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 907b23e46b..0d8c504627 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 @@ -38,12 +38,14 @@ import org.apache.hc.client5.http.async.AsyncExecChainHandler; import org.apache.hc.client5.http.async.AsyncExecRuntime; import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; 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.impl.auth.AuthenticationHandler; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; @@ -84,7 +86,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler { private final AuthenticationStrategy targetAuthStrategy; private final AuthenticationStrategy proxyAuthStrategy; - private final HttpAuthenticator authenticator; + private final AuthenticationHandler authenticator; private final SchemePortResolver schemePortResolver; private final AuthCacheKeeper authCacheKeeper; @@ -95,7 +97,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler { final boolean authCachingDisabled) { this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy"); this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy"); - this.authenticator = new HttpAuthenticator(); + this.authenticator = new AuthenticationHandler(); this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE; this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(this.schemePortResolver); } @@ -305,11 +307,12 @@ private boolean needAuthentication( final HttpHost target, final String pathPrefix, final HttpResponse response, - final HttpClientContext context) { + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { final RequestConfig config = context.getRequestConfigOrDefault(); if (config.isAuthenticationEnabled()) { final boolean targetAuthRequested = authenticator.isChallenged( target, ChallengeType.TARGET, response, targetAuthExchange, context); + final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange); if (authCacheKeeper != null) { if (targetAuthRequested) { @@ -321,6 +324,7 @@ private boolean needAuthentication( final boolean proxyAuthRequested = authenticator.isChallenged( proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -330,8 +334,8 @@ private boolean needAuthentication( } } - if (targetAuthRequested) { - final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response, + if (targetAuthRequested || targetMutualAuthRequired) { + final boolean updated = authenticator.handleResponse(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); if (authCacheKeeper != null) { @@ -340,8 +344,8 @@ private boolean needAuthentication( return updated; } - if (proxyAuthRequested) { - final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, + if (proxyAuthRequested || proxyMutualAuthRequired) { + final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java new file mode 100644 index 0000000000..1546171c0a --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java @@ -0,0 +1,447 @@ +/* + * ==================================================================== + * 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 java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Queue; + +import org.apache.hc.client5.http.AuthenticationStrategy; +import org.apache.hc.client5.http.auth.AuthChallenge; +import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthenticationException; +import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.MalformedChallengeException; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.FormattedHeader; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.message.ParserCursor; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.Asserts; +import org.apache.hc.core5.util.CharArrayBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Internal utility class that implements commons aspects of the client side + * HTTP authentication. + * + * @since 5.4 + */ +@Internal +@Contract(threading = ThreadingBehavior.STATELESS) +public class AuthenticationHandler { + + private static final Logger LOG = LoggerFactory.getLogger(AuthenticationHandler.class); + + private final AuthChallengeParser parser; + + public AuthenticationHandler() { + this.parser = new AuthChallengeParser(); + } + + /** + * Determines whether the given response represents an authentication challenge, and updates + * the {@link AuthExchange} state. + * + * @param host the hostname of the opposite endpoint. + * @param challengeType the challenge type (target or proxy). + * @param response the response message head. + * @param authExchange the current authentication exchange state. Gets updated. + * @param context the current execution context. + * @return {@code true} if the response message represents an authentication challenge, + * {@code false} otherwise. + */ + public boolean isChallenged( + final HttpHost host, + final ChallengeType challengeType, + final HttpResponse response, + final AuthExchange authExchange, + final HttpContext context) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + if (checkChallenged(challengeType, response, clientContext)) { + return true; + } + switch (authExchange.getState()) { + case CHALLENGED: + case HANDSHAKE: + if (LOG.isDebugEnabled()) { + // The mutual auth may still fail + LOG.debug("{} Server has accepted authorization", clientContext.getExchangeId()); + } + authExchange.setState(AuthExchange.State.SUCCESS); + break; + case SUCCESS: + break; + default: + authExchange.setState(AuthExchange.State.UNCHALLENGED); + } + return false; + } + + /** + * Determines whether the given response represents an authentication challenge, without + * changing the {@link AuthExchange} state. + * + * @param challengeType the challenge type (target or proxy). + * @param response the response message head. + * @param clientContext the current execution context. + * @return {@code true} if the response message represents an authentication challenge, + * {@code false} otherwise. + */ + private boolean checkChallenged( + final ChallengeType challengeType, + final HttpResponse response, + final HttpClientContext clientContext) { + final int challengeCode; + switch (challengeType) { + case TARGET: + challengeCode = HttpStatus.SC_UNAUTHORIZED; + break; + case PROXY: + challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED; + break; + default: + throw new IllegalStateException("Unexpected challenge type: " + challengeType); + } + + if (response.getCode() == challengeCode) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} Authentication required", clientContext.getExchangeId()); + } + return true; + } + return false; + } + + /** + * Determines if the scheme requires an auth challenge for responses that do not + * have challenge HTTP code. (i.e whether it needs a mutual authentication token) + * + * @return true is authExchange's scheme is AuthScheme2, which currently expects + * a WWW-Authenticate header even for authorized HTTP responses + */ + public boolean isChallengeExpected(final AuthExchange authExchange) { + final AuthScheme authScheme = authExchange.getAuthScheme(); + return authScheme != null && authScheme.isChallengeExpected(); + } + + public Map extractChallengeMap( + final ChallengeType challengeType, + final HttpResponse response, + final HttpClientContext context) { + final Map challengeMap = new HashMap<>(); + final Iterator

headerIterator = response.headerIterator( + challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE : HttpHeaders.WWW_AUTHENTICATE); + while (headerIterator.hasNext()) { + final Header header = headerIterator.next(); + final CharArrayBuffer buffer; + final int pos; + if (header instanceof FormattedHeader) { + buffer = ((FormattedHeader) header).getBuffer(); + pos = ((FormattedHeader) header).getValuePos(); + } else { + final String s = header.getValue(); + if (s == null) { + continue; + } + buffer = new CharArrayBuffer(s.length()); + buffer.append(s); + pos = 0; + } + final ParserCursor cursor = new ParserCursor(pos, buffer.length()); + final List authChallenges; + try { + authChallenges = parser.parse(challengeType, buffer, cursor); + } catch (final ParseException ex) { + if (LOG.isWarnEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.warn("{} Malformed challenge: {}", exchangeId, header.getValue()); + } + continue; + } + for (final AuthChallenge authChallenge : authChallenges) { + final String schemeName = authChallenge.getSchemeName().toLowerCase(Locale.ROOT); + if (!challengeMap.containsKey(schemeName)) { + challengeMap.put(schemeName, authChallenge); + } + } + } + return challengeMap; + } + + /** + * Handles the response from the opposite endpoint and updates the {@link AuthExchange} + * state based on the challenge presented in the response message using the given + * {@link AuthenticationStrategy}. + * + * @param host the hostname of the opposite endpoint. + * @param challengeType the challenge type (target or proxy). + * @param response the response message head. + * @param authStrategy the authentication strategy. + * @param authExchange the current authentication exchange state. + * @param context the current execution context. + * @return {@code true} if the request needs-to be re-sent, + * {@code false} if the authentication is complete (successful or not). + * + * @throws AuthenticationException if the AuthScheme throws one. In most cases this indicates a + * client side problem, as final server error responses are simply returned. + * @throws MalformedChallengeException if the AuthScheme throws one. In most cases this indicates a + * client side problem, as final server error responses are simply returned. + */ + public boolean handleResponse( + final HttpHost host, + final ChallengeType challengeType, + final HttpResponse response, + final AuthenticationStrategy authStrategy, + final AuthExchange authExchange, + final HttpContext context) throws AuthenticationException, MalformedChallengeException { + + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + final boolean challenged = checkChallenged(challengeType, response, clientContext); + final boolean isChallengeExpected = isChallengeExpected(authExchange); + + if (LOG.isDebugEnabled()) { + LOG.debug("{} {} requested authentication", exchangeId, host.toHostString()); + } + + final Map challengeMap = extractChallengeMap(challengeType, response, clientContext); + + if (challengeMap.isEmpty()) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} Response contains no valid authentication challenges", exchangeId); + } + if (!isChallengeExpected) { + authExchange.reset(); + return false; + } + } + + switch (authExchange.getState()) { + case FAILURE: + return false; + case SUCCESS: + if (!isChallengeExpected) { + authExchange.reset(); + break; + } + // otherwise fall through + case CHALLENGED: + // fall through + case HANDSHAKE: + Asserts.notNull(authExchange.getAuthScheme(), "AuthScheme"); + // fall through + case UNCHALLENGED: + final AuthScheme authScheme = authExchange.getAuthScheme(); + // AuthScheme is only set if we have already sent an auth response, either + // because we have received a challenge for it, or preemptively. + if (authScheme != null) { + final String schemeName = authScheme.getName(); + final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT)); + if (challenge != null || isChallengeExpected) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} Processing authorization challenge {}", exchangeId, challenge); + } + try { + authScheme.processChallenge(host, challenge, clientContext, challenged); + } catch (final AuthenticationException | MalformedChallengeException ex) { + if (LOG.isWarnEnabled()) { + LOG.warn("Exception processing challenge {}", exchangeId, ex); + } + authExchange.reset(); + authExchange.setState(AuthExchange.State.FAILURE); + if (isChallengeExpected) { + throw ex; + } + } + if (authScheme.isChallengeComplete()) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} Authentication failed", exchangeId); + } + authExchange.reset(); + authExchange.setState(AuthExchange.State.FAILURE); + return false; + } + if (!challenged) { + // There are no more challanges sent after the 200 message, + // and if we get here, then the mutual auth phase has succeeded. + authExchange.setState(AuthExchange.State.SUCCESS); + return false; + } else { + authExchange.setState(AuthExchange.State.HANDSHAKE); + } + return true; + } + authExchange.reset(); + // Retry authentication with a different scheme + } + } + + // We reach this if we fell through above because the authScheme has not yet been set, or if + // we receive a 401/407 response for an unexpected scheme. Normally this processes the first + // 401/407 response + final List preferredSchemes = authStrategy.select(challengeType, challengeMap, clientContext); + final CredentialsProvider credsProvider = clientContext.getCredentialsProvider(); + if (credsProvider == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} Credentials provider not set in the context", exchangeId); + } + return false; + } + + final Queue authOptions = new LinkedList<>(); + if (LOG.isDebugEnabled()) { + LOG.debug("{} Selecting authentication options", exchangeId); + } + for (final AuthScheme authScheme: preferredSchemes) { + // We only respond to the first successfully processed challenge. However, the + // original AuthScheme API does not really process the challenge at this point, + // so we need to process/store each challenge here anyway. + try { + final String schemeName = authScheme.getName(); + final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT)); + authScheme.processChallenge(host, challenge, clientContext, challenged); + if (authScheme.isResponseReady(host, credsProvider, clientContext)) { + authOptions.add(authScheme); + } + } catch (final AuthenticationException | MalformedChallengeException ex) { + if (LOG.isWarnEnabled()) { + LOG.warn("Exception while processing Challange", ex); + } + } + } + if (!authOptions.isEmpty()) { + if (LOG.isDebugEnabled()) { + LOG.debug("{} Selected authentication options: {}", exchangeId, authOptions); + } + authExchange.reset(); + authExchange.setState(AuthExchange.State.CHALLENGED); + authExchange.setOptions(authOptions); + return true; + } + return false; + } + + /** + * Generates a response to the authentication challenge based on the actual {@link AuthExchange} state + * and adds it to the given {@link HttpRequest} message . + * + * @param host the hostname of the opposite endpoint. + * @param challengeType the challenge type (target or proxy). + * @param request the request message head. + * @param authExchange the current authentication exchange state. + * @param context the current execution context. + */ + public void addAuthResponse( + final HttpHost host, + final ChallengeType challengeType, + final HttpRequest request, + final AuthExchange authExchange, + final HttpContext context) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + AuthScheme authScheme = authExchange.getAuthScheme(); + switch (authExchange.getState()) { + case FAILURE: + return; + case SUCCESS: + Asserts.notNull(authScheme, "AuthScheme"); + if (authScheme.isConnectionBased()) { + return; + } + break; + case HANDSHAKE: + Asserts.notNull(authScheme, "AuthScheme"); + break; + case CHALLENGED: + final Queue authOptions = authExchange.getAuthOptions(); + if (authOptions != null) { + while (!authOptions.isEmpty()) { + authScheme = authOptions.remove(); + authExchange.select(authScheme); + if (LOG.isDebugEnabled()) { + LOG.debug("{} Generating response to an authentication challenge using {} scheme", + exchangeId, authScheme.getName()); + } + try { + final String authResponse = authScheme.generateAuthResponse(host, request, clientContext); + if (authResponse != null) { + final Header header = new BasicHeader( + challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION, + authResponse); + request.addHeader(header); + } + break; + } catch (final AuthenticationException ex) { + if (LOG.isWarnEnabled()) { + LOG.warn("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage()); + } + } + } + return; + } + Asserts.notNull(authScheme, "AuthScheme"); + default: + } + // This is the SUCCESS and HANDSHAKE states, same as the initial response. + // This only happens if the handshake requires multiple requests, which is + // unlikely in practice. + if (authScheme != null) { + try { + final String authResponse = authScheme.generateAuthResponse(host, request, clientContext); + final Header header = new BasicHeader( + challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION, + authResponse); + request.addHeader(header); + } catch (final AuthenticationException ex) { + if (LOG.isErrorEnabled()) { + LOG.error("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage()); + } + } + } + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java index cd9f7ce723..e3ca1a558b 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java @@ -27,126 +27,34 @@ package org.apache.hc.client5.http.impl.auth; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Queue; - import org.apache.hc.client5.http.AuthenticationStrategy; -import org.apache.hc.client5.http.auth.AuthChallenge; import org.apache.hc.client5.http.auth.AuthExchange; -import org.apache.hc.client5.http.auth.AuthScheme; -import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; -import org.apache.hc.client5.http.auth.CredentialsProvider; -import org.apache.hc.client5.http.auth.MalformedChallengeException; -import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; -import org.apache.hc.core5.http.FormattedHeader; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.ParseException; -import org.apache.hc.core5.http.message.BasicHeader; -import org.apache.hc.core5.http.message.ParserCursor; +import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.protocol.HttpContext; -import org.apache.hc.core5.util.Asserts; -import org.apache.hc.core5.util.CharArrayBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * Utility class that implements commons aspects of the client side HTTP authentication. - *

- * Please note that since version 5.2 this class no longer updated the authentication cache - * bound to the execution context. - * - * @since 4.3 + * @deprecated Do not use. */ +@Deprecated @Contract(threading = ThreadingBehavior.STATELESS) -public final class HttpAuthenticator { - - private static final Logger LOG = LoggerFactory.getLogger(HttpAuthenticator.class); - - private final AuthChallengeParser parser; +public final class HttpAuthenticator extends AuthenticationHandler { - public HttpAuthenticator() { - this.parser = new AuthChallengeParser(); - } - - /** - * Determines whether the given response represents an authentication challenge. - * - * @param host the hostname of the opposite endpoint. - * @param challengeType the challenge type (target or proxy). - * @param response the response message head. - * @param authExchange the current authentication exchange state. - * @param context the current execution context. - * @return {@code true} if the response message represents an authentication challenge, - * {@code false} otherwise. - */ + @Override public boolean isChallenged( final HttpHost host, final ChallengeType challengeType, final HttpResponse response, final AuthExchange authExchange, final HttpContext context) { - final int challengeCode; - switch (challengeType) { - case TARGET: - challengeCode = HttpStatus.SC_UNAUTHORIZED; - break; - case PROXY: - challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED; - break; - default: - throw new IllegalStateException("Unexpected challenge type: " + challengeType); - } - - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - - if (response.getCode() == challengeCode) { - if (LOG.isDebugEnabled()) { - LOG.debug("{} Authentication required", exchangeId); - } - return true; - } - switch (authExchange.getState()) { - case CHALLENGED: - case HANDSHAKE: - if (LOG.isDebugEnabled()) { - LOG.debug("{} Authentication succeeded", exchangeId); - } - authExchange.setState(AuthExchange.State.SUCCESS); - break; - case SUCCESS: - break; - default: - authExchange.setState(AuthExchange.State.UNCHALLENGED); - } - return false; + return super.isChallenged(host, challengeType, response, authExchange, context); } - /** - * Updates the {@link AuthExchange} state based on the challenge presented in the response message - * using the given {@link AuthenticationStrategy}. - * - * @param host the hostname of the opposite endpoint. - * @param challengeType the challenge type (target or proxy). - * @param response the response message head. - * @param authStrategy the authentication strategy. - * @param authExchange the current authentication exchange state. - * @param context the current execution context. - * @return {@code true} if the authentication state has been updated, - * {@code false} if unchanged. - */ public boolean updateAuthState( final HttpHost host, final ChallengeType challengeType, @@ -154,212 +62,21 @@ public boolean updateAuthState( final AuthenticationStrategy authStrategy, final AuthExchange authExchange, final HttpContext context) { - - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - - if (LOG.isDebugEnabled()) { - LOG.debug("{} {} requested authentication", exchangeId, host.toHostString()); - } - - final Header[] headers = response.getHeaders( - challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE : HttpHeaders.WWW_AUTHENTICATE); - final Map challengeMap = new HashMap<>(); - for (final Header header: headers) { - final CharArrayBuffer buffer; - final int pos; - if (header instanceof FormattedHeader) { - buffer = ((FormattedHeader) header).getBuffer(); - pos = ((FormattedHeader) header).getValuePos(); - } else { - final String s = header.getValue(); - if (s == null) { - continue; - } - buffer = new CharArrayBuffer(s.length()); - buffer.append(s); - pos = 0; - } - final ParserCursor cursor = new ParserCursor(pos, buffer.length()); - final List authChallenges; - try { - authChallenges = parser.parse(challengeType, buffer, cursor); - } catch (final ParseException ex) { - if (LOG.isWarnEnabled()) { - LOG.warn("{} Malformed challenge: {}", exchangeId, header.getValue()); - } - continue; - } - for (final AuthChallenge authChallenge: authChallenges) { - final String schemeName = authChallenge.getSchemeName().toLowerCase(Locale.ROOT); - if (!challengeMap.containsKey(schemeName)) { - challengeMap.put(schemeName, authChallenge); - } - } - } - if (challengeMap.isEmpty()) { - if (LOG.isDebugEnabled()) { - LOG.debug("{} Response contains no valid authentication challenges", exchangeId); - } - authExchange.reset(); - return false; - } - - switch (authExchange.getState()) { - case FAILURE: - return false; - case SUCCESS: - authExchange.reset(); - break; - case CHALLENGED: - case HANDSHAKE: - Asserts.notNull(authExchange.getAuthScheme(), "AuthScheme"); - case UNCHALLENGED: - final AuthScheme authScheme = authExchange.getAuthScheme(); - if (authScheme != null) { - final String schemeName = authScheme.getName(); - final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT)); - if (challenge != null) { - if (LOG.isDebugEnabled()) { - LOG.debug("{} Authorization challenge processed", exchangeId); - } - try { - authScheme.processChallenge(challenge, context); - } catch (final MalformedChallengeException ex) { - if (LOG.isWarnEnabled()) { - LOG.warn("{} {}", exchangeId, ex.getMessage()); - } - authExchange.reset(); - authExchange.setState(AuthExchange.State.FAILURE); - return false; - } - if (authScheme.isChallengeComplete()) { - if (LOG.isDebugEnabled()) { - LOG.debug("{} Authentication failed", exchangeId); - } - authExchange.reset(); - authExchange.setState(AuthExchange.State.FAILURE); - return false; - } - authExchange.setState(AuthExchange.State.HANDSHAKE); - return true; - } - authExchange.reset(); - // Retry authentication with a different scheme - } - } - - final List preferredSchemes = authStrategy.select(challengeType, challengeMap, context); - final CredentialsProvider credsProvider = clientContext.getCredentialsProvider(); - if (credsProvider == null) { - if (LOG.isDebugEnabled()) { - LOG.debug("{} Credentials provider not set in the context", exchangeId); - } + try { + return handleResponse(host, challengeType, response, authStrategy, authExchange, context); + } catch (final ProtocolException ex) { return false; } - - final Queue authOptions = new LinkedList<>(); - if (LOG.isDebugEnabled()) { - LOG.debug("{} Selecting authentication options", exchangeId); - } - for (final AuthScheme authScheme: preferredSchemes) { - try { - final String schemeName = authScheme.getName(); - final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT)); - authScheme.processChallenge(challenge, context); - if (authScheme.isResponseReady(host, credsProvider, context)) { - authOptions.add(authScheme); - } - } catch (final AuthenticationException | MalformedChallengeException ex) { - if (LOG.isWarnEnabled()) { - LOG.warn(ex.getMessage()); - } - } - } - if (!authOptions.isEmpty()) { - if (LOG.isDebugEnabled()) { - LOG.debug("{} Selected authentication options: {}", exchangeId, authOptions); - } - authExchange.reset(); - authExchange.setState(AuthExchange.State.CHALLENGED); - authExchange.setOptions(authOptions); - return true; - } - return false; } - /** - * Generates a response to the authentication challenge based on the actual {@link AuthExchange} state - * and adds it to the given {@link HttpRequest} message . - * - * @param host the hostname of the opposite endpoint. - * @param challengeType the challenge type (target or proxy). - * @param request the request message head. - * @param authExchange the current authentication exchange state. - * @param context the current execution context. - */ + @Override public void addAuthResponse( final HttpHost host, final ChallengeType challengeType, final HttpRequest request, final AuthExchange authExchange, final HttpContext context) { - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - AuthScheme authScheme = authExchange.getAuthScheme(); - switch (authExchange.getState()) { - case FAILURE: - return; - case SUCCESS: - Asserts.notNull(authScheme, "AuthScheme"); - if (authScheme.isConnectionBased()) { - return; - } - break; - case HANDSHAKE: - Asserts.notNull(authScheme, "AuthScheme"); - break; - case CHALLENGED: - final Queue authOptions = authExchange.getAuthOptions(); - if (authOptions != null) { - while (!authOptions.isEmpty()) { - authScheme = authOptions.remove(); - authExchange.select(authScheme); - if (LOG.isDebugEnabled()) { - LOG.debug("{} Generating response to an authentication challenge using {} scheme", - exchangeId, authScheme.getName()); - } - try { - final String authResponse = authScheme.generateAuthResponse(host, request, context); - final Header header = new BasicHeader( - challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION, - authResponse); - request.addHeader(header); - break; - } catch (final AuthenticationException ex) { - if (LOG.isWarnEnabled()) { - LOG.warn("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage()); - } - } - } - return; - } - Asserts.notNull(authScheme, "AuthScheme"); - default: - } - if (authScheme != null) { - try { - final String authResponse = authScheme.generateAuthResponse(host, request, context); - final Header header = new BasicHeader( - challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION, - authResponse); - request.addHeader(header); - } catch (final AuthenticationException ex) { - if (LOG.isErrorEnabled()) { - LOG.error("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage()); - } - } - } + super.addAuthResponse(host, challengeType, request, authExchange, context); } } 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 de52644f25..d3383523cb 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 @@ -41,7 +41,7 @@ import org.apache.hc.client5.http.classic.ExecRuntime; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; -import org.apache.hc.client5.http.impl.auth.HttpAuthenticator; +import org.apache.hc.client5.http.impl.auth.AuthenticationHandler; import org.apache.hc.client5.http.impl.routing.BasicRouteDirector; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.routing.HttpRouteDirector; @@ -85,7 +85,7 @@ public final class ConnectExec implements ExecChainHandler { private final ConnectionReuseStrategy reuseStrategy; private final HttpProcessor proxyHttpProcessor; private final AuthenticationStrategy proxyAuthStrategy; - private final HttpAuthenticator authenticator; + private final AuthenticationHandler authenticator; private final AuthCacheKeeper authCacheKeeper; private final HttpRouteDirector routeDirector; @@ -101,7 +101,7 @@ public ConnectExec( this.reuseStrategy = reuseStrategy; this.proxyHttpProcessor = proxyHttpProcessor; this.proxyAuthStrategy = proxyAuthStrategy; - this.authenticator = new HttpAuthenticator(); + this.authenticator = new AuthenticationHandler(); this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(schemePortResolver); this.routeDirector = BasicRouteDirector.INSTANCE; } @@ -252,6 +252,7 @@ private ClassicHttpResponse createTunnelToTarget( if (config.isAuthenticationEnabled()) { final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -261,8 +262,8 @@ private ClassicHttpResponse createTunnelToTarget( } } - if (proxyAuthRequested) { - final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, + if (proxyAuthRequested || proxyMutualAuthRequired) { + final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { 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 bfebce0eaf..c28c94afbe 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 @@ -34,7 +34,9 @@ import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.SchemePortResolver; import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecChainHandler; import org.apache.hc.client5.http.classic.ExecRuntime; @@ -42,7 +44,7 @@ import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; 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.impl.auth.AuthenticationHandler; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; @@ -83,7 +85,7 @@ public final class ProtocolExec implements ExecChainHandler { private final AuthenticationStrategy targetAuthStrategy; private final AuthenticationStrategy proxyAuthStrategy; - private final HttpAuthenticator authenticator; + private final AuthenticationHandler authenticator; private final SchemePortResolver schemePortResolver; private final AuthCacheKeeper authCacheKeeper; @@ -94,7 +96,7 @@ public ProtocolExec( final boolean authCachingDisabled) { this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy"); this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy"); - this.authenticator = new HttpAuthenticator(); + this.authenticator = new AuthenticationHandler(); this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE; this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(this.schemePortResolver); } @@ -189,6 +191,7 @@ public ClassicHttpResponse execute( authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, context); } + // This is where the actual network communication happens (eventually) final ClassicHttpResponse response = chain.proceed(request, scope); if (Method.TRACE.isSame(request.getMethod())) { @@ -218,6 +221,8 @@ public ClassicHttpResponse execute( EntityUtils.consume(responseEntity); } else { execRuntime.disconnectEndpoint(); + // We don't have any connection based AuthScheme2 implementations. + // If one existed, we'd have think about how to handle it if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS && proxyAuthExchange.isConnectionBased()) { if (LOG.isDebugEnabled()) { @@ -265,11 +270,12 @@ private boolean needAuthentication( final HttpHost target, final String pathPrefix, final HttpResponse response, - final HttpClientContext context) { - final RequestConfig config = context.getRequestConfigOrDefault(); + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { + final RequestConfig config = context.getRequestConfigOrDefault(); if (config.isAuthenticationEnabled()) { final boolean targetAuthRequested = authenticator.isChallenged( target, ChallengeType.TARGET, response, targetAuthExchange, context); + final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange); if (authCacheKeeper != null) { if (targetAuthRequested) { @@ -281,6 +287,7 @@ private boolean needAuthentication( final boolean proxyAuthRequested = authenticator.isChallenged( proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -290,8 +297,8 @@ private boolean needAuthentication( } } - if (targetAuthRequested) { - final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response, + if (targetAuthRequested || targetMutualAuthRequired) { + final boolean updated = authenticator.handleResponse(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); if (authCacheKeeper != null) { @@ -300,8 +307,8 @@ private boolean needAuthentication( return updated; } - if (proxyAuthRequested) { - final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, + if (proxyAuthRequested || proxyMutualAuthRequired) { + final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java index a4657a26ab..5d0a6f747b 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java @@ -43,10 +43,10 @@ import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; import org.apache.hc.client5.http.impl.DefaultClientConnectionReuseStrategy; +import org.apache.hc.client5.http.impl.auth.AuthenticationHandler; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory; import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory; -import org.apache.hc.client5.http.impl.auth.HttpAuthenticator; import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory; import org.apache.hc.client5.http.io.ManagedHttpClientConnection; import org.apache.hc.client5.http.protocol.HttpClientContext; @@ -84,7 +84,7 @@ public class ProxyClient { private final HttpProcessor httpProcessor; private final HttpRequestExecutor requestExec; private final AuthenticationStrategy proxyAuthStrategy; - private final HttpAuthenticator authenticator; + private final AuthenticationHandler authenticator; private final AuthExchange proxyAuthExchange; private final Lookup authSchemeRegistry; private final ConnectionReuseStrategy reuseStrategy; @@ -109,7 +109,7 @@ public ProxyClient( new RequestTargetHost(), new RequestClientConnControl(), new RequestUserAgent()); this.requestExec = new HttpRequestExecutor(); this.proxyAuthStrategy = new DefaultAuthenticationStrategy(); - this.authenticator = new HttpAuthenticator(); + this.authenticator = new AuthenticationHandler(); this.proxyAuthExchange = new AuthExchange(); this.authSchemeRegistry = RegistryBuilder.create() .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE) @@ -175,8 +175,9 @@ public Socket tunnel( if (status < 200) { throw new HttpException("Unexpected response to CONNECT request: " + response); } - if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context)) { - if (this.authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, + if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context) + || authenticator.isChallengeExpected(proxyAuthExchange)) { + if (this.authenticator.handleResponse(proxy, ChallengeType.PROXY, response, this.proxyAuthStrategy, this.proxyAuthExchange, context)) { // Retry request if (this.reuseStrategy.keepAlive(connect, response, context)) { diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestAuthenticationHandler.java similarity index 91% rename from httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java rename to httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestAuthenticationHandler.java index 9107e88016..272342be51 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestAuthenticationHandler.java @@ -38,6 +38,7 @@ import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.auth.StandardAuthScheme; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; @@ -59,8 +60,7 @@ import org.mockito.Answers; import org.mockito.Mockito; -@SuppressWarnings({"boxing","static-access"}) -class TestHttpAuthenticator { +class TestAuthenticationHandler { @AuthStateCacheable abstract class CacheableAuthState implements AuthScheme { @@ -78,7 +78,7 @@ public String getName() { private HttpHost defaultHost; private CredentialsProvider credentialsProvider; private Lookup authSchemeRegistry; - private HttpAuthenticator httpAuthenticator; + private AuthenticationHandler httpAuthenticator; @BeforeEach void setUp() { @@ -95,7 +95,7 @@ void setUp() { .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE) .build(); this.context.setAuthSchemeRegistry(this.authSchemeRegistry); - this.httpAuthenticator = new HttpAuthenticator(); + this.httpAuthenticator = new AuthenticationHandler(); } @Test @@ -150,7 +150,7 @@ void testAuthenticationNotRequestedSuccess2() { } @Test - void testAuthentication() { + void testAuthentication() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -163,7 +163,7 @@ void testAuthentication() { final DefaultAuthenticationStrategy authStrategy = new DefaultAuthenticationStrategy(); - Assertions.assertTrue(this.httpAuthenticator.updateAuthState(host, ChallengeType.TARGET, response, authStrategy, + Assertions.assertTrue(this.httpAuthenticator.handleResponse(host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); Assertions.assertEquals(AuthExchange.State.CHALLENGED, this.authExchange.getState()); @@ -179,7 +179,7 @@ void testAuthentication() { } @Test - void testAuthenticationCredentialsForBasic() { + void testAuthenticationCredentialsForBasic() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); @@ -192,7 +192,7 @@ void testAuthenticationCredentialsForBasic() { final DefaultAuthenticationStrategy authStrategy = new DefaultAuthenticationStrategy(); - Assertions.assertTrue(this.httpAuthenticator.updateAuthState(host, ChallengeType.TARGET, response, authStrategy, + Assertions.assertTrue(this.httpAuthenticator.handleResponse(host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); Assertions.assertEquals(AuthExchange.State.CHALLENGED, this.authExchange.getState()); @@ -205,18 +205,18 @@ void testAuthenticationCredentialsForBasic() { } @Test - void testAuthenticationNoChallenges() { + void testAuthenticationNoChallenges() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); final DefaultAuthenticationStrategy authStrategy = new DefaultAuthenticationStrategy(); - Assertions.assertFalse(this.httpAuthenticator.updateAuthState( + Assertions.assertFalse(this.httpAuthenticator.handleResponse( host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); } @Test - void testAuthenticationNoSupportedChallenges() { + void testAuthenticationNoSupportedChallenges() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, "This realm=\"test\"")); @@ -224,12 +224,12 @@ void testAuthenticationNoSupportedChallenges() { final DefaultAuthenticationStrategy authStrategy = new DefaultAuthenticationStrategy(); - Assertions.assertFalse(this.httpAuthenticator.updateAuthState( + Assertions.assertFalse(this.httpAuthenticator.handleResponse( host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); } @Test - void testAuthenticationNoCredentials() { + void testAuthenticationNoCredentials() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -237,12 +237,12 @@ void testAuthenticationNoCredentials() { final DefaultAuthenticationStrategy authStrategy = new DefaultAuthenticationStrategy(); - Assertions.assertFalse(this.httpAuthenticator.updateAuthState( + Assertions.assertFalse(this.httpAuthenticator.handleResponse( host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); } @Test - void testAuthenticationFailed() { + void testAuthenticationFailed() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -253,14 +253,14 @@ void testAuthenticationFailed() { final DefaultAuthenticationStrategy authStrategy = new DefaultAuthenticationStrategy(); - Assertions.assertFalse(this.httpAuthenticator.updateAuthState( + Assertions.assertFalse(this.httpAuthenticator.handleResponse( host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); Assertions.assertEquals(AuthExchange.State.FAILURE, this.authExchange.getState()); } @Test - void testAuthenticationFailedPreviously() { + void testAuthenticationFailedPreviously() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -270,14 +270,14 @@ void testAuthenticationFailedPreviously() { final DefaultAuthenticationStrategy authStrategy = new DefaultAuthenticationStrategy(); - Assertions.assertFalse(this.httpAuthenticator.updateAuthState( + Assertions.assertFalse(this.httpAuthenticator.handleResponse( host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); Assertions.assertEquals(AuthExchange.State.FAILURE, this.authExchange.getState()); } @Test - void testAuthenticationFailure() { + void testAuthenticationFailure() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -289,13 +289,13 @@ void testAuthenticationFailure() { this.authExchange.setState(AuthExchange.State.CHALLENGED); this.authExchange.select(new BasicScheme()); - Assertions.assertFalse(this.httpAuthenticator.updateAuthState( + Assertions.assertFalse(this.httpAuthenticator.handleResponse( host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); Assertions.assertEquals(AuthExchange.State.FAILURE, this.authExchange.getState()); } @Test - void testAuthenticationHandshaking() { + void testAuthenticationHandshaking() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -307,14 +307,14 @@ void testAuthenticationHandshaking() { this.authExchange.setState(AuthExchange.State.CHALLENGED); this.authExchange.select(new DigestScheme()); - Assertions.assertTrue(this.httpAuthenticator.updateAuthState( + Assertions.assertTrue(this.httpAuthenticator.handleResponse( host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); Assertions.assertEquals(AuthExchange.State.HANDSHAKE, this.authExchange.getState()); } @Test - void testAuthenticationNoMatchingChallenge() { + void testAuthenticationNoMatchingChallenge() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"1234\"")); @@ -329,7 +329,7 @@ void testAuthenticationNoMatchingChallenge() { this.authExchange.setState(AuthExchange.State.CHALLENGED); this.authExchange.select(new BasicScheme()); - Assertions.assertTrue(this.httpAuthenticator.updateAuthState( + Assertions.assertTrue(this.httpAuthenticator.handleResponse( host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); Assertions.assertEquals(AuthExchange.State.CHALLENGED, this.authExchange.getState()); @@ -342,7 +342,7 @@ void testAuthenticationNoMatchingChallenge() { } @Test - void testAuthenticationException() { + void testAuthenticationException() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, "blah blah blah")); @@ -351,7 +351,7 @@ void testAuthenticationException() { final DefaultAuthenticationStrategy authStrategy = new DefaultAuthenticationStrategy(); - Assertions.assertFalse(this.httpAuthenticator.updateAuthState( + Assertions.assertFalse(this.httpAuthenticator.handleResponse( host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); Assertions.assertEquals(AuthExchange.State.UNCHALLENGED, this.authExchange.getState());