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 package org.apache.hc.client5.http.impl.async;
28
29 import java.io.IOException;
30 import java.util.Iterator;
31 import java.util.concurrent.atomic.AtomicBoolean;
32
33 import org.apache.hc.client5.http.AuthenticationStrategy;
34 import org.apache.hc.client5.http.HttpRoute;
35 import org.apache.hc.client5.http.SchemePortResolver;
36 import org.apache.hc.client5.http.async.AsyncExecCallback;
37 import org.apache.hc.client5.http.async.AsyncExecChain;
38 import org.apache.hc.client5.http.async.AsyncExecChainHandler;
39 import org.apache.hc.client5.http.async.AsyncExecRuntime;
40 import org.apache.hc.client5.http.auth.AuthExchange;
41 import org.apache.hc.client5.http.auth.ChallengeType;
42 import org.apache.hc.client5.http.config.RequestConfig;
43 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
44 import org.apache.hc.client5.http.impl.RequestSupport;
45 import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
46 import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
47 import org.apache.hc.client5.http.protocol.HttpClientContext;
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.EntityDetails;
52 import org.apache.hc.core5.http.Header;
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.HttpResponse;
58 import org.apache.hc.core5.http.Method;
59 import org.apache.hc.core5.http.ProtocolException;
60 import org.apache.hc.core5.http.nio.AsyncDataConsumer;
61 import org.apache.hc.core5.http.nio.AsyncEntityProducer;
62 import org.apache.hc.core5.http.support.BasicRequestBuilder;
63 import org.apache.hc.core5.net.URIAuthority;
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
76
77
78
79 @Contract(threading = ThreadingBehavior.STATELESS)
80 @Internal
81 public final class AsyncProtocolExec implements AsyncExecChainHandler {
82
83 private static final Logger LOG = LoggerFactory.getLogger(AsyncProtocolExec.class);
84
85 private final AuthenticationStrategy targetAuthStrategy;
86 private final AuthenticationStrategy proxyAuthStrategy;
87 private final HttpAuthenticator authenticator;
88 private final SchemePortResolver schemePortResolver;
89 private final AuthCacheKeeper authCacheKeeper;
90
91 AsyncProtocolExec(
92 final AuthenticationStrategy targetAuthStrategy,
93 final AuthenticationStrategy proxyAuthStrategy,
94 final SchemePortResolver schemePortResolver,
95 final boolean authCachingDisabled) {
96 this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy");
97 this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
98 this.authenticator = new HttpAuthenticator();
99 this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE;
100 this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(this.schemePortResolver);
101 }
102
103 @Override
104 public void execute(
105 final HttpRequest userRequest,
106 final AsyncEntityProducer entityProducer,
107 final AsyncExecChain.Scope scope,
108 final AsyncExecChain chain,
109 final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
110
111 if (Method.CONNECT.isSame(userRequest.getMethod())) {
112 throw new ProtocolException("Direct execution of CONNECT is not allowed");
113 }
114
115 final HttpRoute route = scope.route;
116 final HttpHost routeTarget = route.getTargetHost();
117 final HttpHost proxy = route.getProxyHost();
118 final HttpClientContext clientContext = scope.clientContext;
119
120 final HttpRequest request;
121 if (proxy != null && !route.isTunnelled()) {
122 final BasicRequestBuilder requestBuilder = BasicRequestBuilder.copy(userRequest);
123 if (requestBuilder.getAuthority() == null) {
124 requestBuilder.setAuthority(new URIAuthority(routeTarget));
125 }
126 requestBuilder.setAbsoluteRequestUri(true);
127 request = requestBuilder.build();
128 } else {
129 request = userRequest;
130 }
131
132
133 if (request.getScheme() == null) {
134 request.setScheme(routeTarget.getSchemeName());
135 }
136 if (request.getAuthority() == null) {
137 request.setAuthority(new URIAuthority(routeTarget));
138 }
139
140 final URIAuthority authority = request.getAuthority();
141 if (authority.getUserInfo() != null) {
142 throw new ProtocolException("Request URI authority contains deprecated userinfo component");
143 }
144
145 final HttpHost target = new HttpHost(
146 request.getScheme(),
147 authority.getHostName(),
148 schemePortResolver.resolve(request.getScheme(), authority));
149 final String pathPrefix = RequestSupport.extractPathPrefix(request);
150 final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target);
151 final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange();
152
153 if (!targetAuthExchange.isConnectionBased() &&
154 targetAuthExchange.getPathPrefix() != null &&
155 !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) {
156
157
158 targetAuthExchange.reset();
159 }
160 if (targetAuthExchange.getPathPrefix() == null) {
161 targetAuthExchange.setPathPrefix(pathPrefix);
162 }
163
164 if (authCacheKeeper != null) {
165 authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, clientContext);
166 if (proxy != null) {
167 authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext);
168 }
169 }
170
171 final AtomicBoolean challenged = new AtomicBoolean(false);
172 internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange,
173 challenged, request, entityProducer, scope, chain, asyncExecCallback);
174 }
175
176 private void internalExecute(
177 final HttpHost target,
178 final String pathPrefix,
179 final AuthExchange targetAuthExchange,
180 final AuthExchange proxyAuthExchange,
181 final AtomicBoolean challenged,
182 final HttpRequest request,
183 final AsyncEntityProducer entityProducer,
184 final AsyncExecChain.Scope scope,
185 final AsyncExecChain chain,
186 final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
187 final String exchangeId = scope.exchangeId;
188 final HttpRoute route = scope.route;
189 final HttpClientContext clientContext = scope.clientContext;
190 final AsyncExecRuntime execRuntime = scope.execRuntime;
191
192 final HttpHost proxy = route.getProxyHost();
193
194 if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
195 if (LOG.isDebugEnabled()) {
196 LOG.debug("{} target auth state: {}", exchangeId, targetAuthExchange.getState());
197 }
198 authenticator.addAuthResponse(target, ChallengeType.TARGET, request, targetAuthExchange, clientContext);
199 }
200 if (!request.containsHeader(HttpHeaders.PROXY_AUTHORIZATION) && !route.isTunnelled()) {
201 if (LOG.isDebugEnabled()) {
202 LOG.debug("{} proxy auth state: {}", exchangeId, proxyAuthExchange.getState());
203 }
204 authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, clientContext);
205 }
206
207 chain.proceed(request, entityProducer, scope, new AsyncExecCallback() {
208
209 @Override
210 public AsyncDataConsumer handleResponse(
211 final HttpResponse response,
212 final EntityDetails entityDetails) throws HttpException, IOException {
213
214 if (Method.TRACE.isSame(request.getMethod())) {
215
216 return asyncExecCallback.handleResponse(response, entityDetails);
217 }
218 if (needAuthentication(
219 targetAuthExchange,
220 proxyAuthExchange,
221 proxy != null ? proxy : target,
222 target,
223 pathPrefix,
224 response,
225 clientContext)) {
226 challenged.set(true);
227 return null;
228 }
229 challenged.set(false);
230 return asyncExecCallback.handleResponse(response, entityDetails);
231 }
232
233 @Override
234 public void handleInformationResponse(
235 final HttpResponse response) throws HttpException, IOException {
236 asyncExecCallback.handleInformationResponse(response);
237 }
238
239 @Override
240 public void completed() {
241 if (!execRuntime.isEndpointConnected()) {
242 if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
243 && proxyAuthExchange.isConnectionBased()) {
244 if (LOG.isDebugEnabled()) {
245 LOG.debug("{} resetting proxy auth state", exchangeId);
246 }
247 proxyAuthExchange.reset();
248 }
249 if (targetAuthExchange.getState() == AuthExchange.State.SUCCESS
250 && targetAuthExchange.isConnectionBased()) {
251 if (LOG.isDebugEnabled()) {
252 LOG.debug("{} resetting target auth state", exchangeId);
253 }
254 targetAuthExchange.reset();
255 }
256 }
257
258 if (challenged.get()) {
259 if (entityProducer != null && !entityProducer.isRepeatable()) {
260 if (LOG.isDebugEnabled()) {
261 LOG.debug("{} cannot retry non-repeatable request", exchangeId);
262 }
263 asyncExecCallback.completed();
264 } else {
265
266 final HttpRequest original = scope.originalRequest;
267 request.setHeaders();
268 for (final Iterator<Header> it = original.headerIterator(); it.hasNext(); ) {
269 request.addHeader(it.next());
270 }
271 try {
272 if (entityProducer != null) {
273 entityProducer.releaseResources();
274 }
275 internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange,
276 challenged, request, entityProducer, scope, chain, asyncExecCallback);
277 } catch (final HttpException | IOException ex) {
278 asyncExecCallback.failed(ex);
279 }
280 }
281 } else {
282 asyncExecCallback.completed();
283 }
284 }
285
286 @Override
287 public void failed(final Exception cause) {
288 if (cause instanceof IOException || cause instanceof RuntimeException) {
289 for (final AuthExchange authExchange : clientContext.getAuthExchanges().values()) {
290 if (authExchange.isConnectionBased()) {
291 authExchange.reset();
292 }
293 }
294 }
295 asyncExecCallback.failed(cause);
296 }
297
298 });
299 }
300
301 private boolean needAuthentication(
302 final AuthExchange targetAuthExchange,
303 final AuthExchange proxyAuthExchange,
304 final HttpHost proxy,
305 final HttpHost target,
306 final String pathPrefix,
307 final HttpResponse response,
308 final HttpClientContext context) {
309 final RequestConfig config = context.getRequestConfig();
310 if (config.isAuthenticationEnabled()) {
311 final boolean targetAuthRequested = authenticator.isChallenged(
312 target, ChallengeType.TARGET, response, targetAuthExchange, context);
313
314 if (authCacheKeeper != null) {
315 if (targetAuthRequested) {
316 authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context);
317 } else {
318 authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context);
319 }
320 }
321
322 final boolean proxyAuthRequested = authenticator.isChallenged(
323 proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
324
325 if (authCacheKeeper != null) {
326 if (proxyAuthRequested) {
327 authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
328 } else {
329 authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
330 }
331 }
332
333 if (targetAuthRequested) {
334 final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response,
335 targetAuthStrategy, targetAuthExchange, context);
336
337 if (authCacheKeeper != null) {
338 authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
339 }
340
341 return updated;
342 }
343 if (proxyAuthRequested) {
344 final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
345 proxyAuthStrategy, proxyAuthExchange, context);
346
347 if (authCacheKeeper != null) {
348 authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
349 }
350
351 return updated;
352 }
353 }
354 return false;
355 }
356
357 }