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