1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.AuthenticationHandler;
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
74
75
76
77
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 AuthenticationHandler 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 AuthenticationHandler();
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 }
163 break;
164
165 case HttpRouteDirector.TUNNEL_PROXY: {
166
167
168
169
170 final int hop = fact.getHopCount() - 1;
171 final boolean secure = createTunnelToProxy(route, hop, context);
172 if (LOG.isDebugEnabled()) {
173 LOG.debug("{} tunnel to proxy created.", exchangeId);
174 }
175 tracker.tunnelProxy(route.getHopTarget(hop), secure);
176 }
177 break;
178
179 case HttpRouteDirector.LAYER_PROTOCOL:
180 execRuntime.upgradeTls(context);
181 tracker.layerProtocol(route.isSecure());
182 break;
183
184 case HttpRouteDirector.UNREACHABLE:
185 throw new HttpException("Unable to establish route: " +
186 "planned = " + route + "; current = " + fact);
187 case HttpRouteDirector.COMPLETE:
188 break;
189 default:
190 throw new IllegalStateException("Unknown step indicator "
191 + step + " from RouteDirector.");
192 }
193
194 } while (step > HttpRouteDirector.COMPLETE);
195 }
196 final EndpointInfo endpointInfo = execRuntime.getEndpointInfo();
197 if (endpointInfo != null) {
198 context.setSSLSession(endpointInfo.getSslSession());
199 }
200 return chain.proceed(request, scope);
201
202 } catch (final IOException | HttpException | RuntimeException ex) {
203 execRuntime.discardEndpoint();
204 throw ex;
205 }
206 }
207
208
209
210
211
212
213
214
215
216 private ClassicHttpResponse createTunnelToTarget(
217 final String exchangeId,
218 final HttpRoute route,
219 final HttpRequest request,
220 final ExecRuntime execRuntime,
221 final HttpClientContext context) throws HttpException, IOException {
222
223 final RequestConfig config = context.getRequestConfigOrDefault();
224
225 final HttpHost target = route.getTargetHost();
226 final HttpHost proxy = route.getProxyHost();
227 final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy);
228
229 if (authCacheKeeper != null) {
230 authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context);
231 }
232
233 ClassicHttpResponse response = null;
234
235 final String authority = target.toHostString();
236 final ClassicHttpRequest connect = new BasicClassicHttpRequest(Method.CONNECT, target, authority);
237 connect.setVersion(HttpVersion.HTTP_1_1);
238
239 this.proxyHttpProcessor.process(connect, null, context);
240
241 while (response == null) {
242 connect.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
243 this.authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, proxyAuthExchange, context);
244
245 response = execRuntime.execute(exchangeId, connect, context);
246 this.proxyHttpProcessor.process(response, response.getEntity(), context);
247
248 final int status = response.getCode();
249 if (status < HttpStatus.SC_SUCCESS) {
250 throw new HttpException("Unexpected response to CONNECT request: " + new StatusLine(response));
251 }
252
253 if (config.isAuthenticationEnabled()) {
254 final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
255 final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange);
256
257 if (authCacheKeeper != null) {
258 if (proxyAuthRequested) {
259 authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
260 } else {
261 authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
262 }
263 }
264
265 if (proxyAuthRequested || proxyMutualAuthRequired) {
266 final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
267 proxyAuthStrategy, proxyAuthExchange, context);
268
269 if (authCacheKeeper != null) {
270 authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
271 }
272 if (updated) {
273
274 if (this.reuseStrategy.keepAlive(connect, response, context)) {
275 if (LOG.isDebugEnabled()) {
276 LOG.debug("{} connection kept alive", exchangeId);
277 }
278
279 final HttpEntity entity = response.getEntity();
280 EntityUtils.consume(entity);
281 } else {
282 execRuntime.disconnectEndpoint();
283 }
284 response = null;
285 }
286 }
287 }
288 }
289
290 final int status = response.getCode();
291 if (status == HttpStatus.SC_OK) {
292 context.setProtocolVersion(null);
293 } else {
294 final HttpEntity entity = response.getEntity();
295 if (entity != null) {
296 response.setEntity(new ByteArrayEntity(
297 EntityUtils.toByteArray(entity, 4096),
298 ContentType.parseLenient(entity.getContentType())));
299 execRuntime.discardEndpoint();
300 }
301 return response;
302 }
303 return null;
304 }
305
306
307
308
309
310
311 private boolean createTunnelToProxy(
312 final HttpRoute route,
313 final int hop,
314 final HttpClientContext context) throws HttpException {
315
316
317
318
319
320
321
322
323
324
325 throw new HttpException("Proxy chains are not supported.");
326 }
327
328 }