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.HttpAuthenticator;
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 HttpAuthenticator 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 HttpAuthenticator();
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                         }   break;
163 
164                         case HttpRouteDirector.TUNNEL_PROXY: {
165                             // The most simple example for this case is a proxy chain
166                             // of two proxies, where P1 must be tunnelled to P2.
167                             // route: Source -> P1 -> P2 -> Target (3 hops)
168                             // fact:  Source -> P1 -> Target       (2 hops)
169                             final int hop = fact.getHopCount()-1; // the hop to establish
170                             final boolean secure = createTunnelToProxy(route, hop, context);
171                             if (LOG.isDebugEnabled()) {
172                                 LOG.debug("{} tunnel to proxy created.", exchangeId);
173                             }
174                             tracker.tunnelProxy(route.getHopTarget(hop), secure);
175                         }   break;
176 
177                         case HttpRouteDirector.LAYER_PROTOCOL:
178                             execRuntime.upgradeTls(context);
179                             tracker.layerProtocol(route.isSecure());
180                             break;
181 
182                         case HttpRouteDirector.UNREACHABLE:
183                             throw new HttpException("Unable to establish route: " +
184                                     "planned = " + route + "; current = " + fact);
185                         case HttpRouteDirector.COMPLETE:
186                             break;
187                         default:
188                             throw new IllegalStateException("Unknown step indicator "
189                                     + step + " from RouteDirector.");
190                     }
191 
192                 } while (step > HttpRouteDirector.COMPLETE);
193             }
194             final EndpointInfo endpointInfo = execRuntime.getEndpointInfo();
195             if (endpointInfo != null) {
196                 context.setProtocolVersion(endpointInfo.getProtocol());
197                 context.setSSLSession(endpointInfo.getSslSession());
198             }
199             return chain.proceed(request, scope);
200 
201         } catch (final IOException | HttpException | RuntimeException ex) {
202             execRuntime.discardEndpoint();
203             throw ex;
204         }
205     }
206 
207     /**
208      * Creates a tunnel to the target server.
209      * The connection must be established to the (last) proxy.
210      * A CONNECT request for tunnelling through the proxy will
211      * be created and sent, the response received and checked.
212      * This method does <i>not</i> processChallenge the connection with
213      * information about the tunnel, that is left to the caller.
214      */
215     private ClassicHttpResponse createTunnelToTarget(
216             final String exchangeId,
217             final HttpRoute route,
218             final HttpRequest request,
219             final ExecRuntime execRuntime,
220             final HttpClientContext context) throws HttpException, IOException {
221 
222         final RequestConfig config = context.getRequestConfigOrDefault();
223 
224         final HttpHost target = route.getTargetHost();
225         final HttpHost proxy = route.getProxyHost();
226         final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy);
227 
228         if (authCacheKeeper != null) {
229             authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context);
230         }
231 
232         ClassicHttpResponse response = null;
233 
234         final String authority = target.toHostString();
235         final ClassicHttpRequest connect = new BasicClassicHttpRequest(Method.CONNECT, target, authority);
236         connect.setVersion(HttpVersion.HTTP_1_1);
237 
238         this.proxyHttpProcessor.process(connect, null, context);
239 
240         while (response == null) {
241             connect.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
242             this.authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, proxyAuthExchange, context);
243 
244             response = execRuntime.execute(exchangeId, connect, context);
245             this.proxyHttpProcessor.process(response, response.getEntity(), context);
246 
247             final int status = response.getCode();
248             if (status < HttpStatus.SC_SUCCESS) {
249                 throw new HttpException("Unexpected response to CONNECT request: " + new StatusLine(response));
250             }
251 
252             if (config.isAuthenticationEnabled()) {
253                 final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
254 
255                 if (authCacheKeeper != null) {
256                     if (proxyAuthRequested) {
257                         authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
258                     } else {
259                         authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
260                     }
261                 }
262 
263                 if (proxyAuthRequested) {
264                     final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
265                             proxyAuthStrategy, proxyAuthExchange, context);
266 
267                     if (authCacheKeeper != null) {
268                         authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
269                     }
270                     if (updated) {
271                         // Retry request
272                         if (this.reuseStrategy.keepAlive(connect, response, context)) {
273                             if (LOG.isDebugEnabled()) {
274                                 LOG.debug("{} connection kept alive", exchangeId);
275                             }
276                             // Consume response content
277                             final HttpEntity entity = response.getEntity();
278                             EntityUtils.consume(entity);
279                         } else {
280                             execRuntime.disconnectEndpoint();
281                         }
282                         response = null;
283                     }
284                 }
285             }
286         }
287 
288         final int status = response.getCode();
289         if (status != HttpStatus.SC_OK) {
290             final HttpEntity entity = response.getEntity();
291             if (entity != null) {
292                 response.setEntity(new ByteArrayEntity(
293                         EntityUtils.toByteArray(entity, 4096),
294                         ContentType.parseLenient(entity.getContentType())));
295                 execRuntime.disconnectEndpoint();
296             }
297             return response;
298         }
299         return null;
300     }
301 
302     /**
303      * Creates a tunnel to an intermediate proxy.
304      * This method is <i>not</i> implemented in this class.
305      * It just throws an exception here.
306      */
307     private boolean createTunnelToProxy(
308             final HttpRoute route,
309             final int hop,
310             final HttpClientContext context) throws HttpException {
311 
312         // Have a look at createTunnelToTarget and replicate the parts
313         // you need in a custom derived class. If your proxies don't require
314         // authentication, it is not too hard. But for the stock version of
315         // HttpClient, we cannot make such simplifying assumptions and would
316         // have to include proxy authentication code. The HttpComponents team
317         // is currently not in a position to support rarely used code of this
318         // complexity. Feel free to submit patches that refactor the code in
319         // createTunnelToTarget to facilitate re-use for proxy tunnelling.
320 
321         throw new HttpException("Proxy chains are not supported.");
322     }
323 
324 }