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
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.auth.AuthExchange;
37 import org.apache.hc.client5.http.classic.ExecChain;
38 import org.apache.hc.client5.http.classic.ExecChainHandler;
39 import org.apache.hc.client5.http.classic.methods.HttpGet;
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.message.BasicClassicHttpRequest;
59 import org.apache.hc.core5.util.Args;
60 import org.apache.hc.core5.util.LangUtils;
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 RedirectExec implements ExecChainHandler {
78
79 private static final Logger LOG = LoggerFactory.getLogger(RedirectExec.class);
80
81 private final RedirectStrategy redirectStrategy;
82 private final HttpRoutePlanner routePlanner;
83
84 public RedirectExec(
85 final HttpRoutePlanner routePlanner,
86 final RedirectStrategy redirectStrategy) {
87 super();
88 Args.notNull(routePlanner, "HTTP route planner");
89 Args.notNull(redirectStrategy, "HTTP redirect strategy");
90 this.routePlanner = routePlanner;
91 this.redirectStrategy = redirectStrategy;
92 }
93
94 @Override
95 public ClassicHttpResponse execute(
96 final ClassicHttpRequest request,
97 final ExecChain.Scope scope,
98 final ExecChain chain) throws IOException, HttpException {
99 Args.notNull(request, "HTTP request");
100 Args.notNull(scope, "Scope");
101
102 final HttpClientContext context = scope.clientContext;
103 RedirectLocations redirectLocations = context.getRedirectLocations();
104 if (redirectLocations == null) {
105 redirectLocations = new RedirectLocations();
106 context.setAttribute(HttpClientContext.REDIRECT_LOCATIONS, redirectLocations);
107 }
108 redirectLocations.clear();
109
110 final RequestConfig config = context.getRequestConfig();
111 final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
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 if (!config.isCircularRedirectsAllowed()) {
136 if (redirectLocations.contains(redirectUri)) {
137 throw new CircularRedirectException("Circular redirect to '" + redirectUri + "'");
138 }
139 }
140 redirectLocations.add(redirectUri);
141
142 final ClassicHttpRequest originalRequest = scope.originalRequest;
143 ClassicHttpRequest redirect = null;
144 final int statusCode = response.getCode();
145 switch (statusCode) {
146 case HttpStatus.SC_MOVED_PERMANENTLY:
147 case HttpStatus.SC_MOVED_TEMPORARILY:
148 if (Method.POST.isSame(request.getMethod())) {
149 redirect = new HttpGet(redirectUri);
150 }
151 break;
152 case HttpStatus.SC_SEE_OTHER:
153 if (!Method.GET.isSame(request.getMethod()) && !Method.HEAD.isSame(request.getMethod())) {
154 redirect = new HttpGet(redirectUri);
155 }
156 }
157 if (redirect == null) {
158 redirect = new BasicClassicHttpRequest(originalRequest.getMethod(), redirectUri);
159 redirect.setEntity(originalRequest.getEntity());
160 }
161 redirect.setHeaders(originalRequest.getHeaders());
162
163 final HttpHost newTarget = URIUtils.extractHost(redirectUri);
164 if (newTarget == null) {
165 throw new ProtocolException("Redirect URI does not specify a valid host name: " +
166 redirectUri);
167 }
168
169 final HttpRoute currentRoute = currentScope.route;
170 if (!LangUtils.equals(currentRoute.getTargetHost(), newTarget)) {
171 final HttpRoute newRoute = this.routePlanner.determineRoute(newTarget, context);
172 if (!LangUtils.equals(currentRoute, newRoute)) {
173 if (LOG.isDebugEnabled()) {
174 LOG.debug("{}: new route required", exchangeId);
175 }
176 final AuthExchange targetAuthExchange = context.getAuthExchange(currentRoute.getTargetHost());
177 if (LOG.isDebugEnabled()) {
178 LOG.debug("{}: resetting target auth state", exchangeId);
179 }
180 targetAuthExchange.reset();
181 if (currentRoute.getProxyHost() != null) {
182 final AuthExchange proxyAuthExchange = context.getAuthExchange(currentRoute.getProxyHost());
183 if (proxyAuthExchange.isConnectionBased()) {
184 if (LOG.isDebugEnabled()) {
185 LOG.debug("{}: resetting proxy auth state", exchangeId);
186 }
187 proxyAuthExchange.reset();
188 }
189 }
190 currentScope = new ExecChain.Scope(
191 currentScope.exchangeId,
192 newRoute,
193 currentScope.originalRequest,
194 currentScope.execRuntime,
195 currentScope.clientContext);
196 }
197 }
198
199 if (LOG.isDebugEnabled()) {
200 LOG.debug("{}: redirecting to '{}' via {}", exchangeId, redirectUri, currentRoute);
201 }
202 currentRequest = redirect;
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 }