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