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 = null;
52      //the associated request
53      private ShiroHttpServletRequest request = null;
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  
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     public String encodeUrl(String s) {
120         return encodeURL(s);
121     }
122 
123     /**
124      * Return <code>true</code> if the specified URL should be encoded with
125      * a session identifier.  This will be true if all of the following
126      * conditions are met:
127      * <ul>
128      * <li>The request we are responding to asked for a valid session
129      * <li>The requested session ID was not received via a cookie
130      * <li>The specified URL points back to somewhere within the web
131      * application that is responding to this request
132      * </ul>
133      *
134      * @param location Absolute URL to be validated
135      * @return {@code true} if the specified URL should be encoded with a session identifier, {@code false} otherwise.
136      */
137     protected boolean isEncodeable(final String location) {
138 
139         if (location == null)
140             return (false);
141 
142         // Is this an intra-document reference?
143         if (location.startsWith("#"))
144             return (false);
145 
146         // Are we in a valid session that is not using cookies?
147         final HttpServletRequest hreq = request;
148         final HttpSession session = hreq.getSession(false);
149         if (session == null)
150             return (false);
151         if (hreq.isRequestedSessionIdFromCookie())
152             return (false);
153 
154         return doIsEncodeable(hreq, session, location);
155     }
156 
157     private boolean doIsEncodeable(HttpServletRequest hreq, HttpSession session, String location) {
158         // Is this a valid absolute URL?
159         URL url;
160         try {
161             url = new URL(location);
162         } catch (MalformedURLException e) {
163             return (false);
164         }
165 
166         // Does this URL match down to (and including) the context path?
167         if (!hreq.getScheme().equalsIgnoreCase(url.getProtocol()))
168             return (false);
169         if (!hreq.getServerName().equalsIgnoreCase(url.getHost()))
170             return (false);
171         int serverPort = hreq.getServerPort();
172         if (serverPort == -1) {
173             if ("https".equals(hreq.getScheme()))
174                 serverPort = 443;
175             else
176                 serverPort = 80;
177         }
178         int urlPort = url.getPort();
179         if (urlPort == -1) {
180             if ("https".equals(url.getProtocol()))
181                 urlPort = 443;
182             else
183                 urlPort = 80;
184         }
185         if (serverPort != urlPort)
186             return (false);
187 
188         String contextPath = getRequest().getContextPath();
189         if (contextPath != null) {
190             String file = url.getFile();
191             if ((file == null) || !file.startsWith(contextPath))
192                 return (false);
193             String tok = ";" + DEFAULT_SESSION_ID_PARAMETER_NAME + "=" + session.getId();
194             if (file.indexOf(tok, contextPath.length()) >= 0)
195                 return (false);
196         }
197 
198         // This URL belongs to our web application, so it is encodeable
199         return (true);
200 
201     }
202 
203 
204     /**
205      * Convert (if necessary) and return the absolute URL that represents the
206      * resource referenced by this possibly relative URL.  If this URL is
207      * already absolute, return it unchanged.
208      *
209      * @param location URL to be (possibly) converted and then returned
210      * @return resource location as an absolute url
211      * @throws IllegalArgumentException if a MalformedURLException is
212      *                                  thrown when converting the relative URL to an absolute one
213      */
214     private String toAbsolute(String location) {
215 
216         if (location == null)
217             return (location);
218 
219         boolean leadingSlash = location.startsWith("/");
220 
221         if (leadingSlash || !hasScheme(location)) {
222 
223             StringBuilder buf = new StringBuilder();
224 
225             String scheme = request.getScheme();
226             String name = request.getServerName();
227             int port = request.getServerPort();
228 
229             try {
230                 buf.append(scheme).append("://").append(name);
231                 if ((scheme.equals("http") && port != 80)
232                         || (scheme.equals("https") && port != 443)) {
233                     buf.append(':').append(port);
234                 }
235                 if (!leadingSlash) {
236                     String relativePath = request.getRequestURI();
237                     int pos = relativePath.lastIndexOf('/');
238                     relativePath = relativePath.substring(0, pos);
239 
240                     String encodedURI = URLEncoder.encode(relativePath, getCharacterEncoding());
241                     buf.append(encodedURI).append('/');
242                 }
243                 buf.append(location);
244             } catch (IOException e) {
245                 IllegalArgumentException iae = new IllegalArgumentException(location);
246                 iae.initCause(e);
247                 throw iae;
248             }
249 
250             return buf.toString();
251 
252         } else {
253             return location;
254         }
255     }
256 
257     /**
258      * Determine if the character is allowed in the scheme of a URI.
259      * See RFC 2396, Section 3.1
260      *
261      * @param c the character to check
262      * @return {@code true} if the character is allowed in a URI scheme, {@code false} otherwise.
263      */
264     public static boolean isSchemeChar(char c) {
265         return Character.isLetterOrDigit(c) ||
266                 c == '+' || c == '-' || c == '.';
267     }
268 
269 
270     /**
271      * Returns {@code true} if the URI string has a {@code scheme} component, {@code false} otherwise.
272      *
273      * @param uri the URI string to check for a scheme component
274      * @return {@code true} if the URI string has a {@code scheme} component, {@code false} otherwise.
275      */
276     private boolean hasScheme(String uri) {
277         int len = uri.length();
278         for (int i = 0; i < len; i++) {
279             char c = uri.charAt(i);
280             if (c == ':') {
281                 return i > 0;
282             } else if (!isSchemeChar(c)) {
283                 return false;
284             }
285         }
286         return false;
287     }
288 
289     /**
290      * Return the specified URL with the specified session identifier suitably encoded.
291      *
292      * @param url       URL to be encoded with the session id
293      * @param sessionId Session id to be included in the encoded URL
294      * @return the url with the session identifer properly encoded.
295      */
296     protected String toEncoded(String url, String sessionId) {
297 
298         if ((url == null) || (sessionId == null))
299             return (url);
300 
301         String path = url;
302         String query = "";
303         String anchor = "";
304         int question = url.indexOf('?');
305         if (question >= 0) {
306             path = url.substring(0, question);
307             query = url.substring(question);
308         }
309         int pound = path.indexOf('#');
310         if (pound >= 0) {
311             anchor = path.substring(pound);
312             path = path.substring(0, pound);
313         }
314         StringBuilder sb = new StringBuilder(path);
315         if (sb.length() > 0) { // session id param can't be first.
316             sb.append(";");
317             sb.append(DEFAULT_SESSION_ID_PARAMETER_NAME);
318             sb.append("=");
319             sb.append(sessionId);
320         }
321         sb.append(anchor);
322         sb.append(query);
323         return (sb.toString());
324 
325     }
326 }