View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.web.servlet;
20  
21  import javax.servlet.ServletContext;
22  import javax.servlet.http.HttpServletRequest;
23  import javax.servlet.http.HttpServletResponse;
24  import javax.servlet.http.HttpServletResponseWrapper;
25  import javax.servlet.http.HttpSession;
26  import java.io.IOException;
27  import java.net.MalformedURLException;
28  import java.net.URL;
29  import java.net.URLEncoder;
30  
31  /**
32   * HttpServletResponse implementation to support URL Encoding of Shiro Session IDs.
33   * <p/>
34   * It is only used when using Shiro's native Session Management configuration (and not when using the Servlet
35   * Container session configuration, which is Shiro's default in a web environment).  Because the servlet container
36   * already performs url encoding of its own session ids, instances of this class are only needed when using Shiro
37   * native sessions.
38   * <p/>
39   * Note that this implementation relies in part on source code from the Tomcat 6.x distribution for
40   * encoding URLs for session ID URL Rewriting (we didn't want to re-invent the wheel).  Since Shiro is also
41   * Apache 2.0 license, all regular licenses and conditions have remained in tact.
42   *
43   * @since 0.2
44   */
45  public class ShiroHttpServletResponse extends HttpServletResponseWrapper {
46  
47      //TODO - complete JavaDoc
48  
49      private static final String DEFAULT_SESSION_ID_PARAMETER_NAME = ShiroHttpSession.DEFAULT_SESSION_ID_NAME;
50  
51      private ServletContext context;
52      //the associated request
53      private ShiroHttpServletRequest request;
54  
55      public ShiroHttpServletResponse(HttpServletResponse wrapped, ServletContext context, ShiroHttpServletRequest request) {
56          super(wrapped);
57          this.context = context;
58          this.request = request;
59      }
60  
61      @SuppressWarnings({"UnusedDeclaration"})
62      public ServletContext getContext() {
63          return context;
64      }
65  
66      @SuppressWarnings({"UnusedDeclaration"})
67      public void setContext(ServletContext context) {
68          this.context = context;
69      }
70  
71      public ShiroHttpServletRequest getRequest() {
72          return request;
73      }
74  
75      @SuppressWarnings({"UnusedDeclaration"})
76      public void setRequest(ShiroHttpServletRequest request) {
77          this.request = request;
78      }
79  
80      /**
81       * Encode the session identifier associated with this response
82       * into the specified redirect URL, if necessary.
83       *
84       * @param url URL to be encoded
85       */
86      public String encodeRedirectURL(String url) {
87          if (isEncodeable(toAbsolute(url))) {
88              return toEncoded(url, request.getSession().getId());
89          } else {
90              return url;
91          }
92      }
93  
94      @Deprecated
95      public String encodeRedirectUrl(String s) {
96          return encodeRedirectURL(s);
97      }
98  
99  
100     /**
101      * Encode the session identifier associated with this response
102      * into the specified URL, if necessary.
103      *
104      * @param url URL to be encoded
105      */
106     public String encodeURL(String url) {
107         String absolute = toAbsolute(url);
108         if (isEncodeable(absolute)) {
109             // W3c spec clearly said
110             if (url.equalsIgnoreCase("")) {
111                 url = absolute;
112             }
113             return toEncoded(url, request.getSession().getId());
114         } else {
115             return url;
116         }
117     }
118 
119     @Deprecated
120     public String encodeUrl(String s) {
121         return encodeURL(s);
122     }
123 
124     /**
125      * Return <code>true</code> if the specified URL should be encoded with
126      * a session identifier.  This will be true if all of the following
127      * conditions are met:
128      * <ul>
129      * <li>The request we are responding to asked for a valid session
130      * <li>The requested session ID was not received via a cookie
131      * <li>The specified URL points back to somewhere within the web
132      * application that is responding to this request
133      * </ul>
134      *
135      * @param location Absolute URL to be validated
136      * @return {@code true} if the specified URL should be encoded with a session identifier, {@code false} otherwise.
137      */
138     protected boolean isEncodeable(final String location) {
139 
140         // First check if URL rewriting is disabled globally
141         if (Boolean.FALSE.equals(request.getAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED))) {
142             return (false);
143         }
144 
145         if (location == null) {
146             return (false);
147         }
148 
149         // Is this an intra-document reference?
150         if (location.startsWith("#")) {
151             return (false);
152         }
153 
154         // Are we in a valid session that is not using cookies?
155         final HttpServletRequest hreq = request;
156         final HttpSession session = hreq.getSession(false);
157         if (session == null) {
158             return (false);
159         }
160         if (hreq.isRequestedSessionIdFromCookie()) {
161             return (false);
162         }
163 
164         return doIsEncodeable(hreq, session, location);
165     }
166 
167     @SuppressWarnings({"checkstyle:CyclomaticComplexity", "checkstyle:NPathComplexity", "checkstyle:MagicNumber"})
168     private boolean doIsEncodeable(HttpServletRequest hreq, HttpSession session, String location) {
169         // Is this a valid absolute URL?
170         URL url;
171         try {
172             url = new URL(location);
173         } catch (MalformedURLException e) {
174             return (false);
175         }
176 
177         // Does this URL match down to (and including) the context path?
178         if (!hreq.getScheme().equalsIgnoreCase(url.getProtocol())) {
179             return (false);
180         }
181         if (!hreq.getServerName().equalsIgnoreCase(url.getHost())) {
182             return (false);
183         }
184         int serverPort = hreq.getServerPort();
185         if (serverPort == -1) {
186             if ("https".equals(hreq.getScheme())) {
187                 serverPort = 443;
188             } else {
189                 serverPort = 80;
190             }
191         }
192         int urlPort = url.getPort();
193         if (urlPort == -1) {
194             if ("https".equals(url.getProtocol())) {
195                 urlPort = 443;
196             } else {
197                 urlPort = 80;
198             }
199         }
200         if (serverPort != urlPort) {
201             return (false);
202         }
203 
204         String contextPath = getRequest().getContextPath();
205         if (contextPath != null) {
206             String file = url.getFile();
207             if ((file == null) || !file.startsWith(contextPath)) {
208                 return (false);
209             }
210             String tok = ";" + DEFAULT_SESSION_ID_PARAMETER_NAME + "=" + session.getId();
211             if (file.indexOf(tok, contextPath.length()) >= 0) {
212                 return (false);
213             }
214         }
215 
216         // This URL belongs to our web application, so it is encodeable
217         return (true);
218 
219     }
220 
221 
222     /**
223      * Convert (if necessary) and return the absolute URL that represents the
224      * resource referenced by this possibly relative URL.  If this URL is
225      * already absolute, return it unchanged.
226      *
227      * @param location URL to be (possibly) converted and then returned
228      * @return resource location as an absolute url
229      * @throws IllegalArgumentException if a MalformedURLException is
230      *                                  thrown when converting the relative URL to an absolute one
231      */
232     @SuppressWarnings("checkstyle:MagicNumber")
233     private String toAbsolute(String location) {
234 
235         if (location == null) {
236             return (location);
237         }
238 
239         boolean leadingSlash = location.startsWith("/");
240 
241         if (leadingSlash || !hasScheme(location)) {
242 
243             StringBuilder buf = new StringBuilder();
244 
245             String scheme = request.getScheme();
246             String name = request.getServerName();
247             int port = request.getServerPort();
248 
249             try {
250                 buf.append(scheme).append("://").append(name);
251                 if ((scheme.equals("http") && port != 80)
252                         || (scheme.equals("https") && port != 443)) {
253                     buf.append(':').append(port);
254                 }
255                 if (!leadingSlash) {
256                     String relativePath = request.getRequestURI();
257                     int pos = relativePath.lastIndexOf('/');
258                     relativePath = relativePath.substring(0, pos);
259 
260                     String encodedURI = URLEncoder.encode(relativePath, getCharacterEncoding());
261                     buf.append(encodedURI).append('/');
262                 }
263                 buf.append(location);
264             } catch (IOException e) {
265                 IllegalArgumentException iae = new IllegalArgumentException(location);
266                 iae.initCause(e);
267                 throw iae;
268             }
269 
270             return buf.toString();
271 
272         } else {
273             return location;
274         }
275     }
276 
277     /**
278      * Determine if the character is allowed in the scheme of a URI.
279      * See RFC 2396, Section 3.1
280      *
281      * @param c the character to check
282      * @return {@code true} if the character is allowed in a URI scheme, {@code false} otherwise.
283      */
284     public static boolean isSchemeChar(char c) {
285         return Character.isLetterOrDigit(c) || c == '+' || c == '-' || c == '.';
286     }
287 
288 
289     /**
290      * Returns {@code true} if the URI string has a {@code scheme} component, {@code false} otherwise.
291      *
292      * @param uri the URI string to check for a scheme component
293      * @return {@code true} if the URI string has a {@code scheme} component, {@code false} otherwise.
294      */
295     private boolean hasScheme(String uri) {
296         int len = uri.length();
297         for (int i = 0; i < len; i++) {
298             char c = uri.charAt(i);
299             if (c == ':') {
300                 return i > 0;
301             } else if (!isSchemeChar(c)) {
302                 return false;
303             }
304         }
305         return false;
306     }
307 
308     /**
309      * Return the specified URL with the specified session identifier suitably encoded.
310      *
311      * @param url       URL to be encoded with the session id
312      * @param sessionId Session id to be included in the encoded URL
313      * @return the url with the session identifier properly encoded.
314      */
315     protected String toEncoded(String url, String sessionId) {
316 
317         if ((url == null) || (sessionId == null)) {
318             return (url);
319         }
320 
321         String path = url;
322         String query = "";
323         String anchor = "";
324         int question = url.indexOf('?');
325         if (question >= 0) {
326             path = url.substring(0, question);
327             query = url.substring(question);
328         }
329         int pound = path.indexOf('#');
330         if (pound >= 0) {
331             anchor = path.substring(pound);
332             path = path.substring(0, pound);
333         }
334         StringBuilder sb = new StringBuilder(path);
335         // session id param can't be first.
336         if (sb.length() > 0) {
337             sb.append(";");
338             sb.append(DEFAULT_SESSION_ID_PARAMETER_NAME);
339             sb.append("=");
340             sb.append(sessionId);
341         }
342         sb.append(anchor);
343         sb.append(query);
344         return (sb.toString());
345 
346     }
347 }