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    }