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