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 return true;
95 default:
96 return false;
97 }
98 }
99
100
101
102
103
104
105
106
107
108
109
110 public Result redirect(Request request, Response response) throws InterruptedException, ExecutionException
111 {
112 final AtomicReference<Result> resultRef = new AtomicReference<>();
113 final CountDownLatch latch = new CountDownLatch(1);
114 Request redirect = redirect(request, response, new BufferingResponseListener()
115 {
116 @Override
117 public void onComplete(Result result)
118 {
119 resultRef.set(new Result(result.getRequest(),
120 result.getRequestFailure(),
121 new HttpContentResponse(result.getResponse(), getContent(), getMediaType(), getEncoding()),
122 result.getResponseFailure()));
123 latch.countDown();
124 }
125 });
126
127 try
128 {
129 latch.await();
130 Result result = resultRef.get();
131 if (result.isFailed())
132 throw new ExecutionException(result.getFailure());
133 return result;
134 }
135 catch (InterruptedException x)
136 {
137
138 redirect.abort(x);
139 throw x;
140 }
141 }
142
143
144
145
146
147
148
149
150
151 public Request redirect(Request request, Response response, Response.CompleteListener listener)
152 {
153 if (isRedirect(response))
154 {
155 String location = response.getHeaders().get("Location");
156 URI newURI = extractRedirectURI(response);
157 if (newURI != null)
158 {
159 if (LOG.isDebugEnabled())
160 LOG.debug("Redirecting to {} (Location: {})", newURI, location);
161 return redirect(request, response, listener, newURI);
162 }
163 else
164 {
165 fail(request, response, new HttpResponseException("Invalid 'Location' header: " + location, response));
166 return null;
167 }
168 }
169 else
170 {
171 fail(request, response, new HttpResponseException("Cannot redirect: " + response, response));
172 return null;
173 }
174 }
175
176
177
178
179
180
181
182
183 public URI extractRedirectURI(Response response)
184 {
185 String location = response.getHeaders().get("location");
186 if (location != null)
187 return sanitize(location);
188 return null;
189 }
190
191 private URI sanitize(String location)
192 {
193
194
195
196 try
197 {
198
199 return new URI(location);
200 }
201 catch (URISyntaxException x)
202 {
203 Matcher matcher = URI_PATTERN.matcher(location);
204 if (matcher.matches())
205 {
206 String scheme = matcher.group(2);
207 String authority = matcher.group(3);
208 String path = matcher.group(4);
209 String query = matcher.group(5);
210 if (query.length() == 0)
211 query = null;
212 String fragment = matcher.group(6);
213 if (fragment.length() == 0)
214 fragment = null;
215 try
216 {
217 return new URI(scheme, authority, path, query, fragment);
218 }
219 catch (URISyntaxException xx)
220 {
221
222 }
223 }
224 return null;
225 }
226 }
227
228 private Request redirect(Request request, Response response, Response.CompleteListener listener, URI newURI)
229 {
230 if (!newURI.isAbsolute())
231 newURI = request.getURI().resolve(newURI);
232
233 int status = response.getStatus();
234 switch (status)
235 {
236 case 301:
237 {
238 String method = request.getMethod();
239 if (HttpMethod.GET.is(method) || HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
240 return redirect(request, response, listener, newURI, method);
241 else if (HttpMethod.POST.is(method))
242 return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
243 fail(request, response, new HttpResponseException("HTTP protocol violation: received 301 for non GET/HEAD/POST/PUT request", response));
244 return null;
245 }
246 case 302:
247 {
248 String method = request.getMethod();
249 if (HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
250 return redirect(request, response, listener, newURI, method);
251 else
252 return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
253 }
254 case 303:
255 {
256 String method = request.getMethod();
257 if (HttpMethod.HEAD.is(method))
258 return redirect(request, response, listener, newURI, method);
259 else
260 return redirect(request, response, listener, newURI, HttpMethod.GET.asString());
261 }
262 case 307:
263 {
264
265 return redirect(request, response, listener, newURI, request.getMethod());
266 }
267 default:
268 {
269 fail(request, response, new HttpResponseException("Unhandled HTTP status code " + status, response));
270 return null;
271 }
272 }
273 }
274
275 private Request redirect(Request request, Response response, Response.CompleteListener listener, URI location, String method)
276 {
277 HttpRequest httpRequest = (HttpRequest)request;
278 HttpConversation conversation = httpRequest.getConversation();
279 Integer redirects = (Integer)conversation.getAttribute(ATTRIBUTE);
280 if (redirects == null)
281 redirects = 0;
282 if (redirects < client.getMaxRedirects())
283 {
284 ++redirects;
285 conversation.setAttribute(ATTRIBUTE, redirects);
286 return sendRedirect(httpRequest, response, listener, location, method);
287 }
288 else
289 {
290 fail(request, response, new HttpResponseException("Max redirects exceeded " + redirects, response));
291 return null;
292 }
293 }
294
295 private Request sendRedirect(final HttpRequest httpRequest, Response response, Response.CompleteListener listener, URI location, String method)
296 {
297 try
298 {
299 Request redirect = client.copyRequest(httpRequest, location);
300
301
302 redirect.method(method);
303
304 redirect.onRequestBegin(new Request.BeginListener()
305 {
306 @Override
307 public void onBegin(Request redirect)
308 {
309 Throwable cause = httpRequest.getAbortCause();
310 if (cause != null)
311 redirect.abort(cause);
312 }
313 });
314
315 redirect.send(listener);
316 return redirect;
317 }
318 catch (Throwable x)
319 {
320 fail(httpRequest, response, x);
321 return null;
322 }
323 }
324
325 protected void fail(Request request, Response response, Throwable failure)
326 {
327 HttpConversation conversation = ((HttpRequest)request).getConversation();
328 conversation.updateResponseListeners(null);
329 List<Response.ResponseListener> listeners = conversation.getResponseListeners();
330 notifier.notifyFailure(listeners, response, failure);
331 notifier.notifyComplete(listeners, new Result(request, response, failure));
332 }
333 }