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.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
72
73
74
75
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
160
161
162
163 final int hop = fact.getHopCount()-1;
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
198
199
200
201
202
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
261 if (this.reuseStrategy.keepAlive(connect, response, context)) {
262 if (LOG.isDebugEnabled()) {
263 LOG.debug("{} connection kept alive", exchangeId);
264 }
265
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
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
288
289
290
291 return false;
292 }
293
294
295
296
297
298
299 private boolean createTunnelToProxy(
300 final HttpRoute route,
301 final int hop,
302 final HttpClientContext context) throws HttpException {
303
304
305
306
307
308
309
310
311
312
313 throw new HttpException("Proxy chains are not supported.");
314 }
315
316 }