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.auth.CredentialsProvider;
39  import org.apache.hc.client5.http.auth.CredentialsStore;
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.AuthSupport;
45  import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
46  import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
47  import org.apache.hc.client5.http.protocol.HttpClientContext;
48  import org.apache.hc.client5.http.routing.RoutingSupport;
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.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 classic 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 4.3
81   */
82  @Contract(threading = ThreadingBehavior.STATELESS)
83  @Internal
84  public final class ProtocolExec implements ExecChainHandler {
85  
86      private static final Logger LOG = LoggerFactory.getLogger(ProtocolExec.class);
87  
88      private final HttpProcessor httpProcessor;
89      private final AuthenticationStrategy targetAuthStrategy;
90      private final AuthenticationStrategy proxyAuthStrategy;
91      private final HttpAuthenticator authenticator;
92      private final SchemePortResolver schemePortResolver;
93  
94      public ProtocolExec(
95              final HttpProcessor httpProcessor,
96              final AuthenticationStrategy targetAuthStrategy,
97              final AuthenticationStrategy proxyAuthStrategy,
98              final SchemePortResolver schemePortResolver) {
99          this.httpProcessor = Args.notNull(httpProcessor, "HTTP protocol processor");
100         this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy");
101         this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
102         this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE;
103         this.authenticator = new HttpAuthenticator();
104     }
105 
106     public ProtocolExec(
107             final HttpProcessor httpProcessor,
108             final AuthenticationStrategy targetAuthStrategy,
109             final AuthenticationStrategy proxyAuthStrategy) {
110         this(httpProcessor, targetAuthStrategy, proxyAuthStrategy, null);
111     }
112 
113     @Override
114     public ClassicHttpResponse execute(
115             final ClassicHttpRequest userRequest,
116             final ExecChain.Scope scope,
117             final ExecChain chain) throws IOException, HttpException {
118         Args.notNull(userRequest, "HTTP request");
119         Args.notNull(scope, "Scope");
120 
121         if (Method.CONNECT.isSame(userRequest.getMethod())) {
122             throw new ProtocolException("Direct execution of CONNECT is not allowed");
123         }
124 
125         final String exchangeId = scope.exchangeId;
126         final HttpRoute route = scope.route;
127         final HttpClientContext context = scope.clientContext;
128         final ExecRuntime execRuntime = scope.execRuntime;
129 
130         final HttpHost routeTarget = route.getTargetHost();
131         final HttpHost proxy = route.getProxyHost();
132 
133         try {
134             final ClassicHttpRequest request;
135             if (proxy != null && !route.isTunnelled()) {
136                 final ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.copy(userRequest);
137                 if (requestBuilder.getAuthority() == null) {
138                     requestBuilder.setAuthority(new URIAuthority(routeTarget));
139                 }
140                 requestBuilder.setAbsoluteRequestUri(true);
141                 request = requestBuilder.build();
142             } else {
143                 request = userRequest;
144             }
145 
146             // Ensure the request has a scheme and an authority
147             if (request.getScheme() == null) {
148                 request.setScheme(routeTarget.getSchemeName());
149             }
150             if (request.getAuthority() == null) {
151                 request.setAuthority(new URIAuthority(routeTarget));
152             }
153 
154             final URIAuthority authority = request.getAuthority();
155             final CredentialsProvider credsProvider = context.getCredentialsProvider();
156             if (credsProvider instanceof CredentialsStore) {
157                 AuthSupport.extractFromAuthority(request.getScheme(), authority, (CredentialsStore) credsProvider);
158             }
159 
160             final HttpHost target = RoutingSupport.normalize(
161                     new HttpHost(request.getScheme(), request.getAuthority()),
162                     schemePortResolver);
163 
164             final AuthExchange targetAuthExchange = context.getAuthExchange(target);
165             final AuthExchange proxyAuthExchange = proxy != AuthExchanges="jxr_keyword">null ? context.getAuthExchange(proxy) : new AuthExchange();
166 
167             RequestEntityProxy.enhance(request);
168 
169             for (;;) {
170 
171                 // Run request protocol interceptors
172                 context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
173                 context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
174 
175                 httpProcessor.process(request, request.getEntity(), context);
176 
177                 if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
178                     if (LOG.isDebugEnabled()) {
179                         LOG.debug("{} target auth state: {}", exchangeId, targetAuthExchange.getState());
180                     }
181                     authenticator.addAuthResponse(target, ChallengeType.TARGET, request, targetAuthExchange, context);
182                 }
183                 if (!request.containsHeader(HttpHeaders.PROXY_AUTHORIZATION) && !route.isTunnelled()) {
184                     if (LOG.isDebugEnabled()) {
185                         LOG.debug("{} proxy auth state: {}", exchangeId, proxyAuthExchange.getState());
186                     }
187                     authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, context);
188                 }
189 
190                 final ClassicHttpResponse response = chain.proceed(request, scope);
191 
192                 context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
193                 httpProcessor.process(response, response.getEntity(), context);
194 
195                 if (Method.TRACE.isSame(request.getMethod())) {
196                     // Do not perform authentication for TRACE request
197                     ResponseEntityProxy.enhance(response, execRuntime);
198                     return response;
199                 }
200                 final HttpEntity requestEntity = request.getEntity();
201                 if (requestEntity != null && !requestEntity.isRepeatable()) {
202                     if (LOG.isDebugEnabled()) {
203                         LOG.debug("{} Cannot retry non-repeatable request", exchangeId);
204                     }
205                     ResponseEntityProxy.enhance(response, execRuntime);
206                     return response;
207                 }
208                 if (needAuthentication(targetAuthExchange, proxyAuthExchange, route, request, response, context)) {
209                     // Make sure the response body is fully consumed, if present
210                     final HttpEntity responseEntity = response.getEntity();
211                     if (execRuntime.isConnectionReusable()) {
212                         EntityUtils.consume(responseEntity);
213                     } else {
214                         execRuntime.disconnectEndpoint();
215                         if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
216                                 && proxyAuthExchange.isConnectionBased()) {
217                             if (LOG.isDebugEnabled()) {
218                                 LOG.debug("{} resetting proxy auth state", exchangeId);
219                             }
220                             proxyAuthExchange.reset();
221                         }
222                         if (targetAuthExchange.getState() == AuthExchange.State.SUCCESS
223                                 && targetAuthExchange.isConnectionBased()) {
224                             if (LOG.isDebugEnabled()) {
225                                 LOG.debug("{} resetting target auth state", exchangeId);
226                             }
227                             targetAuthExchange.reset();
228                         }
229                     }
230                     // Reset request headers
231                     final ClassicHttpRequest original = scope.originalRequest;
232                     request.setHeaders();
233                     for (final Iterator<Header> it = original.headerIterator(); it.hasNext(); ) {
234                         request.addHeader(it.next());
235                     }
236                 } else {
237                     ResponseEntityProxy.enhance(response, execRuntime);
238                     return response;
239                 }
240             }
241         } catch (final HttpException ex) {
242             execRuntime.discardEndpoint();
243             throw ex;
244         } catch (final RuntimeException | IOException ex) {
245             execRuntime.discardEndpoint();
246             for (final AuthExchange authExchange : context.getAuthExchanges().values()) {
247                 if (authExchange.isConnectionBased()) {
248                     authExchange.reset();
249                 }
250             }
251             throw ex;
252         }
253     }
254 
255     private boolean needAuthentication(
256             final AuthExchange targetAuthExchange,
257             final AuthExchange proxyAuthExchange,
258             final HttpRoute route,
259             final ClassicHttpRequest request,
260             final HttpResponse response,
261             final HttpClientContext context) {
262         final RequestConfig config = context.getRequestConfig();
263         if (config.isAuthenticationEnabled()) {
264             final HttpHost target = AuthSupport.resolveAuthTarget(request, route);
265             final boolean targetAuthRequested = authenticator.isChallenged(
266                     target, ChallengeType.TARGET, response, targetAuthExchange, context);
267 
268             HttpHost proxy = route.getProxyHost();
269             // if proxy is not set use target host instead
270             if (proxy == null) {
271                 proxy = route.getTargetHost();
272             }
273             final boolean proxyAuthRequested = authenticator.isChallenged(
274                     proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
275 
276             if (targetAuthRequested) {
277                 return authenticator.updateAuthState(target, ChallengeType.TARGET, response,
278                         targetAuthStrategy, targetAuthExchange, context);
279             }
280             if (proxyAuthRequested) {
281                 return authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
282                         proxyAuthStrategy, proxyAuthExchange, context);
283             }
284         }
285         return false;
286     }
287 
288 }