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.util;
020
021 import org.apache.shiro.SecurityUtils;
022 import org.apache.shiro.session.Session;
023 import org.apache.shiro.subject.Subject;
024 import org.apache.shiro.util.StringUtils;
025 import org.apache.shiro.web.filter.AccessControlFilter;
026 import org.slf4j.Logger;
027 import org.slf4j.LoggerFactory;
028
029 import javax.servlet.ServletRequest;
030 import javax.servlet.ServletResponse;
031 import javax.servlet.http.HttpServletRequest;
032 import javax.servlet.http.HttpServletResponse;
033 import java.io.IOException;
034 import java.io.UnsupportedEncodingException;
035 import java.net.URLDecoder;
036 import java.util.Map;
037
038 /**
039 * Simple utility class for operations used across multiple class hierarchies in the web framework code.
040 * <p/>
041 * Some methods in this class were copied from the Spring Framework so we didn't have to re-invent the wheel,
042 * and in these cases, we have retained all license, copyright and author information.
043 *
044 * @since 0.9
045 */
046 public class WebUtils {
047
048 //TODO - complete JavaDoc
049
050 private static final Logger log = LoggerFactory.getLogger(WebUtils.class);
051
052 public static final String SERVLET_REQUEST_KEY = ServletRequest.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY";
053 public static final String SERVLET_RESPONSE_KEY = ServletResponse.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY";
054
055 /**
056 * {@link org.apache.shiro.session.Session Session} key used to save a request and later restore it, for example when redirecting to a
057 * requested page after login, equal to {@code shiroSavedRequest}.
058 */
059 public static final String SAVED_REQUEST_KEY = "shiroSavedRequest";
060
061 /**
062 * Standard Servlet 2.3+ spec request attributes for include URI and paths.
063 * <p>If included via a RequestDispatcher, the current resource will see the
064 * originating request. Its own URI and paths are exposed as request attributes.
065 */
066 public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
067 public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path";
068 public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path";
069 public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info";
070 public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string";
071
072 /**
073 * Standard Servlet 2.4+ spec request attributes for forward URI and paths.
074 * <p>If forwarded to via a RequestDispatcher, the current resource will see its
075 * own URI and paths. The originating URI and paths are exposed as request attributes.
076 */
077 public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri";
078 public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path";
079 public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path";
080 public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info";
081 public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string";
082
083 /**
084 * Default character encoding to use when <code>request.getCharacterEncoding</code>
085 * returns <code>null</code>, according to the Servlet spec.
086 *
087 * @see javax.servlet.ServletRequest#getCharacterEncoding
088 */
089 public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
090
091 /**
092 * Return the path within the web application for the given request.
093 * <p>Detects include request URL if called within a RequestDispatcher include.
094 * <p/>
095 * For example, for a request to URL
096 * <p/>
097 * <code>http://www.somehost.com/myapp/my/url.jsp</code>,
098 * <p/>
099 * for an application deployed to <code>/mayapp</code> (the application's context path), this method would return
100 * <p/>
101 * <code>/my/url.jsp</code>.
102 *
103 * @param request current HTTP request
104 * @return the path within the web application
105 */
106 public static String getPathWithinApplication(HttpServletRequest request) {
107 String contextPath = getContextPath(request);
108 String requestUri = getRequestUri(request);
109 if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) {
110 // Normal case: URI contains context path.
111 String path = requestUri.substring(contextPath.length());
112 return (StringUtils.hasText(path) ? path : "/");
113 } else {
114 // Special case: rather unusual.
115 return requestUri;
116 }
117 }
118
119 /**
120 * Return the request URI for the given request, detecting an include request
121 * URL if called within a RequestDispatcher include.
122 * <p>As the value returned by <code>request.getRequestURI()</code> is <i>not</i>
123 * decoded by the servlet container, this method will decode it.
124 * <p>The URI that the web container resolves <i>should</i> be correct, but some
125 * containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid"
126 * in the URI. This method cuts off such incorrect appendices.
127 *
128 * @param request current HTTP request
129 * @return the request URI
130 */
131 public static String getRequestUri(HttpServletRequest request) {
132 String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);
133 if (uri == null) {
134 uri = request.getRequestURI();
135 }
136 return normalize(decodeAndCleanUriString(request, uri));
137 }
138
139 /**
140 * Normalize a relative URI path that may have relative values ("/./",
141 * "/../", and so on ) it it. <strong>WARNING</strong> - This method is
142 * useful only for normalizing application-generated paths. It does not
143 * try to perform security checks for malicious input.
144 * Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in
145 * Tomcat trunk, r939305
146 *
147 * @param path Relative path to be normalized
148 *
149 */
150 private static String normalize(String path) {
151 return normalize(path, true);
152 }
153
154 /**
155 * Normalize a relative URI path that may have relative values ("/./",
156 * "/../", and so on ) it it. <strong>WARNING</strong> - This method is
157 * useful only for normalizing application-generated paths. It does not
158 * try to perform security checks for malicious input.
159 * Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in
160 * Tomcat trunk, r939305
161 *
162 * @param path Relative path to be normalized
163 * @param replaceBackSlash Should '\\' be replaced with '/'
164 */
165 private static String normalize(String path, boolean replaceBackSlash) {
166
167 if (path == null)
168 return null;
169
170 // Create a place for the normalized path
171 String normalized = path;
172
173 if (replaceBackSlash && normalized.indexOf('\\') >= 0)
174 normalized = normalized.replace('\\', '/');
175
176 if (normalized.equals("/."))
177 return "/";
178
179 // Add a leading "/" if necessary
180 if (!normalized.startsWith("/"))
181 normalized = "/" + normalized;
182
183 // Resolve occurrences of "//" in the normalized path
184 while (true) {
185 int index = normalized.indexOf("//");
186 if (index < 0)
187 break;
188 normalized = normalized.substring(0, index) +
189 normalized.substring(index + 1);
190 }
191
192 // Resolve occurrences of "/./" in the normalized path
193 while (true) {
194 int index = normalized.indexOf("/./");
195 if (index < 0)
196 break;
197 normalized = normalized.substring(0, index) +
198 normalized.substring(index + 2);
199 }
200
201 // Resolve occurrences of "/../" in the normalized path
202 while (true) {
203 int index = normalized.indexOf("/../");
204 if (index < 0)
205 break;
206 if (index == 0)
207 return (null); // Trying to go outside our context
208 int index2 = normalized.lastIndexOf('/', index - 1);
209 normalized = normalized.substring(0, index2) +
210 normalized.substring(index + 3);
211 }
212
213 // Return the normalized path that we have completed
214 return (normalized);
215
216 }
217
218
219 /**
220 * Decode the supplied URI string and strips any extraneous portion after a ';'.
221 *
222 * @param request the incoming HttpServletRequest
223 * @param uri the application's URI string
224 * @return the supplied URI string stripped of any extraneous portion after a ';'.
225 */
226 private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
227 uri = decodeRequestString(request, uri);
228 int semicolonIndex = uri.indexOf(';');
229 return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
230 }
231
232 /**
233 * Return the context path for the given request, detecting an include request
234 * URL if called within a RequestDispatcher include.
235 * <p>As the value returned by <code>request.getContextPath()</code> is <i>not</i>
236 * decoded by the servlet container, this method will decode it.
237 *
238 * @param request current HTTP request
239 * @return the context path
240 */
241 public static String getContextPath(HttpServletRequest request) {
242 String contextPath = (String) request.getAttribute(INCLUDE_CONTEXT_PATH_ATTRIBUTE);
243 if (contextPath == null) {
244 contextPath = request.getContextPath();
245 }
246 if ("/".equals(contextPath)) {
247 // Invalid case, but happens for includes on Jetty: silently adapt it.
248 contextPath = "";
249 }
250 return decodeRequestString(request, contextPath);
251 }
252
253 /**
254 * Decode the given source string with a URLDecoder. The encoding will be taken
255 * from the request, falling back to the default "ISO-8859-1".
256 * <p>The default implementation uses <code>URLDecoder.decode(input, enc)</code>.
257 *
258 * @param request current HTTP request
259 * @param source the String to decode
260 * @return the decoded String
261 * @see #DEFAULT_CHARACTER_ENCODING
262 * @see javax.servlet.ServletRequest#getCharacterEncoding
263 * @see java.net.URLDecoder#decode(String, String)
264 * @see java.net.URLDecoder#decode(String)
265 */
266 @SuppressWarnings({"deprecation"})
267 public static String decodeRequestString(HttpServletRequest request, String source) {
268 String enc = determineEncoding(request);
269 try {
270 return URLDecoder.decode(source, enc);
271 }
272 catch (UnsupportedEncodingException ex) {
273 if (log.isWarnEnabled()) {
274 log.warn("Could not decode request string [" + source + "] with encoding '" + enc +
275 "': falling back to platform default encoding; exception message: " + ex.getMessage());
276 }
277 return URLDecoder.decode(source);
278 }
279 }
280
281 /**
282 * Determine the encoding for the given request.
283 * Can be overridden in subclasses.
284 * <p>The default implementation checks the request's
285 * {@link ServletRequest#getCharacterEncoding() character encoding}, and if that
286 * <code>null</code>, falls back to the {@link #DEFAULT_CHARACTER_ENCODING}.
287 *
288 * @param request current HTTP request
289 * @return the encoding for the request (never <code>null</code>)
290 * @see javax.servlet.ServletRequest#getCharacterEncoding()
291 */
292 protected static String determineEncoding(HttpServletRequest request) {
293 String enc = request.getCharacterEncoding();
294 if (enc == null) {
295 enc = DEFAULT_CHARACTER_ENCODING;
296 }
297 return enc;
298 }
299
300 /*
301 * Returns {@code true} IFF the specified {@code SubjectContext}:
302 * <ol>
303 * <li>A {@link WebSubjectContext} instance</li>
304 * <li>The {@code WebSubjectContext}'s request/response pair are not null</li>
305 * <li>The request is an {@link HttpServletRequest} instance</li>
306 * <li>The response is an {@link HttpServletResponse} instance</li>
307 * </ol>
308 *
309 * @param context the SubjectContext to check to see if it is HTTP compatible.
310 * @return {@code true} IFF the specified context has HTTP request/response objects, {@code false} otherwise.
311 * @since 1.0
312 */
313
314 public static boolean isWeb(Object requestPairSource) {
315 return requestPairSource instanceof RequestPairSource && isWeb((RequestPairSource) requestPairSource);
316 }
317
318 public static boolean isHttp(Object requestPairSource) {
319 return requestPairSource instanceof RequestPairSource && isHttp((RequestPairSource) requestPairSource);
320 }
321
322 public static ServletRequest getRequest(Object requestPairSource) {
323 if (requestPairSource instanceof RequestPairSource) {
324 return ((RequestPairSource) requestPairSource).getServletRequest();
325 }
326 return null;
327 }
328
329 public static ServletResponse getResponse(Object requestPairSource) {
330 if (requestPairSource instanceof RequestPairSource) {
331 return ((RequestPairSource) requestPairSource).getServletResponse();
332 }
333 return null;
334 }
335
336 public static HttpServletRequest getHttpRequest(Object requestPairSource) {
337 ServletRequest request = getRequest(requestPairSource);
338 if (request instanceof HttpServletRequest) {
339 return (HttpServletRequest) request;
340 }
341 return null;
342 }
343
344 public static HttpServletResponse getHttpResponse(Object requestPairSource) {
345 ServletResponse response = getResponse(requestPairSource);
346 if (response instanceof HttpServletResponse) {
347 return (HttpServletResponse) response;
348 }
349 return null;
350 }
351
352 private static boolean isWeb(RequestPairSource source) {
353 ServletRequest request = source.getServletRequest();
354 ServletResponse response = source.getServletResponse();
355 return request != null && response != null;
356 }
357
358 private static boolean isHttp(RequestPairSource source) {
359 ServletRequest request = source.getServletRequest();
360 ServletResponse response = source.getServletResponse();
361 return request instanceof HttpServletRequest && response instanceof HttpServletResponse;
362 }
363
364 /**
365 * A convenience method that merely casts the incoming <code>ServletRequest</code> to an
366 * <code>HttpServletRequest</code>:
367 * <p/>
368 * <code>return (HttpServletRequest)request;</code>
369 * <p/>
370 * Logic could be changed in the future for logging or throwing an meaningful exception in
371 * non HTTP request environments (e.g. Portlet API).
372 *
373 * @param request the incoming ServletRequest
374 * @return the <code>request</code> argument casted to an <code>HttpServletRequest</code>.
375 */
376 public static HttpServletRequest toHttp(ServletRequest request) {
377 return (HttpServletRequest) request;
378 }
379
380 /**
381 * A convenience method that merely casts the incoming <code>ServletResponse</code> to an
382 * <code>HttpServletResponse</code>:
383 * <p/>
384 * <code>return (HttpServletResponse)response;</code>
385 * <p/>
386 * Logic could be changed in the future for logging or throwing an meaningful exception in
387 * non HTTP request environments (e.g. Portlet API).
388 *
389 * @param response the outgoing ServletResponse
390 * @return the <code>response</code> argument casted to an <code>HttpServletResponse</code>.
391 */
392 public static HttpServletResponse toHttp(ServletResponse response) {
393 return (HttpServletResponse) response;
394 }
395
396 /**
397 * Redirects the current request to a new URL based on the given parameters.
398 *
399 * @param request the servlet request.
400 * @param response the servlet response.
401 * @param url the URL to redirect the user to.
402 * @param queryParams a map of parameters that should be set as request parameters for the new request.
403 * @param contextRelative true if the URL is relative to the servlet context path, or false if the URL is absolute.
404 * @param http10Compatible whether to stay compatible with HTTP 1.0 clients.
405 * @throws java.io.IOException if thrown by response methods.
406 */
407 public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException {
408 RedirectView view = new RedirectView(url, contextRelative, http10Compatible);
409 view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response));
410 }
411
412 /**
413 * Redirects the current request to a new URL based on the given parameters and default values
414 * for unspecified parameters.
415 *
416 * @param request the servlet request.
417 * @param response the servlet response.
418 * @param url the URL to redirect the user to.
419 * @throws java.io.IOException if thrown by response methods.
420 */
421 public static void issueRedirect(ServletRequest request, ServletResponse response, String url) throws IOException {
422 issueRedirect(request, response, url, null, true, true);
423 }
424
425 /**
426 * Redirects the current request to a new URL based on the given parameters and default values
427 * for unspecified parameters.
428 *
429 * @param request the servlet request.
430 * @param response the servlet response.
431 * @param url the URL to redirect the user to.
432 * @param queryParams a map of parameters that should be set as request parameters for the new request.
433 * @throws java.io.IOException if thrown by response methods.
434 */
435 public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams) throws IOException {
436 issueRedirect(request, response, url, queryParams, true, true);
437 }
438
439 /**
440 * Redirects the current request to a new URL based on the given parameters and default values
441 * for unspecified parameters.
442 *
443 * @param request the servlet request.
444 * @param response the servlet response.
445 * @param url the URL to redirect the user to.
446 * @param queryParams a map of parameters that should be set as request parameters for the new request.
447 * @param contextRelative true if the URL is relative to the servlet context path, or false if the URL is absolute.
448 * @throws java.io.IOException if thrown by response methods.
449 */
450 public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative) throws IOException {
451 issueRedirect(request, response, url, queryParams, contextRelative, true);
452 }
453
454 /**
455 * <p>Checks to see if a request param is considered true using a loose matching strategy for
456 * general values that indicate that something is true or enabled, etc.</p>
457 * <p/>
458 * <p>Values that are considered "true" include (case-insensitive): true, t, 1, enabled, y, yes, on.</p>
459 *
460 * @param request the servlet request
461 * @param paramName @return true if the param value is considered true or false if it isn't.
462 * @return true if the given parameter is considered "true" - false otherwise.
463 */
464 public static boolean isTrue(ServletRequest request, String paramName) {
465 String value = getCleanParam(request, paramName);
466 return value != null &&
467 (value.equalsIgnoreCase("true") ||
468 value.equalsIgnoreCase("t") ||
469 value.equalsIgnoreCase("1") ||
470 value.equalsIgnoreCase("enabled") ||
471 value.equalsIgnoreCase("y") ||
472 value.equalsIgnoreCase("yes") ||
473 value.equalsIgnoreCase("on"));
474 }
475
476 /**
477 * Convenience method that returns a request parameter value, first running it through
478 * {@link StringUtils#clean(String)}.
479 *
480 * @param request the servlet request.
481 * @param paramName the parameter name.
482 * @return the clean param value, or null if the param does not exist or is empty.
483 */
484 public static String getCleanParam(ServletRequest request, String paramName) {
485 return StringUtils.clean(request.getParameter(paramName));
486 }
487
488 public static void saveRequest(ServletRequest request) {
489 Subject subject = SecurityUtils.getSubject();
490 Session session = subject.getSession();
491 HttpServletRequest httpRequest = toHttp(request);
492 SavedRequest savedRequest = new SavedRequest(httpRequest);
493 session.setAttribute(SAVED_REQUEST_KEY, savedRequest);
494 }
495
496 public static SavedRequest getAndClearSavedRequest(ServletRequest request) {
497 SavedRequest savedRequest = getSavedRequest(request);
498 if (savedRequest != null) {
499 Subject subject = SecurityUtils.getSubject();
500 Session session = subject.getSession();
501 session.removeAttribute(SAVED_REQUEST_KEY);
502 }
503 return savedRequest;
504 }
505
506 public static SavedRequest getSavedRequest(ServletRequest request) {
507 SavedRequest savedRequest = null;
508 Subject subject = SecurityUtils.getSubject();
509 Session session = subject.getSession(false);
510 if (session != null) {
511 savedRequest = (SavedRequest) session.getAttribute(SAVED_REQUEST_KEY);
512 }
513 return savedRequest;
514 }
515
516 /**
517 * Redirects the to the request url from a previously
518 * {@link #saveRequest(javax.servlet.ServletRequest) saved} request, or if there is no saved request, redirects the
519 * end user to the specified {@code fallbackUrl}. If there is no saved request or fallback url, this method
520 * throws an {@link IllegalStateException}.
521 * <p/>
522 * This method is primarily used to support a common login scenario - if an unauthenticated user accesses a
523 * page that requires authentication, it is expected that request is
524 * {@link #saveRequest(javax.servlet.ServletRequest) saved} first and then redirected to the login page. Then,
525 * after a successful login, this method can be called to redirect them back to their originally requested URL, a
526 * nice usability feature.
527 *
528 * @param request the incoming request
529 * @param response the outgoing response
530 * @param fallbackUrl the fallback url to redirect to if there is no saved request available.
531 * @throws IllegalStateException if there is no saved request and the {@code fallbackUrl} is {@code null}.
532 * @throws IOException if there is an error redirecting
533 * @since 1.0
534 */
535 public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl)
536 throws IOException {
537 String successUrl = null;
538 boolean contextRelative = true;
539 SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
540 if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) {
541 successUrl = savedRequest.getRequestUrl();
542 contextRelative = false;
543 }
544
545 if (successUrl == null) {
546 successUrl = fallbackUrl;
547 }
548
549 if (successUrl == null) {
550 throw new IllegalStateException("Success URL not available via saved request or via the " +
551 "successUrlFallback method parameter. One of these must be non-null for " +
552 "issueSuccessRedirect() to work.");
553 }
554
555 WebUtils.issueRedirect(request, response, successUrl, null, contextRelative);
556 }
557
558 }