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