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.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
73
74
75
76
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
161
162
163
164 final int hop = fact.getHopCount()-1;
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
204
205
206
207
208
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
267 if (this.reuseStrategy.keepAlive(connect, response, context)) {
268 if (LOG.isDebugEnabled()) {
269 LOG.debug("{} connection kept alive", exchangeId);
270 }
271
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
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
294
295
296
297 return false;
298 }
299
300
301
302
303
304
305 private boolean createTunnelToProxy(
306 final HttpRoute route,
307 final int hop,
308 final HttpClientContext context) throws HttpException {
309
310
311
312
313
314
315
316
317
318
319 throw new HttpException("Proxy chains are not supported.");
320 }
321
322 }