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        // First check if URL rewriting is disabled globally
140        if (Boolean.FALSE.equals(request.getAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED)))
141            return (false);
142
143        if (location == null)
144            return (false);
145
146        // Is this an intra-document reference?
147        if (location.startsWith("#"))
148            return (false);
149
150        // Are we in a valid session that is not using cookies?
151        final HttpServletRequest hreq = request;
152        final HttpSession session = hreq.getSession(false);
153        if (session == null)
154            return (false);
155        if (hreq.isRequestedSessionIdFromCookie())
156            return (false);
157
158        return doIsEncodeable(hreq, session, location);
159    }
160
161    private boolean doIsEncodeable(HttpServletRequest hreq, HttpSession session, String location) {
162        // Is this a valid absolute URL?
163        URL url;
164        try {
165            url = new URL(location);
166        } catch (MalformedURLException e) {
167            return (false);
168        }
169
170        // Does this URL match down to (and including) the context path?
171        if (!hreq.getScheme().equalsIgnoreCase(url.getProtocol()))
172            return (false);
173        if (!hreq.getServerName().equalsIgnoreCase(url.getHost()))
174            return (false);
175        int serverPort = hreq.getServerPort();
176        if (serverPort == -1) {
177            if ("https".equals(hreq.getScheme()))
178                serverPort = 443;
179            else
180                serverPort = 80;
181        }
182        int urlPort = url.getPort();
183        if (urlPort == -1) {
184            if ("https".equals(url.getProtocol()))
185                urlPort = 443;
186            else
187                urlPort = 80;
188        }
189        if (serverPort != urlPort)
190            return (false);
191
192        String contextPath = getRequest().getContextPath();
193        if (contextPath != null) {
194            String file = url.getFile();
195            if ((file == null) || !file.startsWith(contextPath))
196                return (false);
197            String tok = ";" + DEFAULT_SESSION_ID_PARAMETER_NAME + "=" + session.getId();
198            if (file.indexOf(tok, contextPath.length()) >= 0)
199                return (false);
200        }
201
202        // This URL belongs to our web application, so it is encodeable
203        return (true);
204
205    }
206
207
208    /**
209     * Convert (if necessary) and return the absolute URL that represents the
210     * resource referenced by this possibly relative URL.  If this URL is
211     * already absolute, return it unchanged.
212     *
213     * @param location URL to be (possibly) converted and then returned
214     * @return resource location as an absolute url
215     * @throws IllegalArgumentException if a MalformedURLException is
216     *                                  thrown when converting the relative URL to an absolute one
217     */
218    private String toAbsolute(String location) {
219
220        if (location == null)
221            return (location);
222
223        boolean leadingSlash = location.startsWith("/");
224
225        if (leadingSlash || !hasScheme(location)) {
226
227            StringBuilder buf = new StringBuilder();
228
229            String scheme = request.getScheme();
230            String name = request.getServerName();
231            int port = request.getServerPort();
232
233            try {
234                buf.append(scheme).append("://").append(name);
235                if ((scheme.equals("http") && port != 80)
236                        || (scheme.equals("https") && port != 443)) {
237                    buf.append(':').append(port);
238                }
239                if (!leadingSlash) {
240                    String relativePath = request.getRequestURI();
241                    int pos = relativePath.lastIndexOf('/');
242                    relativePath = relativePath.substring(0, pos);
243
244                    String encodedURI = URLEncoder.encode(relativePath, getCharacterEncoding());
245                    buf.append(encodedURI).append('/');
246                }
247                buf.append(location);
248            } catch (IOException e) {
249                IllegalArgumentException iae = new IllegalArgumentException(location);
250                iae.initCause(e);
251                throw iae;
252            }
253
254            return buf.toString();
255
256        } else {
257            return location;
258        }
259    }
260
261    /**
262     * Determine if the character is allowed in the scheme of a URI.
263     * See RFC 2396, Section 3.1
264     *
265     * @param c the character to check
266     * @return {@code true} if the character is allowed in a URI scheme, {@code false} otherwise.
267     */
268    public static boolean isSchemeChar(char c) {
269        return Character.isLetterOrDigit(c) ||
270                c == '+' || c == '-' || c == '.';
271    }
272
273
274    /**
275     * Returns {@code true} if the URI string has a {@code scheme} component, {@code false} otherwise.
276     *
277     * @param uri the URI string to check for a scheme component
278     * @return {@code true} if the URI string has a {@code scheme} component, {@code false} otherwise.
279     */
280    private boolean hasScheme(String uri) {
281        int len = uri.length();
282        for (int i = 0; i < len; i++) {
283            char c = uri.charAt(i);
284            if (c == ':') {
285                return i > 0;
286            } else if (!isSchemeChar(c)) {
287                return false;
288            }
289        }
290        return false;
291    }
292
293    /**
294     * Return the specified URL with the specified session identifier suitably encoded.
295     *
296     * @param url       URL to be encoded with the session id
297     * @param sessionId Session id to be included in the encoded URL
298     * @return the url with the session identifer properly encoded.
299     */
300    protected String toEncoded(String url, String sessionId) {
301
302        if ((url == null) || (sessionId == null))
303            return (url);
304
305        String path = url;
306        String query = "";
307        String anchor = "";
308        int question = url.indexOf('?');
309        if (question >= 0) {
310            path = url.substring(0, question);
311            query = url.substring(question);
312        }
313        int pound = path.indexOf('#');
314        if (pound >= 0) {
315            anchor = path.substring(pound);
316            path = path.substring(0, pound);
317        }
318        StringBuilder sb = new StringBuilder(path);
319        if (sb.length() > 0) { // session id param can't be first.
320            sb.append(";");
321            sb.append(DEFAULT_SESSION_ID_PARAMETER_NAME);
322            sb.append("=");
323            sb.append(sessionId);
324        }
325        sb.append(anchor);
326        sb.append(query);
327        return (sb.toString());
328
329    }
330}