/FileExtraction/Java_unseen/dlitz_resin/modules/resin/src/com/caucho/security/AbstractLogin.java
/*
 * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.security;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.security.Principal;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.caucho.server.session.SessionImpl;

/**
 * Used to authenticate users in a servlet request.  AbstractLogin handles
 * the different login types like "basic" or "form".  Normally, a Login
 * will delegate the actual authentication to a ServletAuthenticator.
 *
 * <p>The Login is primarily responsible for extracting the credentials
 * from the request (typically username and password) and passing those
 * to the ServletAuthenticator.
 *
 * <p>The Servlet API calls the Login in two contexts: directly from
 * <code>ServletRequest.getUserPrincipal()</code>, and during
 * security checking.   When called from the Servlet API, the login class
 * can't change the response.  In other words, if an application
 * calls getUserPrincipal(), the Login class can't return a forbidden
 * error page.  When the servlet engine calls authenticate(), the login class
 * can return an error page (or forward internally.)
 *
 * <p>Normally, Login implementations will defer the actual authentication
 * to a ServletAuthenticator class.  That way, both "basic" and "form" login
 * can use the same DatabaseAuthenticator.  Some applications, like SSL
 * client certificate login, may want to combine the Login and authentication
 * into one class.
 *
 * <p>Login instances are configured through bean introspection.  Adding
 * a public <code>setFoo(String foo)</code> method will be configured with
 * the following login-config:
 *
 * <code><pre>
 * &lt;myfoo:CustomLogin xmlns:myfoo="urn:java:com.foo.myfoo">
 *   &lt;foo>bar&lt;/foo>
 * &lt;/myfoo:CustomLogin>
 * </pre></code>
 *
 * @since Resin 4.0.0
 */
public abstract class AbstractLogin implements Login {
  protected final static Logger log
    = Logger.getLogger(AbstractLogin.class.getName());

  /**
   * The configured authenticator for the login.  Implementing classes will
   * typically delegate calls to the authenticator after extracting the
   * username and password.
   */
  protected Authenticator _auth;
  protected SingleSignon _singleSignon;

  private @Inject Instance<Authenticator> _authInstance;
  private @Inject Instance<SingleSignon> _signonInstance;

  private boolean _isSessionSaveLogin = true;
  private boolean _isLogoutOnTimeout = true;

  protected AbstractLogin()
  {
  }

  /**
   * Sets the authenticator.
   */
  public void setAuthenticator(Authenticator auth)
  {
    _auth = auth;
  }

  /**
   * Gets the authenticator.
   */
  @Override
  public Authenticator getAuthenticator()
  {
    if (_auth == null) {
      if (! _authInstance.isUnsatisfied()) {
        _auth = _authInstance.get();
      }

      if (_auth == null) {
        _auth = new NullAuthenticator();
      }

      if (log.isLoggable(Level.FINE))
        log.fine(toString() + " using " + _auth);
    }

    return _auth;
  }

  protected SingleSignon getSingleSignon()
  {
    if (_singleSignon == null) {
      Authenticator auth = getAuthenticator();

      if (_auth instanceof AbstractAuthenticator) {
        AbstractAuthenticator abstractAuth
          = (AbstractAuthenticator) auth;

        _singleSignon = abstractAuth.getSingleSignon();
      }
      
      // server/1al4
      /*
      if (_singleSignon == null) {
        try {
          _singleSignon = new ClusterSingleSignon("login");
        } catch (Exception e) {
          log.log(Level.FINE, e.toString(), e);
        }
      }
      */
    }

    return _singleSignon;
  }

  /**
   * Returns true if the user should be logged out on a session timeout.
   */
  public boolean isLogoutOnSessionTimeout()
  {
    return _isLogoutOnTimeout;
  }

  /**
   * Sets true if the principal should logout when the session times out.
   */
  public void setLogoutOnSessionTimeout(boolean logout)
  {
    _isLogoutOnTimeout = logout;
  }

  /**
   * Sets true if the user should be saved in the session.
   */
  public void setSessionSaveLogin(boolean isSave)
  {
    _isSessionSaveLogin = isSave;
  }

