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