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.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
70
71
72
73
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
154
155
156
157 final int hop = fact.getHopCount()-1;
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
192
193
194
195
196
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
236 if (this.reuseStrategy.keepAlive(connect, response, context)) {
237 if (LOG.isDebugEnabled()) {
238 LOG.debug("{} connection kept alive", exchangeId);
239 }
240
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
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
263
264
265
266 return false;
267 }
268
269
270
271
272
273
274 private boolean createTunnelToProxy(
275 final HttpRoute route,
276 final int hop,
277 final HttpClientContext context) throws HttpException {
278
279
280
281
282
283
284
285
286
287
288 throw new HttpException("Proxy chains are not supported.");
289 }
290
291 }