View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
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.nio.AsyncDataConsumer;
58  import org.apache.hc.core5.http.nio.AsyncEntityProducer;
59  import org.apache.hc.core5.http.support.BasicRequestBuilder;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  /**
64   * Request execution handler in the asynchronous request execution chain
65   * responsible for handling of request redirects.
66   * <p>
67   * Further responsibilities such as communication with the opposite
68   * endpoint is delegated to the next executor in the request execution
69   * chain.
70   * </p>
71   *
72   * @since 5.0
73   */
74  @Contract(threading = ThreadingBehavior.STATELESS)
75  @Internal
76  public final class AsyncRedirectExec implements AsyncExecChainHandler {
77  
78      private static final Logger LOG = LoggerFactory.getLogger(AsyncRedirectExec.class);
79  
80      private final HttpRoutePlanner routePlanner;
81      private final RedirectStrategy redirectStrategy;
82  
83      AsyncRedirectExec(final HttpRoutePlanner routePlanner, final RedirectStrategy redirectStrategy) {
84          this.routePlanner = routePlanner;
85          this.redirectStrategy = redirectStrategy;
86      }
87  
88      private static class State {
89  
90          volatile URI redirectURI;
91          volatile int maxRedirects;
92          volatile int redirectCount;
93          volatile HttpRequest originalRequest;
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         final HttpRoute currentRoute = scope.route;
113         chain.proceed(request, entityProducer, scope, new AsyncExecCallback() {
114 
115             @Override
116             public AsyncDataConsumer handleResponse(
117                     final HttpResponse response,
118                     final EntityDetails entityDetails) throws HttpException, IOException {
119 
120                 state.redirectURI = null;
121                 final RequestConfig config = clientContext.getRequestConfig();
122                 if (config.isRedirectsEnabled() && redirectStrategy.isRedirected(request, response, clientContext)) {
123                     if (state.redirectCount >= state.maxRedirects) {
124                         throw new RedirectException("Maximum redirects (" + state.maxRedirects + ") exceeded");
125                     }
126 
127                     state.redirectCount++;
128 
129                     final URI redirectUri = redirectStrategy.getLocationURI(request, response, clientContext);
130                     if (LOG.isDebugEnabled()) {
131                         LOG.debug("{} redirect requested to location '{}'", exchangeId, redirectUri);
132                     }
133                     if (!config.isCircularRedirectsAllowed()) {
134                         if (state.redirectLocations.contains(redirectUri)) {
135                             throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'");
136                         }
137                     }
138                     state.redirectLocations.add(redirectUri);
139 
140                     final HttpHost newTarget = URIUtils.extractHost(redirectUri);
141                     if (newTarget == null) {
142                         throw new ProtocolException("Redirect URI does not specify a valid host name: " + redirectUri);
143                     }
144 
145                     final int statusCode = response.getCode();
146                     final BasicRequestBuilder redirectBuilder;
147                     switch (statusCode) {
148                         case HttpStatus.SC_MOVED_PERMANENTLY:
149                         case HttpStatus.SC_MOVED_TEMPORARILY:
150                             if (Method.POST.isSame(request.getMethod())) {
151                                 redirectBuilder = BasicRequestBuilder.get();
152                                 state.currentEntityProducer = null;
153                             } else {
154                                 redirectBuilder = BasicRequestBuilder.copy(state.originalRequest);
155                             }
156                             break;
157                         case HttpStatus.SC_SEE_OTHER:
158                             if (!Method.GET.isSame(request.getMethod()) && !Method.HEAD.isSame(request.getMethod())) {
159                                 redirectBuilder = BasicRequestBuilder.get();
160                                 state.currentEntityProducer = null;
161                             } else {
162                                 redirectBuilder = BasicRequestBuilder.copy(state.originalRequest);
163                             }
164                             break;
165                         default:
166                             redirectBuilder = BasicRequestBuilder.copy(state.originalRequest);
167                     }
168                     redirectBuilder.setUri(redirectUri);
169                     state.reroute = false;
170                     state.redirectURI = redirectUri;
171                     state.originalRequest = redirectBuilder.build();
172                     state.currentRequest = redirectBuilder.build();
173 
174                     if (!Objects.equals(currentRoute.getTargetHost(), newTarget)) {
175                         final HttpRoute newRoute = routePlanner.determineRoute(newTarget, clientContext);
176                         if (!Objects.equals(currentRoute, newRoute)) {
177                             state.reroute = true;
178                             final AuthExchange targetAuthExchange = clientContext.getAuthExchange(currentRoute.getTargetHost());
179                             if (LOG.isDebugEnabled()) {
180                                 LOG.debug("{} resetting target auth state", exchangeId);
181                             }
182                             targetAuthExchange.reset();
183                             if (currentRoute.getProxyHost() != null) {
184                                 final AuthExchange proxyAuthExchange = clientContext.getAuthExchange(currentRoute.getProxyHost());
185                                 if (proxyAuthExchange.isConnectionBased()) {
186                                     if (LOG.isDebugEnabled()) {
187                                         LOG.debug("{} resetting proxy auth state", exchangeId);
188                                     }
189                                     proxyAuthExchange.reset();
190                                 }
191                             }
192                             state.currentScope = new AsyncExecChain.Scope(
193                                     scope.exchangeId,
194                                     newRoute,
195                                     scope.originalRequest,
196                                     scope.cancellableDependency,
197                                     scope.clientContext,
198                                     scope.execRuntime,
199                                     scope.scheduler,
200                                     scope.execCount);
201                         }
202                     }
203                 }
204                 if (state.redirectURI != null) {
205                     if (LOG.isDebugEnabled()) {
206                         LOG.debug("{} redirecting to '{}' via {}", exchangeId, state.redirectURI, currentRoute);
207                     }
208                     return null;
209                 }
210                 return asyncExecCallback.handleResponse(response, entityDetails);
211             }
212 
213             @Override
214             public void handleInformationResponse(
215                     final HttpResponse response) throws HttpException, IOException {
216                 asyncExecCallback.handleInformationResponse(response);
217             }
218 
219             @Override
220             public void completed() {
221                 if (state.redirectURI == null) {
222                     asyncExecCallback.completed();
223                 } else {
224                     final AsyncEntityProducer entityProducer = state.currentEntityProducer;
225                     if (entityProducer != null) {
226                         entityProducer.releaseResources();
227                     }
228                     if (entityProducer != null && !entityProducer.isRepeatable()) {
229                         if (LOG.isDebugEnabled()) {
230                             LOG.debug("{} cannot redirect non-repeatable request", exchangeId);
231                         }
232                         asyncExecCallback.completed();
233                     } else {
234                         try {
235                             if (state.reroute) {
236                                 scope.execRuntime.releaseEndpoint();
237                             }
238                             internalExecute(state, chain, asyncExecCallback);
239                         } catch (final IOException | HttpException ex) {
240                             asyncExecCallback.failed(ex);
241                         }
242                     }
243                 }
244             }
245 
246             @Override
247             public void failed(final Exception cause) {
248                 asyncExecCallback.failed(cause);
249             }
250 
251         });
252 
253     }
254 
255     @Override
256     public void execute(
257             final HttpRequest request,
258             final AsyncEntityProducer entityProducer,
259             final AsyncExecChain.Scope scope,
260             final AsyncExecChain chain,
261             final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
262         final HttpClientContext clientContext = scope.clientContext;
263         RedirectLocations redirectLocations = clientContext.getRedirectLocations();
264         if (redirectLocations == null) {
265             redirectLocations = new RedirectLocations();
266             clientContext.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
267         }
268         redirectLocations.clear();
269 
270         final RequestConfig config = clientContext.getRequestConfig();
271 
272         final State state = new State();
273         state.maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
274         state.redirectCount = 0;
275         state.originalRequest = scope.originalRequest;
276         state.currentRequest = request;
277         state.currentEntityProducer = entityProducer;
278         state.redirectLocations = redirectLocations;
279         state.currentScope = scope;
280 
281         internalExecute(state, chain, asyncExecCallback);
282     }
283 
284 }