  /**
   * Sets true if the user should be saved in the session.
   */
  public boolean isSessionSaveLogin()
  {
    return _isSessionSaveLogin;
  }

  /**
   * Initialize the login.  <code>init()</code> will be called after all
   * the bean parameters have been set.
   */
  @PostConstruct
  public void init()
    throws ServletException
  {
    // server/12cc - XXX: this should be allowed, though

    // XXX: order
    if (_singleSignon == null && ! _signonInstance.isUnsatisfied()) {
      _singleSignon = _signonInstance.get();
    }
  }

  /**
   * Returns the authentication type.  <code>getAuthType</code> is called
   * by <code>HttpServletRequest.getAuthType</code>.
   */
  @Override
  public String getAuthType()
  {
    return "none";
  }

  /**
   * Returns true if the login can be used for this request. This lets
   * webapps use multiple login methods.
   */
  @Override
  public boolean isLoginUsedForRequest(HttpServletRequest request)
  {
    return true;
  }

  /**
   * Returns the Principal associated with the current request.
   * getUserPrincipal is called in response to the Request.getUserPrincipal
   * call.  Login.getUserPrincipal can't modify the response or return
   * an error page.
   *
   * <p/>authenticate is used for the security checks.
   *
   * @param request servlet request
   *
   * @return the logged in principal on success, null on failure.
   */
  @Override
  public Principal getUserPrincipal(HttpServletRequest request)
  {
    Principal user = (Principal) request.getAttribute(LOGIN_USER_NAME);

    if (user != null)
      return user;

    Principal savedUser = findSavedUser(request);

    // server/12c9 - new login overrides old
    if (savedUser != null && isSavedUserValid(request, savedUser)) {
      request.setAttribute(LOGIN_USER_NAME, savedUser);

      return savedUser;
    }

    // server/12d2
    user = getUserPrincipalImpl(request);

    if (user != null || savedUser != null) {
      saveUser(request, user);
    }

    return user;
  }

  /**
   * Logs a user in.  The authenticate method is called during the
   * security check.  If the user does not exist, <code>authenticate</code>
   * sets the reponse error page and returns null.
   *
   * @param request servlet request
   * @param response servlet response for a failed authentication.
   * @param isFail if true send a challenge (Form|HTTP Basic,etc.)
   *
   * @return the logged in principal on success, null on failure.
   */
  @Override
  public Principal login(HttpServletRequest request,
                         HttpServletResponse response,
                         boolean isFail)
  {
    Principal user = (Principal) request.getAttribute(LOGIN_USER_PRINCIPAL);
    
    if (user != null)
      return user;
    
    Principal savedUser = findSavedUser(request);

    // server/12c9 - new login overrides old
    if (savedUser != null && isSavedUserValid(request, savedUser)) {
      request.setAttribute(LOGIN_USER_PRINCIPAL, savedUser);
      
      return savedUser;
    }

    user = login(request, response);
    
    if (user != null)
      request.setAttribute(LOGIN_USER_PRINCIPAL, user);

    try {
      if (user != null || savedUser != null) {
        // server/12h7
        saveUser(request, user);
      }

      if (user != null) {
        loginSuccessResponse(user, request, response);

        return user;
      }

      if (isFail) {
        log.fine(this + " sending login challenge");
        
        loginChallenge(request, response);
      }
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      // server/12d5
      throw new LoginException(e);
    }

    return null;
  }
  
  /**
   * Attempts to login the user if the user cannot be found in the 
   * session or the single-signon. 
   */
  protected Principal login(HttpServletRequest request,
                            HttpServletResponse response)
  {
    // Most login classes will extract the user and password (or some other
    // credentials) from the request and call auth.login.

    return getLoginPrincipalImpl(request);
  }
  
