View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
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   * Utility class that handles HTTP redirects.
40   * <p>
41   * Applications can disable redirection via {@link Request#followRedirects(boolean)}
42   * and then rely on this class to perform the redirect in a simpler way, for example:
43   * <pre>
44   * HttpRedirector redirector = new HttpRedirector(httpClient);
45   *
46   * Request request = httpClient.newRequest("http://host/path").followRedirects(false);
47   * ContentResponse response = request.send();
48   * while (redirector.isRedirect(response))
49   * {
50   *     // Validate the redirect URI
51   *     if (!validate(redirector.extractRedirectURI(response)))
52   *         break;
53   *
54   *     Result result = redirector.redirect(request, response);
55   *     request = result.getRequest();
56   *     response = result.getResponse();
57   * }
58   * </pre>
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      // The location may be relative so the scheme://authority part may be missing
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       * @param response the response to check for redirects
84       * @return whether the response code is a HTTP redirect code
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      * Redirects the given {@code response}, blocking until the redirect is complete.
103      *
104      * @param request the original request that triggered the redirect
105      * @param response the response to the original request
106      * @return a {@link Result} object containing the request to the redirected location and its response
107      * @throws InterruptedException if the thread is interrupted while waiting for the redirect to complete
108      * @throws ExecutionException if the redirect failed
109      * @see #redirect(Request, Response, Response.CompleteListener)
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             // If the application interrupts, we need to abort the redirect
139             redirect.abort(x);
140             throw x;
141         }
142     }
143 
144     /**
145      * Redirects the given {@code response} asynchronously.
146      *
147      * @param request the original request that triggered the redirect
148      * @param response the response to the original request
149      * @param listener the listener that receives response events
150      * @return the request to the redirected location
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      * Extracts and sanitizes (by making it absolute and escaping paths and query parameters)
179      * the redirect URI of the given {@code response}.
180      *
181      * @param response the response to extract the redirect URI from
182      * @return the absolute redirect URI, or null if the response does not contain a valid redirect location
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         // Redirects should be valid, absolute, URIs, with properly escaped paths and encoded
195         // query parameters. However, shit happens, and here we try our best to recover.
196 
197         try
198         {
199             // Direct hit first: if passes, we're good
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                     // Give up
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                 // Keep same method
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             // Use given method
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 }