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