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