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 {
233 URI requestURI = request.getURI();
234 if (requestURI == null)
235 {
236 String uri = request.getScheme() + "://" + request.getHost();
237 int port = request.getPort();
238 if (port > 0)
239 uri += ":" + port;
240 requestURI = URI.create(uri);
241 }
242 newURI = requestURI.resolve(newURI);
243 }
244
245 int status = response.getStatus();
246 switch (status)
247 {
248 case 301:
249 {
250 String method = request.getMethod();
251 if (HttpMethod.GET.is(method) || HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
252 return redirect(request, response, listener, newURI, method);
253 else if (HttpMethod.POST.is(method))
254 return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
255 fail(request, response, new HttpResponseException("HTTP protocol violation: received 301 for non GET/HEAD/POST/PUT request", response));
256 return null;
257 }
258 case 302:
259 {
260 String method = request.getMethod();
261 if (HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
262 return redirect(request, response, listener, newURI, method);
263 else
264 return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
265 }
266 case 303:
267 {
268 String method = request.getMethod();
269 if (HttpMethod.HEAD.is(method))
270 return redirect(request, response, listener, newURI, method);
271 else
272 return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
273 }
274 case 307:
275 case 308:
276 {
277
278 return redirect(request, response, listener, newURI, request.getMethod());
279 }
280 default:
281 {
282 fail(request, response, new HttpResponseException("Unhandled HTTP status code " + status, response));
283 return null;
284 }
285 }
286 }
287
288 private Request redirect(Request request, Response response, Response.CompleteListener listener, URI location, String method)
289 {
290 HttpRequest httpRequest = (HttpRequest)request;
291 HttpConversation conversation = httpRequest.getConversation();
292 Integer redirects = (Integer)conversation.getAttribute(ATTRIBUTE);
293 if (redirects == null)
294 redirects = 0;
295 if (redirects < client.getMaxRedirects())
296 {
297 ++redirects;
298 conversation.setAttribute(ATTRIBUTE, redirects);
299 return sendRedirect(httpRequest, response, listener, location, method);
300 }
301 else
302 {
303 fail(request, response, new HttpResponseException("Max redirects exceeded " + redirects, response));
304 return null;
305 }
306 }
307
308 private Request sendRedirect(final HttpRequest httpRequest, Response response, Response.CompleteListener listener, URI location, String method)
309 {
310 try
311 {
312 Request redirect = client.copyRequest(httpRequest, location);
313
314
315 redirect.method(method);
316
317 redirect.onRequestBegin(new Request.BeginListener()
318 {
319 @Override
320 public void onBegin(Request redirect)
321 {
322 Throwable cause = httpRequest.getAbortCause();
323 if (cause != null)
324 redirect.abort(cause);
325 }
326 });
327
328 redirect.send(listener);
329 return redirect;
330 }
331 catch (Throwable x)
332 {
333 fail(httpRequest, response, x);
334 return null;
335 }
336 }
337
338 protected void fail(Request request, Response response, Throwable failure)
339 {
340 HttpConversation conversation = ((HttpRequest)request).getConversation();
341 conversation.updateResponseListeners(null);
342 List<Response.ResponseListener> listeners = conversation.getResponseListeners();
343 notifier.notifyFailure(listeners, response, failure);
344 notifier.notifyComplete(listeners, new Result(request, response, failure));
345 }
346 }