001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.shiro.web.servlet; 020 021import javax.servlet.ServletContext; 022import javax.servlet.http.HttpServletRequest; 023import javax.servlet.http.HttpServletResponse; 024import javax.servlet.http.HttpServletResponseWrapper; 025import javax.servlet.http.HttpSession; 026import java.io.IOException; 027import java.net.MalformedURLException; 028import java.net.URL; 029import java.net.URLEncoder; 030 031/** 032 * HttpServletResponse implementation to support URL Encoding of Shiro Session IDs. 033 * <p/> 034 * It is only used when using Shiro's native Session Management configuration (and not when using the Servlet 035 * Container session configuration, which is Shiro's default in a web environment). Because the servlet container 036 * already performs url encoding of its own session ids, instances of this class are only needed when using Shiro 037 * native sessions. 038 * <p/> 039 * Note that this implementation relies in part on source code from the Tomcat 6.x distribution for 040 * encoding URLs for session ID URL Rewriting (we didn't want to re-invent the wheel). Since Shiro is also 041 * Apache 2.0 license, all regular licenses and conditions have remained in tact. 042 * 043 * @since 0.2 044 */ 045public class ShiroHttpServletResponse extends HttpServletResponseWrapper { 046 047 //TODO - complete JavaDoc 048 049 private static final String DEFAULT_SESSION_ID_PARAMETER_NAME = ShiroHttpSession.DEFAULT_SESSION_ID_NAME; 050 051 private ServletContext context = null; 052 //the associated request 053 private ShiroHttpServletRequest request = null; 054 055 public ShiroHttpServletResponse(HttpServletResponse wrapped, ServletContext context, ShiroHttpServletRequest request) { 056 super(wrapped); 057 this.context = context; 058 this.request = request; 059 } 060 061 @SuppressWarnings({"UnusedDeclaration"}) 062 public ServletContext getContext() { 063 return context; 064 } 065 066 @SuppressWarnings({"UnusedDeclaration"}) 067 public void setContext(ServletContext context) { 068 this.context = context; 069 } 070 071 public ShiroHttpServletRequest getRequest() { 072 return request; 073 } 074 075 @SuppressWarnings({"UnusedDeclaration"}) 076 public void setRequest(ShiroHttpServletRequest request) { 077 this.request = request; 078 } 079 080 /** 081 * Encode the session identifier associated with this response 082 * into the specified redirect URL, if necessary. 083 * 084 * @param url URL to be encoded 085 */ 086 public String encodeRedirectURL(String url) { 087 if (isEncodeable(toAbsolute(url))) { 088 return toEncoded(url, request.getSession().getId()); 089 } else { 090 return url; 091 } 092 } 093 094 095 public String encodeRedirectUrl(String s) { 096 return encodeRedirectURL(s); 097 } 098 099 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}