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