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; 052 //the associated request 053 private ShiroHttpServletRequest request; 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 @Deprecated 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 @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}