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.net.URI;
31  import java.net.URISyntaxException;
32  import java.util.Iterator;
33  import java.util.concurrent.atomic.AtomicBoolean;
34  
35  import org.apache.hc.client5.http.AuthenticationStrategy;
36  import org.apache.hc.client5.http.HttpRoute;
37  import org.apache.hc.client5.http.async.AsyncExecCallback;
38  import org.apache.hc.client5.http.async.AsyncExecChain;
39  import org.apache.hc.client5.http.async.AsyncExecChainHandler;
40  import org.apache.hc.client5.http.async.AsyncExecRuntime;
41  import org.apache.hc.client5.http.auth.AuthExchange;
42  import org.apache.hc.client5.http.auth.ChallengeType;
43  import org.apache.hc.client5.http.auth.CredentialsProvider;
44  import org.apache.hc.client5.http.auth.CredentialsStore;
45  import org.apache.hc.client5.http.config.RequestConfig;
46  import org.apache.hc.client5.http.impl.AuthSupport;
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.utils.URIUtils;
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.net.URIAuthority;
67  import org.apache.hc.core5.util.Args;
68  import org.slf4j.Logger;
69  import org.slf4j.LoggerFactory;
70  
71  /**
72   * Request execution handler in the asynchronous request execution chain
73   * that is responsible for implementation of HTTP specification requirements.
74   * <p>
75   * Further responsibilities such as communication with the opposite
76   * endpoint is delegated to the next executor in the request execution
77   * chain.
78   * </p>
79   *
80   * @since 5.0
81   */
82  @Contract(threading = ThreadingBehavior.STATELESS)
83  @Internal
84  public final class AsyncProtocolExec implements AsyncExecChainHandler {
85  
86      private static final Logger LOG = LoggerFactory.getLogger(AsyncProtocolExec.class);
87  
88      private final HttpProcessor httpProcessor;
89      private final AuthenticationStrategy targetAuthStrategy;
90      private final AuthenticationStrategy proxyAuthStrategy;
91      private final HttpAuthenticator authenticator;
92  
93      AsyncProtocolExec(
94              final HttpProcessor httpProcessor,
95              final AuthenticationStrategy targetAuthStrategy,
96              final AuthenticationStrategy proxyAuthStrategy) {
97          this.httpProcessor = Args.notNull(httpProcessor, "HTTP protocol processor");
98          this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy");
99          this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
100         this.authenticator = new HttpAuthenticator(LOG);
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 HttpClientContext clientContext = scope.clientContext;
117 
118         final HttpRequest request;
119         if (route.getProxyHost() != null && !route.isTunnelled()) {
120             try {
121                 URI uri = userRequest.getUri();
122                 if (!uri.isAbsolute()) {
123                     uri = URIUtils.rewriteURI(uri, route.getTargetHost(), true);
124                 } else {
125                     uri = URIUtils.rewriteURI(uri);
126                 }
127                 request = HttpProxyRequest.rewrite(userRequest, uri);
128             } catch (final URISyntaxException ex) {
129                 throw new ProtocolException("Invalid request URI: " + userRequest.getRequestUri(), ex);
130             }
131         } else {
132             request = userRequest;
133         }
134 
135         final URIAuthority authority = request.getAuthority();
136         if (authority != null) {
137             final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
138             if (credsProvider instanceof CredentialsStore) {
139                 AuthSupport.extractFromAuthority(request.getScheme(), authority, (CredentialsStore) credsProvider);
140             }
141         }
142 
143         final AtomicBoolean challenged = new AtomicBoolean(false);
144         internalExecute(challenged, request, entityProducer, scope, chain, asyncExecCallback);
145     }
146 
147     private void internalExecute(
148             final AtomicBoolean challenged,
149             final HttpRequest request,
150             final AsyncEntityProducer entityProducer,
151             final AsyncExecChain.Scope scope,
152             final AsyncExecChain chain,
153             final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
154         final String exchangeId = scope.exchangeId;
155         final HttpRoute route = scope.route;
156         final HttpClientContext clientContext = scope.clientContext;
157         final AsyncExecRuntime execRuntime = scope.execRuntime;
158 
159         final HttpHost target = route.getTargetHost();
160         final HttpHost proxy = route.getProxyHost();
161 
162         final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target);
163         final AuthExchange proxyAuthExchange = proxy != AuthExchange_keyword">null ? clientContext.getAuthExchange(proxy) : new AuthExchange();
164 
165         clientContext.setAttribute(HttpClientContext.HTTP_ROUTE, route);
166         clientContext.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
167         httpProcessor.process(request, entityProducer, clientContext);
168 
169         if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
170             if (LOG.isDebugEnabled()) {
171                 LOG.debug("{}: target auth state: {}", exchangeId, targetAuthExchange.getState());
172             }
173             authenticator.addAuthResponse(target, ChallengeType.TARGET, request, targetAuthExchange, clientContext);
174         }
175         if (!request.containsHeader(HttpHeaders.PROXY_AUTHORIZATION) && !route.isTunnelled()) {
176             if (LOG.isDebugEnabled()) {
177                 LOG.debug("{}: proxy auth state: {}", exchangeId, proxyAuthExchange.getState());
178             }
179             authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, clientContext);
180         }
181 
182         chain.proceed(request, entityProducer, scope, new AsyncExecCallback() {
183 
184             @Override
185             public AsyncDataConsumer handleResponse(
186                     final HttpResponse response,
187                     final EntityDetails entityDetails) throws HttpException, IOException {
188 
189                 clientContext.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
190                 httpProcessor.process(response, entityDetails, clientContext);
191 
192                 if (Method.TRACE.isSame(request.getMethod())) {
193                     // Do not perform authentication for TRACE request
194                     return asyncExecCallback.handleResponse(response, entityDetails);
195                 }
196                 if (needAuthentication(targetAuthExchange, proxyAuthExchange, route, request, response, clientContext)) {
197                     challenged.set(true);
198                     return null;
199                 }
200                 challenged.set(false);
201                 return asyncExecCallback.handleResponse(response, entityDetails);
202             }
203 
204             @Override
205             public void handleInformationResponse(
206                     final HttpResponse response) throws HttpException, IOException {
207                 asyncExecCallback.handleInformationResponse(response);
208             }
209 
210             @Override
211             public void completed() {
212                 if (!execRuntime.isEndpointConnected()) {
213                     if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
214                             && proxyAuthExchange.isConnectionBased()) {
215                         if (LOG.isDebugEnabled()) {
216                             LOG.debug("{}: resetting proxy auth state", exchangeId);
217                         }
218                         proxyAuthExchange.reset();
219                     }
220                     if (targetAuthExchange.getState() == AuthExchange.State.SUCCESS
221                             && targetAuthExchange.isConnectionBased()) {
222                         if (LOG.isDebugEnabled()) {
223                             LOG.debug("{}: resetting target auth state", exchangeId);
224                         }
225                         targetAuthExchange.reset();
226                     }
227                 }
228 
229                 if (challenged.get()) {
230                     if (entityProducer != null && !entityProducer.isRepeatable()) {
231                         if (LOG.isDebugEnabled()) {
232                             LOG.debug("{}: cannot retry non-repeatable request", exchangeId);
233                         }
234                         asyncExecCallback.completed();
235                     } else {
236                         // Reset request headers
237                         final HttpRequest original = scope.originalRequest;
238                         request.setHeaders();
239                         for (final Iterator<Header> it = original.headerIterator(); it.hasNext(); ) {
240                             request.addHeader(it.next());
241                         }
242                         try {
243                             if (entityProducer != null) {
244                                 entityProducer.releaseResources();
245                             }
246                             internalExecute(challenged, request, entityProducer, scope, chain, asyncExecCallback);
247                         } catch (final HttpException | IOException ex) {
248                             asyncExecCallback.failed(ex);
249                         }
250                     }
251                 } else {
252                     asyncExecCallback.completed();
253                 }
254             }
255 
256             @Override
257             public void failed(final Exception cause) {
258                 if (cause instanceof IOException || cause instanceof RuntimeException) {
259                     if (proxyAuthExchange.isConnectionBased()) {
260                         proxyAuthExchange.reset();
261                     }
262                     if (targetAuthExchange.isConnectionBased()) {
263                         targetAuthExchange.reset();
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 }