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