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