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 */
019 package org.apache.shiro.web.servlet;
020
021 import javax.servlet.ServletContext;
022 import javax.servlet.http.HttpServletRequest;
023 import javax.servlet.http.HttpServletResponse;
024 import javax.servlet.http.HttpServletResponseWrapper;
025 import javax.servlet.http.HttpSession;
026 import java.io.IOException;
027 import java.net.MalformedURLException;
028 import java.net.URL;
029 import 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 */
045 public 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 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 }