1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.client;
20
21 import java.net.URI;
22 import java.net.URISyntaxException;
23 import java.util.List;
24 import java.util.concurrent.CountDownLatch;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.atomic.AtomicReference;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.client.api.Response;
32 import org.eclipse.jetty.client.api.Result;
33 import org.eclipse.jetty.client.util.BufferingResponseListener;
34 import org.eclipse.jetty.http.HttpMethod;
35 import org.eclipse.jetty.util.log.Log;
36 import org.eclipse.jetty.util.log.Logger;
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60 public class HttpRedirector
61 {
62 private static final Logger LOG = Log.getLogger(HttpRedirector.class);
63 private static final String SCHEME_REGEXP = "(^https?)";
64 private static final String AUTHORITY_REGEXP = "([^/\\?#]+)";
65
66 private static final String DESTINATION_REGEXP = "(" + SCHEME_REGEXP + "://" + AUTHORITY_REGEXP + ")?";
67 private static final String PATH_REGEXP = "([^\\?#]*)";
68 private static final String QUERY_REGEXP = "([^#]*)";
69 private static final String FRAGMENT_REGEXP = "(.*)";
70 private static final Pattern URI_PATTERN = Pattern.compile(DESTINATION_REGEXP + PATH_REGEXP + QUERY_REGEXP + FRAGMENT_REGEXP);
71 private static final String ATTRIBUTE = HttpRedirector.class.getName() + ".redirects";
72
73 private final HttpClient client;
74 private final ResponseNotifier notifier;
75
76 public HttpRedirector(HttpClient client)
77 {
78 this.client = client;
79 this.notifier = new ResponseNotifier();
80 }
81
82
83
84
85
86 public boolean isRedirect(Response response)
87 {
88 switch (response.getStatus())
89 {
90 case 301:
91 case 302:
92 case 303:
93 case 307:
94 case 308:
95 return true;
96 default:
97 return false;
98 }
99 }
100
101
102
103
104
105
106
107
108
109
110
111 public Result redirect(Request request, Response response) throws InterruptedException, ExecutionException
112 {
113 final AtomicReference<Result> resultRef = new AtomicReference<>();
114 final CountDownLatch latch = new CountDownLatch(1);
115 Request redirect = redirect(request, response, new BufferingResponseListener()
116 {
117 @Override
118 public void onComplete(Result result)
119 {
120 resultRef.set(new Result(result.getRequest(),
121 result.getRequestFailure(),
122 new HttpContentResponse(result.getResponse(), getContent(), getMediaType(), getEncoding()),
123 result.getResponseFailure()));
124 latch.countDown();
125 }
126 });
127
128 try
129 {
130 latch.await();
131 Result result = resultRef.get();
132 if (result.isFailed())
133 throw new ExecutionException(result.getFailure());
134 return result;
135 }
136 catch (InterruptedException x)
137 {
138
139 redirect.abort(x);
140 throw x;
141 }
142 }
143
144
145
146
147
148
149
150
151
152 public Request redirect(Request request, Response response, Response.CompleteListener listener)
153 {
154 if (isRedirect(response))
155 {
156 String location = response.getHeaders().get("Location");
157 URI newURI = extractRedirectURI(response);
158 if (newURI != null)
159 {
160 if (LOG.isDebugEnabled())
161 LOG.debug("Redirecting to {} (Location: {})", newURI, location);
162 return redirect(request, response, listener, newURI);
163 }
164 else
165 {
166 fail(request, response, new HttpResponseException("Invalid 'Location' header: " + location, response));
167 return null;
168 }
169 }
170 else
171 {
172 fail(request, response, new HttpResponseException("Cannot redirect: " + response, response));
173 return null;
174 }
175 }
176
177
178
179
180
181
182
183
184 public URI extractRedirectURI(Response response)
185 {
186 String location = response.getHeaders().get("location");
187 if (location != null)
188 return sanitize(location);
189 return null;
190 }
191
192 private URI sanitize(String location)
193 {
194
195
196
197 try
198 {
199
200 return new URI(location);
201 }
202 catch (URISyntaxException x)
203 {
204 Matcher matcher = URI_PATTERN.matcher(location);
205 if (matcher.matches())
206 {
207 String scheme = matcher.group(2);
208 String authority = matcher.group(3);
209 String path = matcher.group(4);
210 String query = matcher.group(5);
211 if (query.length() == 0)
212 query = null;
213 String fragment = matcher.group(6);
214 if (fragment.length() == 0)
215 fragment = null;
216 try
217 {
218 return new URI(scheme, authority, path, query, fragment);
219 }
220 catch (URISyntaxException xx)
221 {
222
223 }
224 }
225 return null;
226 }
227 }
228
229 private Request redirect(Request request, Response response, Response.CompleteListener listener, URI newURI)
230 {
231 if (!newURI.isAbsolute())
232 newURI = request.getURI().resolve(newURI);
233
234 int status = response.getStatus();
235 switch (status)
236 {
237 case 301:
238 {
239 String method = request.getMethod();
240 if (HttpMethod.GET.is(method) || HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
241 return redirect(request, response, listener, newURI, method);
242 else if (HttpMethod.POST.is(method))
243 return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
244 fail(request, response, new HttpResponseException("HTTP protocol violation: received 301 for non GET/HEAD/POST/PUT request", response));
245 return null;
246 }
247 case 302:
248 {
249 String method = request.getMethod();
250 if (HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
251 return redirect(request, response, listener, newURI, method);
252 else
253 return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
254 }
255 case 303:
256 {
257 String method = request.getMethod();
258 if (HttpMethod.HEAD.is(method))
259 return redirect(request, response, listener, newURI, method);
260 else
261 return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
262 }
263 case 307:
264 case 308:
265 {
266
267 return redirect(request, response, listener, newURI, request.getMethod());
268 }
269 default:
270 {
271 fail(request, response, new HttpResponseException("Unhandled HTTP status code " + status, response));
272 return null;
273 }
274 }
275 }
276
277 private Request redirect(Request request, Response response, Response.CompleteListener listener, URI location, String method)
278 {
279 HttpRequest httpRequest = (HttpRequest)request;
280 HttpConversation conversation = httpRequest.getConversation();
281 Integer redirects = (Integer)conversation.getAttribute(ATTRIBUTE);
282 if (redirects == null)
283 redirects = 0;
284 if (redirects < client.getMaxRedirects())
285 {
286 ++redirects;
287 conversation.setAttribute(ATTRIBUTE, redirects);
288 return sendRedirect(httpRequest, response, listener, location, method);
289 }
290 else
291 {
292 fail(request, response, new HttpResponseException("Max redirects exceeded " + redirects, response));
293 return null;
294 }
295 }
296
297 private Request sendRedirect(final HttpRequest httpRequest, Response response, Response.CompleteListener listener, URI location, String method)
298 {
299 try
300 {
301 Request redirect = client.copyRequest(httpRequest, location);
302
303
304 redirect.method(method);
305
306 redirect.onRequestBegin(new Request.BeginListener()
307 {
308 @Override
309 public void onBegin(Request redirect)
310 {
311 Throwable cause = httpRequest.getAbortCause();
312 if (cause != null)
313 redirect.abort(cause);
314 }
315 });
316
317 redirect.send(listener);
318 return redirect;
319 }
320 catch (Throwable x)
321 {
322 fail(httpRequest, response, x);
323 return null;
324 }
325 }
326
327 protected void fail(Request request, Response response, Throwable failure)
328 {
329 HttpConversation conversation = ((HttpRequest)request).getConversation();
330 conversation.updateResponseListeners(null);
331 List<Response.ResponseListener> listeners = conversation.getResponseListeners();
332 notifier.notifyFailure(listeners, response, failure);
333 notifier.notifyComplete(listeners, new Result(request, response, failure));
334 }
335 }