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.net.URI;
31 import java.util.Objects;
32
33 import org.apache.hc.client5.http.CircularRedirectException;
34 import org.apache.hc.client5.http.HttpRoute;
35 import org.apache.hc.client5.http.RedirectException;
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.auth.AuthExchange;
40 import org.apache.hc.client5.http.config.RequestConfig;
41 import org.apache.hc.client5.http.protocol.HttpClientContext;
42 import org.apache.hc.client5.http.protocol.RedirectLocations;
43 import org.apache.hc.client5.http.protocol.RedirectStrategy;
44 import org.apache.hc.client5.http.routing.HttpRoutePlanner;
45 import org.apache.hc.client5.http.utils.URIUtils;
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.EntityDetails;
50 import org.apache.hc.core5.http.HttpException;
51 import org.apache.hc.core5.http.HttpHost;
52 import org.apache.hc.core5.http.HttpRequest;
53 import org.apache.hc.core5.http.HttpResponse;
54 import org.apache.hc.core5.http.HttpStatus;
55 import org.apache.hc.core5.http.Method;
56 import org.apache.hc.core5.http.ProtocolException;
57 import org.apache.hc.core5.http.message.BasicHttpRequest;
58 import org.apache.hc.core5.http.nio.AsyncDataConsumer;
59 import org.apache.hc.core5.http.nio.AsyncEntityProducer;
60 import org.apache.hc.core5.http.support.BasicRequestBuilder;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64
65
66
67
68
69
70
71
72
73
74
75 @Contract(threading = ThreadingBehavior.STATELESS)
76 @Internal
77 public final class AsyncRedirectExec implements AsyncExecChainHandler {
78
79 private static final Logger LOG = LoggerFactory.getLogger(AsyncRedirectExec.class);
80
81 private final HttpRoutePlanner routePlanner;
82 private final RedirectStrategy redirectStrategy;
83
84 AsyncRedirectExec(final HttpRoutePlanner routePlanner, final RedirectStrategy redirectStrategy) {
85 this.routePlanner = routePlanner;
86 this.redirectStrategy = redirectStrategy;
87 }
88
89 private static class State {
90
91 volatile URI redirectURI;
92 volatile int maxRedirects;
93 volatile int redirectCount;
94 volatile HttpRequest currentRequest;
95 volatile AsyncEntityProducer currentEntityProducer;
96 volatile RedirectLocations redirectLocations;
97 volatile AsyncExecChain.Scope currentScope;
98 volatile boolean reroute;
99
100 }
101
102 private void internalExecute(
103 final State state,
104 final AsyncExecChain chain,
105 final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
106
107 final HttpRequest request = state.currentRequest;
108 final AsyncEntityProducer entityProducer = state.currentEntityProducer;
109 final AsyncExecChain.Scope scope = state.currentScope;
110 final HttpClientContext clientContext = scope.clientContext;
111 final String exchangeId = scope.exchangeId;
112 chain.proceed(request, entityProducer, scope, new AsyncExecCallback() {
113
114 @Override
115 public AsyncDataConsumer handleResponse(
116 final HttpResponse response,
117 final EntityDetails entityDetails) throws HttpException, IOException {
118
119 state.redirectURI = null;
120 final RequestConfig config = clientContext.getRequestConfigOrDefault();
121 if (config.isRedirectsEnabled() && redirectStrategy.isRedirected(request, response, clientContext)) {
122 if (state.redirectCount >= state.maxRedirects) {
123 throw new RedirectException("Maximum redirects (" + state.maxRedirects + ") exceeded");
124 }
125
126 state.redirectCount++;
127
128 final URI redirectUri = redirectStrategy.getLocationURI(request, response, clientContext);
129 if (LOG.isDebugEnabled()) {
130 LOG.debug("{} redirect requested to location '{}'", exchangeId, redirectUri);
131 }
132 if (!config.isCircularRedirectsAllowed()) {
133 if (state.redirectLocations.contains(redirectUri)) {
134 throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'");
135 }
136 }
137
138 final AsyncExecChain.Scope currentScope = state.currentScope;
139 final HttpHost newTarget = URIUtils.extractHost(redirectUri);
140 if (newTarget == null) {
141 throw new ProtocolException("Redirect URI does not specify a valid host name: " + redirectUri);
142 }
143
144 final int statusCode = response.getCode();
145 final BasicRequestBuilder redirectBuilder;
146 switch (statusCode) {
147 case HttpStatus.SC_MOVED_PERMANENTLY:
148 case HttpStatus.SC_MOVED_TEMPORARILY:
149 if (Method.POST.isSame(request.getMethod())) {
150 redirectBuilder = BasicRequestBuilder.get();
151 state.currentEntityProducer = null;
152 } else {
153 redirectBuilder = BasicRequestBuilder.copy(currentScope.originalRequest);
154 }
155 break;
156 case HttpStatus.SC_SEE_OTHER:
157 if (!Method.GET.isSame(request.getMethod()) && !Method.HEAD.isSame(request.getMethod())) {
158 redirectBuilder = BasicRequestBuilder.get();
159 state.currentEntityProducer = null;
160 } else {
161 redirectBuilder = BasicRequestBuilder.copy(currentScope.originalRequest);
162 }
163 break;
164 default:
165 redirectBuilder = BasicRequestBuilder.copy(currentScope.originalRequest);
166 }
167 redirectBuilder.setUri(redirectUri);
168 final BasicHttpRequest redirect = redirectBuilder.build();
169
170 final HttpRoute currentRoute = currentScope.route;
171 final HttpHost currentHost = currentRoute.getTargetHost();
172
173 if (!redirectStrategy.isRedirectAllowed(currentHost, newTarget, redirect, clientContext)) {
174 if (LOG.isDebugEnabled()) {
175 LOG.debug("{} cannot redirect due to safety restrictions", exchangeId);
176 }
177 return asyncExecCallback.handleResponse(response, entityDetails);
178 }
179
180 state.redirectLocations.add(redirectUri);
181
182 state.reroute = false;
183 state.redirectURI = redirectUri;
184 state.currentRequest = redirect;
185
186 final HttpRoute newRoute;
187 if (!Objects.equals(currentHost, newTarget)) {
188 newRoute = routePlanner.determineRoute(newTarget, clientContext);
189 if (!Objects.equals(currentRoute, newRoute)) {
190 state.reroute = true;
191 final AuthExchange targetAuthExchange = clientContext.getAuthExchange(currentHost);
192 if (LOG.isDebugEnabled()) {
193 LOG.debug("{} resetting target auth state", exchangeId);
194 }
195 targetAuthExchange.reset();
196 if (currentRoute.getProxyHost() != null) {
197 final AuthExchange proxyAuthExchange = clientContext.getAuthExchange(currentRoute.getProxyHost());
198 if (proxyAuthExchange.isConnectionBased()) {
199 if (LOG.isDebugEnabled()) {
200 LOG.debug("{} resetting proxy auth state", exchangeId);
201 }
202 proxyAuthExchange.reset();
203 }
204 }
205 }
206 } else {
207 newRoute = currentRoute;
208 }
209 state.currentScope = new AsyncExecChain.Scope(
210 scope.exchangeId,
211 newRoute,
212 BasicRequestBuilder.copy(redirect).build(),
213 scope.cancellableDependency,
214 scope.clientContext,
215 scope.execRuntime,
216 scope.scheduler,
217 scope.execCount);
218 if (LOG.isDebugEnabled()) {
219 LOG.debug("{} redirecting to '{}' via {}", exchangeId, redirectUri, newRoute);
220 }
221 return null;
222 }
223 return asyncExecCallback.handleResponse(response, entityDetails);
224 }
225
226 @Override
227 public void handleInformationResponse(
228 final HttpResponse response) throws HttpException, IOException {
229 asyncExecCallback.handleInformationResponse(response);
230 }
231
232 @Override
233 public void completed() {
234 if (state.redirectURI == null) {
235 asyncExecCallback.completed();
236 } else {
237 final AsyncEntityProducer entityProducer = state.currentEntityProducer;
238 if (entityProducer != null) {
239 entityProducer.releaseResources();
240 }
241 if (entityProducer != null && !entityProducer.isRepeatable()) {
242 if (LOG.isDebugEnabled()) {
243 LOG.debug("{} cannot redirect non-repeatable request", exchangeId);
244 }
245 asyncExecCallback.completed();
246 } else {
247 try {
248 if (state.reroute) {
249 scope.execRuntime.releaseEndpoint();
250 }
251 internalExecute(state, chain, asyncExecCallback);
252 } catch (final IOException | HttpException ex) {
253 asyncExecCallback.failed(ex);
254 }
255 }
256 }
257 }
258
259 @Override
260 public void failed(final Exception cause) {
261 asyncExecCallback.failed(cause);
262 }
263
264 });
265
266 }
267
268 @Override
269 public void execute(
270 final HttpRequest request,
271 final AsyncEntityProducer entityProducer,
272 final AsyncExecChain.Scope scope,
273 final AsyncExecChain chain,
274 final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
275 final HttpClientContext clientContext = scope.clientContext;
276 RedirectLocations redirectLocations = clientContext.getRedirectLocations();
277 if (redirectLocations == null) {
278 redirectLocations = new RedirectLocations();
279 clientContext.setRedirectLocations(redirectLocations);
280 }
281 redirectLocations.clear();
282
283 final RequestConfig config = clientContext.getRequestConfigOrDefault();
284
285 final State state = new State();
286 state.maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
287 state.redirectCount = 0;
288 state.currentRequest = request;
289 state.currentEntityProducer = entityProducer;
290 state.redirectLocations = redirectLocations;
291 state.currentScope = scope;
292
293 internalExecute(state, chain, asyncExecCallback);
294 }
295
296 }