  /**
   * Looks up the user based on session or single signon.
   */
  protected Principal findSavedUser(HttpServletRequest request)
  {
    SingleSignon singleSignon = getSingleSignon();

    SessionImpl session = (SessionImpl) request.getSession(false);

    String sessionId;

    if (session != null)
      sessionId = session.getId();
    else
      sessionId = request.getRequestedSessionId();

    if (sessionId == null)
      return null;
    else if (singleSignon != null) {
      Principal user = singleSignon.get(sessionId);
      
      if (user != null && log.isLoggable(Level.FINER))
        log.finer(this + " load user '" + user + "' from " + singleSignon);
      
      return user;
    }
    else if (isSessionSaveLogin() && session != null) {
      Principal user = (Principal) session.getAttribute(LOGIN_USER_PRINCIPAL);
      
      if (user != null && log.isLoggable(Level.FINER))
        log.finer(this + " load user '" + user + "' from session");
      
      return user;
    }
    else
      return null;
  }

  /**
   * Saves the user based on session or single signon.
   */
  protected void saveUser(HttpServletRequest request,
                          Principal user)
  {
    SingleSignon singleSignon = getSingleSignon();

    SessionImpl session;

    if (isSessionSaveLogin())
      session = (SessionImpl) request.getSession(true);
    else
      session = (SessionImpl) request.getSession(false);

    String sessionId;

    if (session != null)
      sessionId = session.getId();
    else
      sessionId = request.getRequestedSessionId();

    if (sessionId == null) {
    }
    else if (singleSignon != null) {
      singleSignon.put(sessionId, user);
      
      if (log.isLoggable(Level.FINER))
        log.finer(this + " save user '" + user +"' in single signon " + singleSignon);
    }
    else if (isSessionSaveLogin()) {
      session.setAttribute(LOGIN_USER_PRINCIPAL, user);
      
      if (log.isLoggable(Level.FINER))
        log.finer(this + " save user '" + user +"' in session " + singleSignon);
    }
  }

  @Override
  public boolean isPasswordBased()
  {
    return false;
  }

  /**
   * Gets the user from a persistent cookie, using authenticateCookie
   * to actually look the cookie up.
   */
  protected Principal getUserPrincipalImpl(HttpServletRequest request)
  {
    return null;
  }

  /**
   * Returns the non-authenticated principal for the user request
   */
  protected boolean isSavedUserValid(HttpServletRequest request,
                                     Principal savedUser)
  {
    return true;
  }

  /**
   * Gets the user from a persistent cookie, using authenticateCookie
   * to actually look the cookie up.
   */
  protected Principal getLoginPrincipalImpl(HttpServletRequest request)
  {
    return getUserPrincipalImpl(request);
  }

  /**
   * Implementation of the login challenge
   */
  protected void loginChallenge(HttpServletRequest request,
                                HttpServletResponse response)
    throws ServletException, IOException
  {
  }

  /**
   * HTTP updates after a successful login
   */
  protected void loginSuccessResponse(Principal user,
                                      HttpServletRequest request,
                                      HttpServletResponse response)
    throws ServletException, IOException
  {
  }

  /**
   * Returns true if the current user plays the named role.
   * <code>isUserInRole</code> is called in response to the
   * <code>HttpServletRequest.isUserInRole</code> call.
   *
   * @param user UserPrincipal object associated with request
   * @param role to be tested
   *
   * @return the logged in principal on success, null on failure.
   */
  @Override
  public boolean isUserInRole(Principal user, String role)
  {
    return getAuthenticator().isUserInRole(user, role);
  }

  /**
   * Logs the user out from the given request.
   *
   * <p>Since there is no servlet API for logout, this must be called
   * directly from user code.  Resin stores the web-app's login object
   * in the ServletContext attribute "caucho.login".
   */
  @Override
  public void logout(Principal user,
                     HttpServletRequest request,
                     HttpServletResponse response)
  {
    String sessionId = request.getRequestedSessionId();

    logoutImpl(user, request, response);
    
    HttpSession session = request.getSession(false);
    
    if (session != null)
      session.removeAttribute(LOGIN_USER_PRINCIPAL);

    SingleSignon singleSignon = getSingleSignon();

    if (singleSignon != null)
      singleSignon.remove(sessionId);
  }

