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  
28  package org.apache.hc.client5.http.impl.classic;
29  
30  import java.io.IOException;
31  import java.util.Iterator;
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.auth.AuthExchange;
37  import org.apache.hc.client5.http.auth.AuthenticationException;
38  import org.apache.hc.client5.http.auth.ChallengeType;
39  import org.apache.hc.client5.http.auth.MalformedChallengeException;
40  import org.apache.hc.client5.http.classic.ExecChain;
41  import org.apache.hc.client5.http.classic.ExecChainHandler;
42  import org.apache.hc.client5.http.classic.ExecRuntime;
43  import org.apache.hc.client5.http.config.RequestConfig;
44  import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
45  import org.apache.hc.client5.http.impl.RequestSupport;
46  import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
47  import org.apache.hc.client5.http.impl.auth.AuthenticationHandler;
48  import org.apache.hc.client5.http.protocol.HttpClientContext;
49  import org.apache.hc.core5.annotation.Contract;
50  import org.apache.hc.core5.annotation.Internal;
51  import org.apache.hc.core5.annotation.ThreadingBehavior;
52  import org.apache.hc.core5.http.ClassicHttpRequest;
53  import org.apache.hc.core5.http.ClassicHttpResponse;
54  import org.apache.hc.core5.http.Header;
55  import org.apache.hc.core5.http.HttpEntity;
56  import org.apache.hc.core5.http.HttpException;
57  import org.apache.hc.core5.http.HttpHeaders;
58  import org.apache.hc.core5.http.HttpHost;
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.io.entity.EntityUtils;
63  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
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 classic 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 4.3
79   */
80  @Contract(threading = ThreadingBehavior.STATELESS)
81  @Internal
82  public final class ProtocolExec implements ExecChainHandler {
83  
84      private static final Logger LOG = LoggerFactory.getLogger(ProtocolExec.class);
85  
86      private final AuthenticationStrategy targetAuthStrategy;
87      private final AuthenticationStrategy proxyAuthStrategy;
88      private final AuthenticationHandler authenticator;
89      private final SchemePortResolver schemePortResolver;
90      private final AuthCacheKeeper authCacheKeeper;
91  
92      public ProtocolExec(
93              final AuthenticationStrategy targetAuthStrategy,
94              final AuthenticationStrategy proxyAuthStrategy,
95              final SchemePortResolver schemePortResolver,
96              final boolean authCachingDisabled) {
97          this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy");
98          this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
99          this.authenticator = new AuthenticationHandler();
100         this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE;
101         this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(this.schemePortResolver);
102     }
103 
104     @Override
105     public ClassicHttpResponse execute(
106             final ClassicHttpRequest userRequest,
107             final ExecChain.Scope scope,
108             final ExecChain chain) throws IOException, HttpException {
109         Args.notNull(userRequest, "HTTP request");
110         Args.notNull(scope, "Scope");
111 
112         if (Method.CONNECT.isSame(userRequest.getMethod())) {
113             throw new ProtocolException("Direct execution of CONNECT is not allowed");
114         }
115 
116         final String exchangeId = scope.exchangeId;
117         final HttpRoute route = scope.route;
118         final HttpClientContext context = scope.clientContext;
119         final ExecRuntime execRuntime = scope.execRuntime;
120 
121         final HttpHost routeTarget = route.getTargetHost();
122         final HttpHost proxy = route.getProxyHost();
123 
124         try {
125             final ClassicHttpRequest request;
126             if (proxy != null && !route.isTunnelled()) {
127                 final ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.copy(userRequest);
128                 if (requestBuilder.getAuthority() == null) {
129                     requestBuilder.setAuthority(new URIAuthority(routeTarget));
130                 }
131                 requestBuilder.setAbsoluteRequestUri(true);
132                 request = requestBuilder.build();
133             } else {
134                 request = userRequest;
135             }
136 
137             // Ensure the request has a scheme and an authority
138             if (request.getScheme() == null) {
139                 request.setScheme(routeTarget.getSchemeName());
140             }
141             if (request.getAuthority() == null) {
142                 request.setAuthority(new URIAuthority(routeTarget));
143             }
144 
145             final URIAuthority authority = request.getAuthority();
146             if (authority.getUserInfo() != null) {
147                 throw new ProtocolException("Request URI authority contains deprecated userinfo component");
148             }
149 
150             final HttpHost target = new HttpHost(
151                     request.getScheme(),
152                     authority.getHostName(),
153                     schemePortResolver.resolve(request.getScheme(), authority));
154             final String pathPrefix = RequestSupport.extractPathPrefix(request);
155 
156             final AuthExchange targetAuthExchange = context.getAuthExchange(target);
157             final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange();
158 
159             if (!targetAuthExchange.isConnectionBased() &&
160                     targetAuthExchange.getPathPrefix() != null &&
161                     !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) {
162                 // force re-authentication if the current path prefix does not match
163                 // that of the previous authentication exchange.
164                 targetAuthExchange.reset();
165             }
166             if (targetAuthExchange.getPathPrefix() == null) {
167                 targetAuthExchange.setPathPrefix(pathPrefix);
168             }
169 
170             if (authCacheKeeper != null) {
171                 authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, context);
172                 if (proxy != null) {
173                     authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context);
174                 }
175             }
176 
177             RequestEntityProxy.enhance(request);
178 
179             for (;;) {
180 
181                 if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
182                     if (LOG.isDebugEnabled()) {
183                         LOG.debug("{} target auth state: {}", exchangeId, targetAuthExchange.getState());
184                     }
185                     authenticator.addAuthResponse(target, ChallengeType.TARGET, request, targetAuthExchange, context);
186                 }
187                 if (!request.containsHeader(HttpHeaders.PROXY_AUTHORIZATION) && !route.isTunnelled()) {
188                     if (LOG.isDebugEnabled()) {
189                         LOG.debug("{} proxy auth state: {}", exchangeId, proxyAuthExchange.getState());
190                     }
191                     authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, context);
192                 }
193 
194                 // This is where the actual network communication happens (eventually)
195                 final ClassicHttpResponse response = chain.proceed(request, scope);
196 
197                 if (Method.TRACE.isSame(request.getMethod())) {
198                     // Do not perform authentication for TRACE request
199                     ResponseEntityProxy.enhance(response, execRuntime);
200                     return response;
201                 }
202                 final HttpEntity requestEntity = request.getEntity();
203                 if (requestEntity != null && !requestEntity.isRepeatable()) {
204                     if (LOG.isDebugEnabled()) {
205                         LOG.debug("{} Cannot retry non-repeatable request", exchangeId);
206                     }
207                     ResponseEntityProxy.enhance(response, execRuntime);
208                     return response;
209                 }
210                 if (needAuthentication(
211                         targetAuthExchange,
212                         proxyAuthExchange,
213                         proxy != null ? proxy : target,
214                         target,
215                         pathPrefix,
216                         response,
217                         context)) {
218                     // Make sure the response body is fully consumed, if present
219                     final HttpEntity responseEntity = response.getEntity();
220                     if (execRuntime.isConnectionReusable()) {
221                         EntityUtils.consume(responseEntity);
222                     } else {
223                         execRuntime.disconnectEndpoint();
224                         if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
225                                 && proxyAuthExchange.isConnectionBased()) {
226                             if (LOG.isDebugEnabled()) {
227                                 LOG.debug("{} resetting proxy auth state", exchangeId);
228                             }
229                             proxyAuthExchange.reset();
230                         }
231                         if (targetAuthExchange.getState() == AuthExchange.State.SUCCESS
232                                 && targetAuthExchange.isConnectionBased()) {
233                             if (LOG.isDebugEnabled()) {
234                                 LOG.debug("{} resetting target auth state", exchangeId);
235                             }
236                             targetAuthExchange.reset();
237                         }
238                     }
239                     // Reset request headers
240                     final ClassicHttpRequest original = scope.originalRequest;
241                     request.setHeaders();
242                     for (final Iterator<Header> it = original.headerIterator(); it.hasNext(); ) {
243                         request.addHeader(it.next());
244                     }
245                 } else {
246                     ResponseEntityProxy.enhance(response, execRuntime);
247                     return response;
248                 }
249             }
250         } catch (final HttpException ex) {
251             execRuntime.discardEndpoint();
252             throw ex;
253         } catch (final RuntimeException | IOException ex) {
254             execRuntime.discardEndpoint();
255             for (final AuthExchange authExchange : context.getAuthExchanges().values()) {
256                 if (authExchange.isConnectionBased()) {
257                     authExchange.reset();
258                 }
259             }
260             throw ex;
261         }
262     }
263 
264     private boolean needAuthentication(
265             final AuthExchange targetAuthExchange,
266             final AuthExchange proxyAuthExchange,
267             final HttpHost proxy,
268             final HttpHost target,
269             final String pathPrefix,
270             final HttpResponse response,
271             final HttpClientContext context) throws AuthenticationException, MalformedChallengeException {
272                 final RequestConfig config = context.getRequestConfigOrDefault();
273         if (config.isAuthenticationEnabled()) {
274             final boolean targetAuthRequested = authenticator.isChallenged(
275                     target, ChallengeType.TARGET, response, targetAuthExchange, context);
276             final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange);
277 
278             if (authCacheKeeper != null) {
279                 if (targetAuthRequested) {
280                     authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context);
281                 } else {
282                     authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context);
283                 }
284             }
285 
286             final boolean proxyAuthRequested = authenticator.isChallenged(
287                     proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
288             final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange);
289 
290             if (authCacheKeeper != null) {
291                 if (proxyAuthRequested) {
292                     authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
293                 } else {
294                     authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
295                 }
296             }
297 
298             if (targetAuthRequested || targetMutualAuthRequired) {
299                 final boolean updated = authenticator.handleResponse(target, ChallengeType.TARGET, response,
300                         targetAuthStrategy, targetAuthExchange, context);
301 
302                 if (authCacheKeeper != null) {
303                     authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
304                 }
305 
306                 return updated;
307             }
308             if (proxyAuthRequested || proxyMutualAuthRequired) {
309                 final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
310                         proxyAuthStrategy, proxyAuthExchange, context);
311 
312                 if (authCacheKeeper != null) {
313                     authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
314                 }
315 
316                 return updated;
317             }
318         }
319         return false;
320     }
321 
322 }