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