  /**
   * Called when the session invalidates.
   */
  @Override
  public void sessionInvalidate(HttpSession session,
                                boolean isTimeout)
  {
    //LoginPrincipal login = (LoginPrincipal) session.getAttribute(LOGIN_NAME);

    if (session != null) {
      SingleSignon singleSignon = getSingleSignon();

      // server/12cg
      if (singleSignon != null
          && (! isTimeout || isLogoutOnSessionTimeout())) {
        singleSignon.remove(session.getId());
      }
    }
  }

  /**
   * Logs the user out from the given request.
   *
   * <p>Since there is no servlet API for logout, this must be called
   * directly from user code.  Resin stores the web-app's login object
   * in the ServletContext attribute "caucho.login".
   */
  protected void logoutImpl(Principal user,
                            HttpServletRequest request,
                            HttpServletResponse response)
  {
  }

  /**
   * Logs the user out from the session.
   *
   * @param user the logged in user
   */
  /*
  public void logout(Principal user)
  {
    if (log.isLoggable(Level.FINE))
      log.fine(this + " logout " + user);

    if (sessionId != null) {
      if (_principalCache == null) {
      }
      else if (timeoutSession != null) {
        PrincipalEntry entry =  _principalCache.get(sessionId);

        if (entry != null && entry.logout(timeoutSession)) {
          _principalCache.remove(sessionId);
        }
      }
      else {
        PrincipalEntry entry =  _principalCache.remove(sessionId);

        if (entry != null)
          entry.logout();
      }

      Application app = (Application) application;
      SessionManager manager = app.getSessionManager();

      if (manager != null) {
        try {
          SessionImpl session = manager.getSession(sessionId,
                                                   Alarm.getCurrentTime(),
                                                   false, true);

          if (session != null) {
            session.finish();
            session.logout();
          }
        } catch (Exception e) {
          log.log(Level.FINE, e.toString(), e);
        }
      }
    }
  }
  */

  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "[]";
  }

  static class PrincipalEntry {
    private Principal _principal;
    private ArrayList<SoftReference<SessionImpl>> _sessions;

    PrincipalEntry(Principal principal)
    {
      _principal = principal;
    }

    Principal getPrincipal()
    {
      return _principal;
    }

    void addSession(SessionImpl session)
    {
      if (_sessions == null)
        _sessions = new ArrayList<SoftReference<SessionImpl>>();

      _sessions.add(new SoftReference<SessionImpl>(session));
    }

    /**
     * Logout only the given session, returning true if it's the
     * last session to logout.
     */
    boolean logout(HttpSession timeoutSession)
    {
      ArrayList<SoftReference<SessionImpl>> sessions = _sessions;

      if (sessions == null)
        return true;

      boolean isEmpty = true;
      for (int i = sessions.size() - 1; i >= 0; i--) {
        SoftReference<SessionImpl> ref = sessions.get(i);
        SessionImpl session = ref.get();

        try {
          if (session == timeoutSession) {
            sessions.remove(i);
            // session.logout();
            // XXX: invalidate?
          }
          else if (session == null)
            sessions.remove(i);
          else
            isEmpty = false;
        } catch (Exception e) {
          log.log(Level.WARNING, e.toString(), e);
        }
      }

      return isEmpty;
    }

    void logout()
    {
      ArrayList<SoftReference<SessionImpl>> sessions = _sessions;
      _sessions = null;

      for (int i = 0; sessions != null && i < sessions.size(); i++) {
        SoftReference<SessionImpl> ref = sessions.get(i);
        SessionImpl session = ref.get();

        try {
          if (session != null) {
            // session.logout();
            session.invalidateLogout();  // #599,  server/12i3
          }
        } catch (Exception e) {
          log.log(Level.WARNING, e.toString(), e);
        }
      }
    }
  }

  @SuppressWarnings("serial")
  static class LoginPrincipal implements java.io.Serializable {
    // server/12ci - XXX: this was transient before.
    private Principal _user;

    LoginPrincipal(Principal user)
    {
      _user = user;
    }

    public Principal getUser()
    {
      return _user;
    }

    public String toString()
    {
      return getClass().getSimpleName() + "[" + _user + "]";
    }
  }
}
