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 import java.net.URI;
32 import java.util.Objects;
33
34 import org.apache.hc.client5.http.CircularRedirectException;
35 import org.apache.hc.client5.http.HttpRoute;
36 import org.apache.hc.client5.http.RedirectException;
37 import org.apache.hc.client5.http.auth.AuthExchange;
38 import org.apache.hc.client5.http.classic.ExecChain;
39 import org.apache.hc.client5.http.classic.ExecChainHandler;
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.ClassicHttpRequest;
50 import org.apache.hc.core5.http.ClassicHttpResponse;
51 import org.apache.hc.core5.http.HttpEntity;
52 import org.apache.hc.core5.http.HttpException;
53 import org.apache.hc.core5.http.HttpHost;
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.io.entity.EntityUtils;
58 import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
59 import org.apache.hc.core5.util.Args;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63
64
65
66
67
68
69
70
71
72
73
74 @Contract(threading = ThreadingBehavior.STATELESS)
75 @Internal
76 public final class RedirectExec implements ExecChainHandler {
77
78 private static final Logger LOG = LoggerFactory.getLogger(RedirectExec.class);
79
80 private final RedirectStrategy redirectStrategy;
81 private final HttpRoutePlanner routePlanner;
82
83 public RedirectExec(
84 final HttpRoutePlanner routePlanner,
85 final RedirectStrategy redirectStrategy) {
86 super();
87 Args.notNull(routePlanner, "HTTP route planner");
88 Args.notNull(redirectStrategy, "HTTP redirect strategy");
89 this.routePlanner = routePlanner;
90 this.redirectStrategy = redirectStrategy;
91 }
92
93 @Override
94 public ClassicHttpResponse execute(
95 final ClassicHttpRequest request,
96 final ExecChain.Scope scope,
97 final ExecChain chain) throws IOException, HttpException {
98 Args.notNull(request, "HTTP request");
99 Args.notNull(scope, "Scope");
100
101 final HttpClientContext context = scope.clientContext;
102 RedirectLocations redirectLocations = context.getRedirectLocations();
103 if (redirectLocations == null) {
104 redirectLocations = new RedirectLocations();
105 context.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
106 }
107 redirectLocations.clear();
108
109 final RequestConfig config = context.getRequestConfig();
110 final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
111 ClassicHttpRequest currentRequest = request;
112 ExecChain.Scope currentScope = scope;
113 for (int redirectCount = 0;;) {
114 final String exchangeId = currentScope.exchangeId;
115 final ClassicHttpResponse response = chain.proceed(currentRequest, currentScope);
116 try {
117 if (config.isRedirectsEnabled() && this.redirectStrategy.isRedirected(request, response, context)) {
118 final HttpEntity requestEntity = request.getEntity();
119 if (requestEntity != null && !requestEntity.isRepeatable()) {
120 if (LOG.isDebugEnabled()) {
121 LOG.debug("{} cannot redirect non-repeatable request", exchangeId);
122 }
123 return response;
124 }
125 if (redirectCount >= maxRedirects) {
126 throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded");
127 }
128 redirectCount++;
129
130 final URI redirectUri = this.redirectStrategy.getLocationURI(currentRequest, response, context);
131 if (LOG.isDebugEnabled()) {
132 LOG.debug("{} redirect requested to location '{}'", exchangeId, redirectUri);
133 }
134
135 final HttpHost newTarget = URIUtils.extractHost(redirectUri);
136 if (newTarget == null) {
137 throw new ProtocolException("Redirect URI does not specify a valid host name: " +
138 redirectUri);
139 }
140
141 if (!config.isCircularRedirectsAllowed()) {
142 if (redirectLocations.contains(redirectUri)) {
143 throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'");
144 }
145 }
146 redirectLocations.add(redirectUri);
147
148 final int statusCode = response.getCode();
149 final ClassicRequestBuilder redirectBuilder;
150 switch (statusCode) {
151 case HttpStatus.SC_MOVED_PERMANENTLY:
152 case HttpStatus.SC_MOVED_TEMPORARILY:
153 if (Method.POST.isSame(request.getMethod())) {
154 redirectBuilder = ClassicRequestBuilder.get();
155 } else {
156 redirectBuilder = ClassicRequestBuilder.copy(scope.originalRequest);
157 }
158 break;
159 case HttpStatus.SC_SEE_OTHER:
160 if (!Method.GET.isSame(request.getMethod()) && !Method.HEAD.isSame(request.getMethod())) {
161 redirectBuilder = ClassicRequestBuilder.get();
162 } else {
163 redirectBuilder = ClassicRequestBuilder.copy(scope.originalRequest);
164 }
165 break;
166 default:
167 redirectBuilder = ClassicRequestBuilder.copy(scope.originalRequest);
168 }
169 redirectBuilder.setUri(redirectUri);
170
171 final HttpRoute currentRoute = currentScope.route;
172 if (!Objects.equals(currentRoute.getTargetHost(), newTarget)) {
173 final HttpRoute newRoute = this.routePlanner.determineRoute(newTarget, context);
174 if (!Objects.equals(currentRoute, newRoute)) {
175 if (LOG.isDebugEnabled()) {
176 LOG.debug("{} new route required", exchangeId);
177 }
178 final AuthExchange targetAuthExchange = context.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 = context.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 currentScope = new ExecChain.Scope(
193 currentScope.exchangeId,
194 newRoute,
195 currentScope.originalRequest,
196 currentScope.execRuntime,
197 currentScope.clientContext);
198 }
199 }
200
201 if (LOG.isDebugEnabled()) {
202 LOG.debug("{} redirecting to '{}' via {}", exchangeId, redirectUri, currentRoute);
203 }
204 currentRequest = redirectBuilder.build();
205 RequestEntityProxy.enhance(currentRequest);
206
207 EntityUtils.consume(response.getEntity());
208 response.close();
209 } else {
210 return response;
211 }
212 } catch (final RuntimeException | IOException ex) {
213 response.close();
214 throw ex;
215 } catch (final HttpException ex) {
216
217
218 try {
219 EntityUtils.consume(response.getEntity());
220 } catch (final IOException ioex) {
221 if (LOG.isDebugEnabled()) {
222 LOG.debug("{} I/O error while releasing connection", exchangeId, ioex);
223 }
224 } finally {
225 response.close();
226 }
227 throw ex;
228 }
229 }
230 }
231
232 }