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