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 originalRequest = scope.originalRequest;
112 ClassicHttpRequest currentRequest = request;
113 ExecChain.Scope currentScope = scope;
114 for (int redirectCount = 0;;) {
115 final String exchangeId = currentScope.exchangeId;
116 final ClassicHttpResponse response = chain.proceed(currentRequest, currentScope);
117 try {
118 if (config.isRedirectsEnabled() && this.redirectStrategy.isRedirected(request, response, context)) {
119 final HttpEntity requestEntity = request.getEntity();
120 if (requestEntity != null && !requestEntity.isRepeatable()) {
121 if (LOG.isDebugEnabled()) {
122 LOG.debug("{} cannot redirect non-repeatable request", exchangeId);
123 }
124 return response;
125 }
126 if (redirectCount >= maxRedirects) {
127 throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded");
128 }
129 redirectCount++;
130
131 final URI redirectUri = this.redirectStrategy.getLocationURI(currentRequest, response, context);
132 if (LOG.isDebugEnabled()) {
133 LOG.debug("{} redirect requested to location '{}'", exchangeId, redirectUri);
134 }
135
136 final HttpHost newTarget = URIUtils.extractHost(redirectUri);
137 if (newTarget == null) {
138 throw new ProtocolException("Redirect URI does not specify a valid host name: " +
139 redirectUri);
140 }
141
142 if (!config.isCircularRedirectsAllowed()) {
143 if (redirectLocations.contains(redirectUri)) {
144 throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'");
145 }
146 }
147 redirectLocations.add(redirectUri);
148
149 final int statusCode = response.getCode();
150 final ClassicRequestBuilder redirectBuilder;
151 switch (statusCode) {
152 case HttpStatus.SC_MOVED_PERMANENTLY:
153 case HttpStatus.SC_MOVED_TEMPORARILY:
154 if (Method.POST.isSame(request.getMethod())) {
155 redirectBuilder = ClassicRequestBuilder.get();
156 } else {
157 redirectBuilder = ClassicRequestBuilder.copy(originalRequest);
158 }
159 break;
160 case HttpStatus.SC_SEE_OTHER:
161 if (!Method.GET.isSame(request.getMethod()) && !Method.HEAD.isSame(request.getMethod())) {
162 redirectBuilder = ClassicRequestBuilder.get();
163 } else {
164 redirectBuilder = ClassicRequestBuilder.copy(originalRequest);
165 }
166 break;
167 default:
168 redirectBuilder = ClassicRequestBuilder.copy(originalRequest);
169 }
170 redirectBuilder.setUri(redirectUri);
171
172 final HttpRoute currentRoute = currentScope.route;
173 if (!Objects.equals(currentRoute.getTargetHost(), newTarget)) {
174 final HttpRoute newRoute = this.routePlanner.determineRoute(newTarget, context);
175 if (!Objects.equals(currentRoute, newRoute)) {
176 if (LOG.isDebugEnabled()) {
177 LOG.debug("{} new route required", exchangeId);
178 }
179 final AuthExchange targetAuthExchange = context.getAuthExchange(currentRoute.getTargetHost());
180 if (LOG.isDebugEnabled()) {
181 LOG.debug("{} resetting target auth state", exchangeId);
182 }
183 targetAuthExchange.reset();
184 if (currentRoute.getProxyHost() != null) {
185 final AuthExchange proxyAuthExchange = context.getAuthExchange(currentRoute.getProxyHost());
186 if (proxyAuthExchange.isConnectionBased()) {
187 if (LOG.isDebugEnabled()) {
188 LOG.debug("{} resetting proxy auth state", exchangeId);
189 }
190 proxyAuthExchange.reset();
191 }
192 }
193 currentScope = new ExecChain.Scope(
194 currentScope.exchangeId,
195 newRoute,
196 currentScope.originalRequest,
197 currentScope.execRuntime,
198 currentScope.clientContext);
199 }
200 }
201
202 if (LOG.isDebugEnabled()) {
203 LOG.debug("{} redirecting to '{}' via {}", exchangeId, redirectUri, currentRoute);
204 }
205 originalRequest = redirectBuilder.build();
206 currentRequest = redirectBuilder.build();
207 RequestEntityProxy.enhance(currentRequest);
208
209 EntityUtils.consume(response.getEntity());
210 response.close();
211 } else {
212 return response;
213 }
214 } catch (final RuntimeException | IOException ex) {
215 response.close();
216 throw ex;
217 } catch (final HttpException ex) {
218
219
220 try {
221 EntityUtils.consume(response.getEntity());
222 } catch (final IOException ioex) {
223 if (LOG.isDebugEnabled()) {
224 LOG.debug("{} I/O error while releasing connection", exchangeId, ioex);
225 }
226 } finally {
227 response.close();
228 }
229 throw ex;
230 }
231 }
232 }
233
234 }