View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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.regex.Matcher;
25  import java.util.regex.Pattern;
26  
27  import org.eclipse.jetty.client.api.Request;
28  import org.eclipse.jetty.client.api.Response;
29  import org.eclipse.jetty.client.api.Result;
30  import org.eclipse.jetty.http.HttpMethod;
31  import org.eclipse.jetty.util.log.Log;
32  import org.eclipse.jetty.util.log.Logger;
33  
34  public class RedirectProtocolHandler extends Response.Listener.Empty implements ProtocolHandler
35  {
36      private static final Logger LOG = Log.getLogger(RedirectProtocolHandler.class);
37      private static final String SCHEME_REGEXP = "(^https?)";
38      private static final String AUTHORITY_REGEXP = "([^/\\?#]+)";
39      // The location may be relative so the scheme://authority part may be missing
40      private static final String DESTINATION_REGEXP = "(" + SCHEME_REGEXP + "://" + AUTHORITY_REGEXP + ")?";
41      private static final String PATH_REGEXP = "([^\\?#]*)";
42      private static final String QUERY_REGEXP = "([^#]*)";
43      private static final String FRAGMENT_REGEXP = "(.*)";
44      private static final Pattern URI_PATTERN = Pattern.compile(DESTINATION_REGEXP + PATH_REGEXP + QUERY_REGEXP + FRAGMENT_REGEXP);
45      private static final String ATTRIBUTE = RedirectProtocolHandler.class.getName() + ".redirects";
46  
47      private final HttpClient client;
48      private final ResponseNotifier notifier;
49  
50      public RedirectProtocolHandler(HttpClient client)
51      {
52          this.client = client;
53          this.notifier = new ResponseNotifier(client);
54      }
55  
56      @Override
57      public boolean accept(Request request, Response response)
58      {
59          switch (response.getStatus())
60          {
61              case 301:
62              case 302:
63              case 303:
64              case 307:
65                  return request.isFollowRedirects();
66          }
67          return false;
68      }
69  
70      @Override
71      public Response.Listener getResponseListener()
72      {
73          return this;
74      }
75  
76      @Override
77      public void onComplete(Result result)
78      {
79          if (!result.isFailed())
80          {
81              Request request = result.getRequest();
82              Response response = result.getResponse();
83              String location = response.getHeaders().get("location");
84              if (location != null)
85              {
86                  URI newURI = sanitize(location);
87                  LOG.debug("Redirecting to {} (Location: {})", newURI, location);
88                  if (newURI != null)
89                  {
90                      if (!newURI.isAbsolute())
91                          newURI = request.getURI().resolve(newURI);
92  
93                      int status = response.getStatus();
94                      switch (status)
95                      {
96                          case 301:
97                          {
98                              String method = request.method();
99                              if (HttpMethod.GET.is(method) || HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
100                                 redirect(result, method, newURI);
101                             else if (HttpMethod.POST.is(method))
102                                 redirect(result, HttpMethod.GET.asString(), newURI);
103                             else
104                                 fail(result, new HttpResponseException("HTTP protocol violation: received 301 for non GET/HEAD/POST/PUT request", response));
105                             break;
106                         }
107                         case 302:
108                         {
109                             String method = request.method();
110                             if (HttpMethod.HEAD.is(method) || HttpMethod.PUT.is(method))
111                                 redirect(result, method, newURI);
112                             else
113                                 redirect(result, HttpMethod.GET.asString(), newURI);
114                             break;
115                         }
116                         case 303:
117                         {
118                             String method = request.method();
119                             if (HttpMethod.HEAD.is(method))
120                                 redirect(result, method, newURI);
121                             else
122                                 redirect(result, HttpMethod.GET.asString(), newURI);
123                             break;
124                         }
125                         case 307:
126                         {
127                             // Keep same method
128                             redirect(result, request.method(), newURI);
129                             break;
130                         }
131                         default:
132                         {
133                             fail(result, new HttpResponseException("Unhandled HTTP status code " + status, response));
134                             break;
135                         }
136                     }
137                 }
138                 else
139                 {
140                     fail(result, new HttpResponseException("Malformed Location header " + location, response));
141                 }
142             }
143             else
144             {
145                 fail(result, new HttpResponseException("Missing Location header " + location, response));
146             }
147         }
148         else
149         {
150             fail(result, result.getFailure());
151         }
152     }
153 
154     private URI sanitize(String location)
155     {
156         // Redirects should be valid, absolute, URIs, with properly escaped paths and encoded
157         // query parameters. However, shit happens, and here we try our best to recover.
158 
159         try
160         {
161             // Direct hit first: if passes, we're good
162             return new URI(location);
163         }
164         catch (URISyntaxException x)
165         {
166             Matcher matcher = URI_PATTERN.matcher(location);
167             if (matcher.matches())
168             {
169                 String scheme = matcher.group(2);
170                 String authority = matcher.group(3);
171                 String path = matcher.group(4);
172                 String query = matcher.group(5);
173                 if (query.length() == 0)
174                     query = null;
175                 String fragment = matcher.group(6);
176                 if (fragment.length() == 0)
177                     fragment = null;
178                 try
179                 {
180                     return new URI(scheme, authority, path, query, fragment);
181                 }
182                 catch (URISyntaxException xx)
183                 {
184                     // Give up
185                 }
186             }
187             return null;
188         }
189     }
190 
191     private void redirect(Result result, String method, URI location)
192     {
193         final Request request = result.getRequest();
194         HttpConversation conversation = client.getConversation(request.getConversationID(), false);
195         Integer redirects = (Integer)conversation.getAttribute(ATTRIBUTE);
196         if (redirects == null)
197             redirects = 0;
198 
199         if (redirects < client.getMaxRedirects())
200         {
201             ++redirects;
202             conversation.setAttribute(ATTRIBUTE, redirects);
203 
204             Request redirect = client.copyRequest(request, location);
205 
206             // Use given method
207             redirect.method(method);
208 
209             redirect.onRequestBegin(new Request.BeginListener()
210             {
211                 @Override
212                 public void onBegin(Request redirect)
213                 {
214                     Throwable cause = request.getAbortCause();
215                     if (cause != null)
216                         redirect.abort(cause);
217                 }
218             });
219 
220             redirect.send(null);
221         }
222         else
223         {
224             fail(result, new HttpResponseException("Max redirects exceeded " + redirects, result.getResponse()));
225         }
226     }
227 
228     private void fail(Result result, Throwable failure)
229     {
230         Request request = result.getRequest();
231         Response response = result.getResponse();
232         HttpConversation conversation = client.getConversation(request.getConversationID(), false);
233         conversation.updateResponseListeners(null);
234         List<Response.ResponseListener> listeners = conversation.getResponseListeners();
235         notifier.notifyFailure(listeners, response, failure);
236         notifier.notifyComplete(listeners, new Result(request, response, failure));
237     }
238 }