blob: f41766c64dc1b5018ea8c0647e5d9978e241608f [file] [log] [blame]
/*
* Copyright 2005-2008 Les Hazlewood and the original authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jsecurity.web.servlet;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
/**
* HttpServletResponse implementation to support URL Encoding of JSecurity Session IDs.
*
* It is only used when using JSecurity's native Session Management configuration (and not when using the Servlet
* Container session configuration, which is JSecurity's default in a web environment). Because the servlet container
* already performs url encoding of its own session ids, instances of this class are only needed when using JSecurity
* native sessions.
*
* <p>Note that this implementation relies in part on source code from the Tomcat 6.x distribution for
* encoding URLs for session ID URL Rewriting (we didn't want to re-invent the wheel). Since JSecurity is also
* Apache 2.0 license, all regular licenses and conditions have remained in tact.
*
* @author Les Hazlewood
* @since 0.2
*/
@SuppressWarnings({"deprecated", "deprecation"})
public class JSecurityHttpServletResponse extends HttpServletResponseWrapper {
private static final String DEFAULT_SESSION_ID_PARAMETER_NAME = JSecurityHttpSession.DEFAULT_SESSION_ID_NAME;
private ServletContext context = null;
//the associated request
private JSecurityHttpServletRequest request = null;
public JSecurityHttpServletResponse( HttpServletResponse wrapped, ServletContext context, JSecurityHttpServletRequest request ) {
super( wrapped );
this.context = context;
this.request = request;
}
public ServletContext getContext() {
return context;
}
public void setContext( ServletContext context ) {
this.context = context;
}
public JSecurityHttpServletRequest getRequest() {
return request;
}
public void setRequest( JSecurityHttpServletRequest request ) {
this.request = request;
}
/**
* Encode the session identifier associated with this response
* into the specified redirect URL, if necessary.
*
* @param url URL to be encoded
*/
public String encodeRedirectURL( String url ) {
if ( isEncodeable( toAbsolute( url ) ) ) {
return toEncoded( url, request.getSession().getId() );
} else {
return url;
}
}
public String encodeRedirectUrl( String s ) {
return encodeRedirectURL( s );
}
/**
* Encode the session identifier associated with this response
* into the specified URL, if necessary.
*
* @param url URL to be encoded
*/
public String encodeURL( String url ) {
String absolute = toAbsolute( url );
if ( isEncodeable( absolute ) ) {
// W3c spec clearly said
if ( url.equalsIgnoreCase( "" ) ) {
url = absolute;
}
return toEncoded( url, request.getSession().getId() );
} else {
return url;
}
}
public String encodeUrl( String s ) {
return encodeURL( s );
}
/**
* Return <code>true</code> if the specified URL should be encoded with
* a session identifier. This will be true if all of the following
* conditions are met:
* <ul>
* <li>The request we are responding to asked for a valid session
* <li>The requested session ID was not received via a cookie
* <li>The specified URL points back to somewhere within the web
* application that is responding to this request
* </ul>
*
* @param location Absolute URL to be validated
*/
protected boolean isEncodeable( final String location ) {
if ( location == null )
return ( false );
// Is this an intra-document reference?
if ( location.startsWith( "#" ) )
return ( false );
// Are we in a valid session that is not using cookies?
final HttpServletRequest hreq = request;
final HttpSession session = hreq.getSession( false );
if ( session == null )
return ( false );
if ( hreq.isRequestedSessionIdFromCookie() )
return ( false );
return doIsEncodeable( hreq, session, location );
}
private boolean doIsEncodeable( HttpServletRequest hreq, HttpSession session, String location ) {
// Is this a valid absolute URL?
URL url = null;
try {
url = new URL( location );
} catch ( MalformedURLException e ) {
return ( false );
}
// Does this URL match down to (and including) the context path?
if ( !hreq.getScheme().equalsIgnoreCase( url.getProtocol() ) )
return ( false );
if ( !hreq.getServerName().equalsIgnoreCase( url.getHost() ) )
return ( false );
int serverPort = hreq.getServerPort();
if ( serverPort == -1 ) {
if ( "https".equals( hreq.getScheme() ) )
serverPort = 443;
else
serverPort = 80;
}
int urlPort = url.getPort();
if ( urlPort == -1 ) {
if ( "https".equals( url.getProtocol() ) )
urlPort = 443;
else
urlPort = 80;
}
if ( serverPort != urlPort )
return ( false );
String contextPath = getRequest().getContextPath();
if ( contextPath != null ) {
String file = url.getFile();
if ( ( file == null ) || !file.startsWith( contextPath ) )
return ( false );
String tok = ";" + DEFAULT_SESSION_ID_PARAMETER_NAME + "=" + session.getId();
if ( file.indexOf( tok, contextPath.length() ) >= 0 )
return ( false );
}
// This URL belongs to our web application, so it is encodeable
return ( true );
}
/**
* Convert (if necessary) and return the absolute URL that represents the
* resource referenced by this possibly relative URL. If this URL is
* already absolute, return it unchanged.
*
* @param location URL to be (possibly) converted and then returned
* @throws IllegalArgumentException if a MalformedURLException is
* thrown when converting the relative URL to an absolute one
*/
private String toAbsolute( String location ) {
if ( location == null )
return ( location );
boolean leadingSlash = location.startsWith( "/" );
if ( leadingSlash || !hasScheme( location ) ) {
StringBuffer buf = new StringBuffer();
String scheme = request.getScheme();
String name = request.getServerName();
int port = request.getServerPort();
try {
buf.append( scheme ).append( "://" ).append( name );
if ( ( scheme.equals( "http" ) && port != 80 )
|| ( scheme.equals( "https" ) && port != 443 ) ) {
buf.append( ':' ).append( port );
}
if ( !leadingSlash ) {
String relativePath = request.getRequestURI();
int pos = relativePath.lastIndexOf( '/' );
relativePath = relativePath.substring( 0, pos );
String encodedURI = URLEncoder.encode( relativePath, getCharacterEncoding() );
buf.append( encodedURI ).append( '/' );
}
buf.append( location );
} catch ( IOException e ) {
IllegalArgumentException iae = new IllegalArgumentException( location );
iae.initCause( e );
throw iae;
}
return buf.toString();
} else {
return location;
}
}
/**
* Determine if the character is allowed in the scheme of a URI.
* See RFC 2396, Section 3.1
*/
public static boolean isSchemeChar( char c ) {
return Character.isLetterOrDigit( c ) ||
c == '+' || c == '-' || c == '.';
}
/**
* Determine if a URI string has a <code>scheme</code> component.
*/
private boolean hasScheme( String uri ) {
int len = uri.length();
for ( int i = 0; i < len; i++ ) {
char c = uri.charAt( i );
if ( c == ':' ) {
return i > 0;
} else if ( !isSchemeChar( c ) ) {
return false;
}
}
return false;
}
/**
* Return the specified URL with the specified session identifier
* suitably encoded.
*
* @param url URL to be encoded with the session id
* @param sessionId Session id to be included in the encoded URL
*/
protected String toEncoded( String url, String sessionId ) {
if ( ( url == null ) || ( sessionId == null ) )
return ( url );
String path = url;
String query = "";
String anchor = "";
int question = url.indexOf( '?' );
if ( question >= 0 ) {
path = url.substring( 0, question );
query = url.substring( question );
}
int pound = path.indexOf( '#' );
if ( pound >= 0 ) {
anchor = path.substring( pound );
path = path.substring( 0, pound );
}
StringBuffer sb = new StringBuffer( path );
if ( sb.length() > 0 ) { // jsessionid can't be first.
sb.append( ";" );
sb.append( DEFAULT_SESSION_ID_PARAMETER_NAME );
sb.append( "=" );
sb.append( sessionId );
}
sb.append( anchor );
sb.append( query );
return ( sb.toString() );
}
}