View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.async;
28  
29  import java.io.IOException;
30  import java.util.Iterator;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  
33  import org.apache.hc.client5.http.AuthenticationStrategy;
34  import org.apache.hc.client5.http.HttpRoute;
35  import org.apache.hc.client5.http.SchemePortResolver;
36  import org.apache.hc.client5.http.async.AsyncExecCallback;
37  import org.apache.hc.client5.http.async.AsyncExecChain;
38  import org.apache.hc.client5.http.async.AsyncExecChainHandler;
39  import org.apache.hc.client5.http.async.AsyncExecRuntime;
40  import org.apache.hc.client5.http.auth.AuthExchange;
41  import org.apache.hc.client5.http.auth.AuthenticationException;
42  import org.apache.hc.client5.http.auth.ChallengeType;
43  import org.apache.hc.client5.http.auth.MalformedChallengeException;
44  import org.apache.hc.client5.http.config.RequestConfig;
45  import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
46  import org.apache.hc.client5.http.impl.RequestSupport;
47  import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
48  import org.apache.hc.client5.http.impl.auth.AuthenticationHandler;
49  import org.apache.hc.client5.http.protocol.HttpClientContext;
50  import org.apache.hc.core5.annotation.Contract;
51  import org.apache.hc.core5.annotation.Internal;
52  import org.apache.hc.core5.annotation.ThreadingBehavior;
53  import org.apache.hc.core5.http.EntityDetails;
54  import org.apache.hc.core5.http.Header;
55  import org.apache.hc.core5.http.HttpException;
56  import org.apache.hc.core5.http.HttpHeaders;
57  import org.apache.hc.core5.http.HttpHost;
58  import org.apache.hc.core5.http.HttpRequest;
59  import org.apache.hc.core5.http.HttpResponse;
60  import org.apache.hc.core5.http.Method;
61  import org.apache.hc.core5.http.ProtocolException;
62  import org.apache.hc.core5.http.nio.AsyncDataConsumer;
63  import org.apache.hc.core5.http.nio.AsyncEntityProducer;
64  import org.apache.hc.core5.http.support.BasicRequestBuilder;
65  import org.apache.hc.core5.net.URIAuthority;
66  import org.apache.hc.core5.util.Args;
67  import org.slf4j.Logger;
68  import org.slf4j.LoggerFactory;
69  
70  /**
71   * Request execution handler in the asynchronous request execution chain
72   * that is responsible for implementation of HTTP specification requirements.
73   * <p>
74   * Further responsibilities such as communication with the opposite
75   * endpoint is delegated to the next executor in the request execution
76   * chain.
77   * </p>
78   *
79   * @since 5.0
80   */
81  @Contract(threading = ThreadingBehavior.STATELESS)
82  @Internal
83  public final class AsyncProtocolExec implements AsyncExecChainHandler {
84  
85      private static final Logger LOG = LoggerFactory.getLogger(AsyncProtocolExec.class);
86  
87      private final AuthenticationStrategy targetAuthStrategy;
88      private final AuthenticationStrategy proxyAuthStrategy;
89      private final AuthenticationHandler authenticator;
90      private final SchemePortResolver schemePortResolver;
91      private final AuthCacheKeeper authCacheKeeper;
92  
93      AsyncProtocolExec(
94              final AuthenticationStrategy targetAuthStrategy,
95              final AuthenticationStrategy proxyAuthStrategy,
96              final SchemePortResolver schemePortResolver,
97              final boolean authCachingDisabled) {
98          this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy");
99          this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
100         this.authenticator = new AuthenticationHandler();
101         this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE;
102         this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(this.schemePortResolver);
103     }
104 
105     @Override
106     public void execute(
107             final HttpRequest userRequest,
108             final AsyncEntityProducer entityProducer,
109             final AsyncExecChain.Scope scope,
110             final AsyncExecChain chain,
111             final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
112 
113         if (Method.CONNECT.isSame(userRequest.getMethod())) {
114             throw new ProtocolException("Direct execution of CONNECT is not allowed");
115         }
116 
117         final HttpRoute route = scope.route;
118         final HttpHost routeTarget = route.getTargetHost();
119         final HttpHost proxy = route.getProxyHost();
120         final HttpClientContext clientContext = scope.clientContext;
121 
122         final HttpRequest request;
123         if (proxy != null && !route.isTunnelled()) {
124             final BasicRequestBuilder requestBuilder = BasicRequestBuilder.copy(userRequest);
125             if (requestBuilder.getAuthority() == null) {
126                 requestBuilder.setAuthority(new URIAuthority(routeTarget));
127             }
128             requestBuilder.setAbsoluteRequestUri(true);
129             request = requestBuilder.build();
130         } else {
131             request = userRequest;
132         }
133 
134         // Ensure the request has a scheme and an authority
135         if (request.getScheme() == null) {
136             request.setScheme(routeTarget.getSchemeName());
137         }
138         if (request.getAuthority() == null) {
139             request.setAuthority(new URIAuthority(routeTarget));
140         }
141 
142         final URIAuthority authority = request.getAuthority();
143         if (authority.getUserInfo() != null) {
144             throw new ProtocolException("Request URI authority contains deprecated userinfo component");
145         }
146 
147         final HttpHost target = new HttpHost(
148                 request.getScheme(),
149                 authority.getHostName(),
150                 schemePortResolver.resolve(request.getScheme(), authority));
151         final String pathPrefix = RequestSupport.extractPathPrefix(request);
152         final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target);
153         final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange();
154 
155         if (!targetAuthExchange.isConnectionBased() &&
156                 targetAuthExchange.getPathPrefix() != null &&
157                 !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) {
158             // force re-authentication if the current path prefix does not match
159             // that of the previous authentication exchange.
160             targetAuthExchange.reset();
161         }
162         if (targetAuthExchange.getPathPrefix() == null) {
163             targetAuthExchange.setPathPrefix(pathPrefix);
164         }
165 
166         if (authCacheKeeper != null) {
167             authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, clientContext);
168             if (proxy != null) {
169                 authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext);
170             }
171         }
172 
173         final AtomicBoolean challenged = new AtomicBoolean(false);
174         internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange,
175                 challenged, request, entityProducer, scope, chain, asyncExecCallback);
176     }
177 
178     private void internalExecute(
179             final HttpHost target,
180             final String pathPrefix,
181             final AuthExchange targetAuthExchange,
182             final AuthExchange proxyAuthExchange,
183             final AtomicBoolean challenged,
184             final HttpRequest request,
185             final AsyncEntityProducer entityProducer,
186             final AsyncExecChain.Scope scope,
187             final AsyncExecChain chain,
188             final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
189         final String exchangeId = scope.exchangeId;
190         final HttpRoute route = scope.route;
191         final HttpClientContext clientContext = scope.clientContext;
192         final AsyncExecRuntime execRuntime = scope.execRuntime;
193 
194         final HttpHost proxy = route.getProxyHost();
195 
196         if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
197             if (LOG.isDebugEnabled()) {
198                 LOG.debug("{} target auth state: {}", exchangeId, targetAuthExchange.getState());
199             }
200             authenticator.addAuthResponse(target, ChallengeType.TARGET, request, targetAuthExchange, clientContext);
201         }
202         if (!request.containsHeader(HttpHeaders.PROXY_AUTHORIZATION) && !route.isTunnelled()) {
203             if (LOG.isDebugEnabled()) {
204                 LOG.debug("{} proxy auth state: {}", exchangeId, proxyAuthExchange.getState());
205             }
206             authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, clientContext);
207         }
208 
209         chain.proceed(request, entityProducer, scope, new AsyncExecCallback() {
210 
211             @Override
212             public AsyncDataConsumer handleResponse(
213                     final HttpResponse response,
214                     final EntityDetails entityDetails) throws HttpException, IOException {
215 
216                 if (Method.TRACE.isSame(request.getMethod())) {
217                     // Do not perform authentication for TRACE request
218                     return asyncExecCallback.handleResponse(response, entityDetails);
219                 }
220                 if (needAuthentication(
221                         targetAuthExchange,
222                         proxyAuthExchange,
223                         proxy != null ? proxy : target,
224                         target,
225                         pathPrefix,
226                         response,
227                         clientContext)) {
228                     challenged.set(true);
229                     return null;
230                 }
231                 challenged.set(false);
232                 return asyncExecCallback.handleResponse(response, entityDetails);
233             }
234 
235             @Override
236             public void handleInformationResponse(
237                     final HttpResponse response) throws HttpException, IOException {
238                 asyncExecCallback.handleInformationResponse(response);
239             }
240 
241             @Override
242             public void completed() {
243                 if (!execRuntime.isEndpointConnected()) {
244                     if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
245                             && proxyAuthExchange.isConnectionBased()) {
246                         if (LOG.isDebugEnabled()) {
247                             LOG.debug("{} resetting proxy auth state", exchangeId);
248                         }
249                         proxyAuthExchange.reset();
250                     }
251                     if (targetAuthExchange.getState() == AuthExchange.State.SUCCESS
252                             && targetAuthExchange.isConnectionBased()) {
253                         if (LOG.isDebugEnabled()) {
254                             LOG.debug("{} resetting target auth state", exchangeId);
255                         }
256                         targetAuthExchange.reset();
257                     }
258                 }
259 
260                 if (challenged.get()) {
261                     if (entityProducer != null && !entityProducer.isRepeatable()) {
262                         if (LOG.isDebugEnabled()) {
263                             LOG.debug("{} cannot retry non-repeatable request", exchangeId);
264                         }
265                         asyncExecCallback.completed();
266                     } else {
267                         // Reset request headers
268                         final HttpRequest original = scope.originalRequest;
269                         request.setHeaders();
270                         for (final Iterator<Header> it = original.headerIterator(); it.hasNext(); ) {
271                             request.addHeader(it.next());
272                         }
273                         try {
274                             if (entityProducer != null) {
275                                 entityProducer.releaseResources();
276                             }
277                             internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange,
278                                     challenged, request, entityProducer, scope, chain, asyncExecCallback);
279                         } catch (final HttpException | IOException ex) {
280                             asyncExecCallback.failed(ex);
281                         }
282                     }
283                 } else {
284                     asyncExecCallback.completed();
285                 }
286             }
287 
288             @Override
289             public void failed(final Exception cause) {
290                 if (cause instanceof IOException || cause instanceof RuntimeException) {
291                     for (final AuthExchange authExchange : clientContext.getAuthExchanges().values()) {
292                         if (authExchange.isConnectionBased()) {
293                             authExchange.reset();
294                         }
295                     }
296                 }
297                 asyncExecCallback.failed(cause);
298             }
299 
300         });
301     }
302 
303     private boolean needAuthentication(
304             final AuthExchange targetAuthExchange,
305             final AuthExchange proxyAuthExchange,
306             final HttpHost proxy,
307             final HttpHost target,
308             final String pathPrefix,
309             final HttpResponse response,
310             final HttpClientContext context) throws AuthenticationException, MalformedChallengeException {
311         final RequestConfig config = context.getRequestConfigOrDefault();
312         if (config.isAuthenticationEnabled()) {
313             final boolean targetAuthRequested = authenticator.isChallenged(
314                     target, ChallengeType.TARGET, response, targetAuthExchange, context);
315             final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange);
316 
317             if (authCacheKeeper != null) {
318                 if (targetAuthRequested) {
319                     authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context);
320                 } else {
321                     authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context);
322                 }
323             }
324 
325             final boolean proxyAuthRequested = authenticator.isChallenged(
326                     proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
327             final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange);
328 
329             if (authCacheKeeper != null) {
330                 if (proxyAuthRequested) {
331                     authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
332                 } else {
333                     authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
334                 }
335             }
336 
337             if (targetAuthRequested || targetMutualAuthRequired) {
338                 final boolean updated = authenticator.handleResponse(target, ChallengeType.TARGET, response,
339                         targetAuthStrategy, targetAuthExchange, context);
340 
341                 if (authCacheKeeper != null) {
342                     authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
343                 }
344 
345                 return updated;
346             }
347             if (proxyAuthRequested || proxyMutualAuthRequired) {
348                 final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
349                         proxyAuthStrategy, proxyAuthExchange, context);
350 
351                 if (authCacheKeeper != null) {
352                     authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
353                 }
354 
355                 return updated;
356             }
357         }
358         return false;
359     }
360 
361 }