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  
32  import org.apache.hc.client5.http.AuthenticationStrategy;
33  import org.apache.hc.client5.http.EndpointInfo;
34  import org.apache.hc.client5.http.HttpRoute;
35  import org.apache.hc.client5.http.RouteTracker;
36  import org.apache.hc.client5.http.SchemePortResolver;
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.classic.ExecChain;
40  import org.apache.hc.client5.http.classic.ExecChainHandler;
41  import org.apache.hc.client5.http.classic.ExecRuntime;
42  import org.apache.hc.client5.http.config.RequestConfig;
43  import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
44  import org.apache.hc.client5.http.impl.auth.AuthenticationHandler;
45  import org.apache.hc.client5.http.impl.routing.BasicRouteDirector;
46  import org.apache.hc.client5.http.protocol.HttpClientContext;
47  import org.apache.hc.client5.http.routing.HttpRouteDirector;
48  import org.apache.hc.core5.annotation.Contract;
49  import org.apache.hc.core5.annotation.Internal;
50  import org.apache.hc.core5.annotation.ThreadingBehavior;
51  import org.apache.hc.core5.http.ClassicHttpRequest;
52  import org.apache.hc.core5.http.ClassicHttpResponse;
53  import org.apache.hc.core5.http.ConnectionReuseStrategy;
54  import org.apache.hc.core5.http.ContentType;
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.HttpRequest;
60  import org.apache.hc.core5.http.HttpStatus;
61  import org.apache.hc.core5.http.HttpVersion;
62  import org.apache.hc.core5.http.Method;
63  import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
64  import org.apache.hc.core5.http.io.entity.EntityUtils;
65  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
66  import org.apache.hc.core5.http.message.StatusLine;
67  import org.apache.hc.core5.http.protocol.HttpProcessor;
68  import org.apache.hc.core5.util.Args;
69  import org.slf4j.Logger;
70  import org.slf4j.LoggerFactory;
71  
72  /**
73   * Request execution handler in the classic request execution chain
74   * that is responsible for establishing connection to the target
75   * origin server as specified by the current connection route.
76   *
77   * @since 5.0
78   */
79  @Contract(threading = ThreadingBehavior.STATELESS)
80  @Internal
81  public final class ConnectExec implements ExecChainHandler {
82  
83      private static final Logger LOG = LoggerFactory.getLogger(ConnectExec.class);
84  
85      private final ConnectionReuseStrategy reuseStrategy;
86      private final HttpProcessor proxyHttpProcessor;
87      private final AuthenticationStrategy proxyAuthStrategy;
88      private final AuthenticationHandler authenticator;
89      private final AuthCacheKeeper authCacheKeeper;
90      private final HttpRouteDirector routeDirector;
91  
92      public ConnectExec(
93              final ConnectionReuseStrategy reuseStrategy,
94              final HttpProcessor proxyHttpProcessor,
95              final AuthenticationStrategy proxyAuthStrategy,
96              final SchemePortResolver schemePortResolver,
97              final boolean authCachingDisabled) {
98          Args.notNull(reuseStrategy, "Connection reuse strategy");
99          Args.notNull(proxyHttpProcessor, "Proxy HTTP processor");
100         Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
101         this.reuseStrategy = reuseStrategy;
102         this.proxyHttpProcessor = proxyHttpProcessor;
103         this.proxyAuthStrategy = proxyAuthStrategy;
104         this.authenticator = new AuthenticationHandler();
105         this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(schemePortResolver);
106         this.routeDirector = BasicRouteDirector.INSTANCE;
107     }
108 
109     @Override
110     public ClassicHttpResponse execute(
111             final ClassicHttpRequest request,
112             final ExecChain.Scope scope,
113             final ExecChain chain) throws IOException, HttpException {
114         Args.notNull(request, "HTTP request");
115         Args.notNull(scope, "Scope");
116 
117         final String exchangeId = scope.exchangeId;
118         final HttpRoute route = scope.route;
119         final HttpClientContext context = scope.clientContext;
120         final ExecRuntime execRuntime = scope.execRuntime;
121 
122         if (!execRuntime.isEndpointAcquired()) {
123             final Object userToken = context.getUserToken();
124             if (LOG.isDebugEnabled()) {
125                 LOG.debug("{} acquiring connection with route {}", exchangeId, route);
126             }
127             execRuntime.acquireEndpoint(exchangeId, route, userToken, context);
128         }
129         try {
130             if (!execRuntime.isEndpointConnected()) {
131                 if (LOG.isDebugEnabled()) {
132                     LOG.debug("{} opening connection {}", exchangeId, route);
133                 }
134 
135                 final RouteTracker tracker = new RouteTracker(route);
136                 int step;
137                 do {
138                     final HttpRoute fact = tracker.toRoute();
139                     step = this.routeDirector.nextStep(route, fact);
140 
141                     switch (step) {
142 
143                         case HttpRouteDirector.CONNECT_TARGET:
144                             execRuntime.connectEndpoint(context);
145                             tracker.connectTarget(route.isSecure());
146                             break;
147                         case HttpRouteDirector.CONNECT_PROXY:
148                             execRuntime.connectEndpoint(context);
149                             final HttpHost proxy = route.getProxyHost();
150                             tracker.connectProxy(proxy, route.isSecure() && !route.isTunnelled());
151                             break;
152                         case HttpRouteDirector.TUNNEL_TARGET: {
153                             final ClassicHttpResponse finalResponse = createTunnelToTarget(
154                                     exchangeId, route, request, execRuntime, context);
155                             if (finalResponse != null) {
156                                 return finalResponse;
157                             }
158                             if (LOG.isDebugEnabled()) {
159                                 LOG.debug("{} tunnel to target created.", exchangeId);
160                             }
161                             tracker.tunnelTarget(false);
162                         }
163                         break;
164 
165                         case HttpRouteDirector.TUNNEL_PROXY: {
166                             // The most simple example for this case is a proxy chain
167                             // of two proxies, where P1 must be tunnelled to P2.
168                             // route: Source -> P1 -> P2 -> Target (3 hops)
169                             // fact:  Source -> P1 -> Target       (2 hops)
170                             final int hop = fact.getHopCount() - 1; // the hop to establish
171                             final boolean secure = createTunnelToProxy(route, hop, context);
172                             if (LOG.isDebugEnabled()) {
173                                 LOG.debug("{} tunnel to proxy created.", exchangeId);
174                             }
175                             tracker.tunnelProxy(route.getHopTarget(hop), secure);
176                         }
177                         break;
178 
179                         case HttpRouteDirector.LAYER_PROTOCOL:
180                             execRuntime.upgradeTls(context);
181                             tracker.layerProtocol(route.isSecure());
182                             break;
183 
184                         case HttpRouteDirector.UNREACHABLE:
185                             throw new HttpException("Unable to establish route: " +
186                                     "planned = " + route + "; current = " + fact);
187                         case HttpRouteDirector.COMPLETE:
188                             break;
189                         default:
190                             throw new IllegalStateException("Unknown step indicator "
191                                     + step + " from RouteDirector.");
192                     }
193 
194                 } while (step > HttpRouteDirector.COMPLETE);
195             }
196             final EndpointInfo endpointInfo = execRuntime.getEndpointInfo();
197             if (endpointInfo != null) {
198                 context.setSSLSession(endpointInfo.getSslSession());
199             }
200             return chain.proceed(request, scope);
201 
202         } catch (final IOException | HttpException | RuntimeException ex) {
203             execRuntime.discardEndpoint();
204             throw ex;
205         }
206     }
207 
208     /**
209      * Creates a tunnel to the target server.
210      * The connection must be established to the (last) proxy.
211      * A CONNECT request for tunnelling through the proxy will
212      * be created and sent, the response received and checked.
213      * This method does <i>not</i> processChallenge the connection with
214      * information about the tunnel, that is left to the caller.
215      */
216     private ClassicHttpResponse createTunnelToTarget(
217             final String exchangeId,
218             final HttpRoute route,
219             final HttpRequest request,
220             final ExecRuntime execRuntime,
221             final HttpClientContext context) throws HttpException, IOException {
222 
223         final RequestConfig config = context.getRequestConfigOrDefault();
224 
225         final HttpHost target = route.getTargetHost();
226         final HttpHost proxy = route.getProxyHost();
227         final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy);
228 
229         if (authCacheKeeper != null) {
230             authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context);
231         }
232 
233         ClassicHttpResponse response = null;
234 
235         final String authority = target.toHostString();
236         final ClassicHttpRequest connect = new BasicClassicHttpRequest(Method.CONNECT, target, authority);
237         connect.setVersion(HttpVersion.HTTP_1_1);
238 
239         this.proxyHttpProcessor.process(connect, null, context);
240 
241         while (response == null) {
242             connect.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
243             this.authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, proxyAuthExchange, context);
244 
245             response = execRuntime.execute(exchangeId, connect, context);
246             this.proxyHttpProcessor.process(response, response.getEntity(), context);
247 
248             final int status = response.getCode();
249             if (status < HttpStatus.SC_SUCCESS) {
250                 throw new HttpException("Unexpected response to CONNECT request: " + new StatusLine(response));
251             }
252 
253             if (config.isAuthenticationEnabled()) {
254                 final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
255                 final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange);
256 
257                 if (authCacheKeeper != null) {
258                     if (proxyAuthRequested) {
259                         authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
260                     } else {
261                         authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
262                     }
263                 }
264 
265                 if (proxyAuthRequested || proxyMutualAuthRequired) {
266                     final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
267                             proxyAuthStrategy, proxyAuthExchange, context);
268 
269                     if (authCacheKeeper != null) {
270                         authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
271                     }
272                     if (updated) {
273                         // Retry request
274                         if (this.reuseStrategy.keepAlive(connect, response, context)) {
275                             if (LOG.isDebugEnabled()) {
276                                 LOG.debug("{} connection kept alive", exchangeId);
277                             }
278                             // Consume response content
279                             final HttpEntity entity = response.getEntity();
280                             EntityUtils.consume(entity);
281                         } else {
282                             execRuntime.disconnectEndpoint();
283                         }
284                         response = null;
285                     }
286                 }
287             }
288         }
289 
290         final int status = response.getCode();
291         if (status == HttpStatus.SC_OK) {
292             context.setProtocolVersion(null);
293         } else {
294             final HttpEntity entity = response.getEntity();
295             if (entity != null) {
296                 response.setEntity(new ByteArrayEntity(
297                         EntityUtils.toByteArray(entity, 4096),
298                         ContentType.parseLenient(entity.getContentType())));
299                 execRuntime.discardEndpoint();
300             }
301             return response;
302         }
303         return null;
304     }
305 
306     /**
307      * Creates a tunnel to an intermediate proxy.
308      * This method is <i>not</i> implemented in this class.
309      * It just throws an exception here.
310      */
311     private boolean createTunnelToProxy(
312             final HttpRoute route,
313             final int hop,
314             final HttpClientContext context) throws HttpException {
315 
316         // Have a look at createTunnelToTarget and replicate the parts
317         // you need in a custom derived class. If your proxies don't require
318         // authentication, it is not too hard. But for the stock version of
319         // HttpClient, we cannot make such simplifying assumptions and would
320         // have to include proxy authentication code. The HttpComponents team
321         // is currently not in a position to support rarely used code of this
322         // complexity. Feel free to submit patches that refactor the code in
323         // createTunnelToTarget to facilitate re-use for proxy tunnelling.
324 
325         throw new HttpException("Proxy chains are not supported.");
326     }
327 
328 }