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    }