package hirondelle.web4j.security;

import java.io.*;
import java.util.logging.*;
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import hirondelle.web4j.util.Util;

/**
 Suppress the creation of unwanted sessions.
 
 <P><b>Using this filter means that browsers must have cookies enabled.</b>
 Some users believe that disabling cookies protects them. For web applications,  
 this seems unadvisable since its replacement -- URL rewriting -- has a 
 much higher security risk. URL rewriting is <em>dangerous</em> since it is a vector for 
 session hijacking and session fixation.
 
 <P>This class can be used only when form-based login is used. 
 When form-based login is used, the generation of the initial <tt>JSESSIONID</tt> 
 cookie is done only once per session, by the container. This filter helps you enforce the 
 policy that form-based login should be the only time a session cookie 
 is generated. 
 
 <P>Superfluous sessions and session ids represent a security risk.
 Here, the following approach is taken:
 <ul>
 <li>during form-based login, the <em>container</em> sends a session cookie to the browser 
 <li>at no other time does the <em>web application</em> itself send a <tt>JSESSIONID</tt>, either in a 
 cookie or in a rewritten URL
 <li>upon logoff, the <em>web application</em> instructs the browser to delete the <tt>JSESSIONID</tt> cookie
 </ul>
 
 <P>Note how the container and the web application work together to manage the <tt>JSESSIONID</tt> cookie.
 
 <P>It's unfortunate that the Servlet API and Java Server Pages make it 
 a bit too easy to create new sessions. To circumvent that, this filter uses  
 custom wrappers for the underlying HTTP request and response. 
 These wrappers alter the implementations of the following methods related to creating sessions :
 <ul>
 <li><tt>{@link javax.servlet.http.HttpServletRequest#getSession()}</tt>
 <li><tt>{@link javax.servlet.http.HttpServletRequest#getSession(boolean)}</tt>
 <li><tt>{@link javax.servlet.http.HttpServletResponse#encodeRedirectURL(java.lang.String)}</tt>
 <li><tt>{@link javax.servlet.http.HttpServletResponse#encodeRedirectURL(java.lang.String)}</tt>
 <li><tt>{@link javax.servlet.http.HttpServletResponse#encodeRedirectUrl(java.lang.String)}</tt>
 <li><tt>{@link javax.servlet.http.HttpServletResponse#encodeURL(java.lang.String)}</tt>
 <li><tt>{@link javax.servlet.http.HttpServletResponse#encodeUrl(java.lang.String)}</tt>
 </ul>
 
 <b>Calls to the <tt>getSession</tt> methods are in effect all coerced to <tt>getSession(false)</tt>.</b> 
 <b>Since this doesn't affect the form-based login mechanism, the user will 
 still receive a <tt>JSESSIONID</tt> cookie during form-based login</b>. This policy ensures that your code 
 cannot mistakenly create a superfluous session. 
 
 <P>The <tt>encodeXXX</tt> methods are no-operations, and simply return the given String unchanged.
 This policy in effect <b>disables URL rewriting</b>. URL rewriting is a security risk since it allows 
 session ids to appear in simple links, which are subject to session hijacking. 
 
 <P>As a convenience, this class will also detect sessions that do not have a user login, 
 and will log such occurrences as a warning.
*/
public class SuppressUnwantedSessions implements Filter {
  
   public void doFilter(ServletRequest aRequest, ServletResponse aResponse, FilterChain aChain) throws IOException, ServletException {
     fLogger.fine("START SuppressUnwantedSessions Filter.");
     
     HttpServletResponse response = (HttpServletResponse) aResponse;
     HttpServletRequest request = (HttpServletRequest) aRequest;
     
     DisableSessionCreation requestWrapper = new DisableSessionCreation(request);
     DisableUrlRewriting responseWrapper = new DisableUrlRewriting(response);
     
     aChain.doFilter(requestWrapper,  responseWrapper);
     
     checkForSessionWithNoLogin((HttpServletRequest)aRequest);
     
     fLogger.fine("END SuppressUnwantedSessions Filter.");
   }
   
   /** This implementation does nothing.   */
   public void init(FilterConfig aFilterConfig)  {
     fLogger.config("INIT : " + this.getClass().getName());
   }
   
   /** This implementation does nothing.  */
   public void destroy() {
     fLogger.config("DESTROY : " + this.getClass().getName());
   }
   
   // PRIVATE //
   private static final Logger fLogger = Util.getLogger(SuppressUnwantedSessions.class);
   private static final boolean DO_NOT_CREATE = false;
    
   private void checkForSessionWithNoLogin(HttpServletRequest aRequest) {
     HttpSession session = aRequest.getSession(DO_NOT_CREATE);
     if ( sessionAlreadyExists(session) ){
       if( ! userHasLoggedIn(aRequest)){
         fLogger.warning("Session exists, but user has not yet logged in.");
       }
     }
   }
   
   private boolean sessionAlreadyExists(HttpSession aSession){
     return aSession != null;
   }
   
   private boolean userHasLoggedIn(HttpServletRequest aRequest){
     return aRequest.getUserPrincipal() != null;
   }
   
  /** Alter the implementation of <tt>getSession()</tt> methods.  */
  private static final class DisableSessionCreation extends HttpServletRequestWrapper {
     DisableSessionCreation(HttpServletRequest aRequest){
       super(aRequest);
     }
     @Override public HttpSession getSession() {
       // FYI - Tomcat 5.5 JSPs call getSession(). 
       fLog.fine("Calling getSession(). Method not supported by this wrapper. Coercing to getSession(false).");
       return getSession(false);
     }
     @Override public HttpSession getSession(boolean aCreateIfAbsent) {
       if( aCreateIfAbsent ){
         fLog.warning("Calling getSession(true). Method call not supported by this wrapper. Coercing to getSession(false).");
       }
       else {
         //fLogger.finest("Calling getSession(false).");
       }
       return super.getSession(NEVER_CREATE);
     }
     private static final Logger fLog = Util.getLogger(DisableSessionCreation.class);
     private static final boolean NEVER_CREATE = false;
   }
  
  /** Disable URL rewriting entirely.  */
  private static final class DisableUrlRewriting extends HttpServletResponseWrapper {
    DisableUrlRewriting(HttpServletResponse aResponse){ 
      super(aResponse);
    }
    @Override  public String encodeUrl(String aURL) {
      return aURL;
    }
    @Override public String encodeURL(String aURL) {
      return aURL;
    }
    @Override public String encodeRedirectURL(String aURL) {
      return aURL;
    }
    @Override public String encodeRedirectUrl(String aURL) {
      return aURL;
    }
  }
}
