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