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 = 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}