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