/*
 *  Copyright (c) 2000-2003 Yale University. All rights reserved.
 *
 *  THIS SOFTWARE IS PROVIDED "AS IS," AND ANY EXPRESS OR IMPLIED
 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE EXPRESSLY
 *  DISCLAIMED. IN NO EVENT SHALL YALE UNIVERSITY OR ITS EMPLOYEES BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED, THE COSTS OF
 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED IN ADVANCE OF THE POSSIBILITY OF SUCH
 *  DAMAGE.
 *
 *  Redistribution and use of this software in source or binary forms,
 *  with or without modification, are permitted, provided that the
 *  following conditions are met:
 *
 *  1. Any redistribution must include the above copyright notice and
 *  disclaimer and this list of conditions in any related documentation
 *  and, if feasible, in the redistributed software.
 *
 *  2. Any redistribution must include the acknowledgment, "This product
 *  includes software developed by Yale University," in any related
 *  documentation and, if feasible, in the redistributed software.
 *
 *  3. The names "Yale" and "Yale University" must not be used to endorse
 *  or promote products derived from this software.
 */

package org.esupportail.filter.CASFilter;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.StringTokenizer;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.SAXException;

import edu.yale.its.tp.cas.client.ProxyTicketValidator;
import edu.yale.its.tp.cas.client.Util;

/**
 * <p>Protects web-accessible resources with CAS.</p>
 *
 * <p>The following filter initialization parameters are declared in 
 * <code>web.xml</code>:</p>
 *
 * <ul> 
 *   <li><code>edu.yale.its.tp.cas.client.filter.loginUrl</code>: URL to 
 *   login page on CAS server.  (Required)</li>
 *   <li><code>edu.yale.its.tp.cas.client.filter.validateUrl</code>: URL
 *   to validation URL on CAS server.  (Required)</li>
 *   <li><code>edu.yale.its.tp.cas.client.filter.serviceUrl</code>: URL
 *   of this service.  (Required if <code>serverName</code> is not 
 *   specified)</li>
 *   <li><code>edu.yale.its.tp.cas.client.filter.serverName</code>: full
 *   hostname with port number (e.g. <code>www.foo.com:8080</code>).  
 *   Port number isn't required if it is standard (80 for HTTP, 443 for 
 *   HTTPS).  (Required if <code>serviceUrl</code> is not specified)</li>
 *   <li><code>edu.yale.its.tp.cas.client.filter.authorizedProxy</code>:
 *   whitespace-delimited list of valid proxies through which authentication
 *   may have proceeded.  One one proxy must match.  (Optional.  If nothing
 *   is specified, the filter will only accept service tickets &#150; not
 *   proxy tickets.)</li>
 *   <li><code>edu.yale.its.tp.cas.client.filter.renew</code>: value of
 *   CAS "renew" parameter.  Bypasses single sign-on and requires user
 *   to provide CAS with his/her credentials again.  (Optional.  If nothing
 *   is specified, this defaults to false.)</li>
 *   <li><code>edu.yale.its.tp.cas.client.filter.wrapRequest</code>:
 *   wrap the <code>HttpServletRequest</code> object, overriding the
 *   <code>getRemoteUser()</code> method.  When set to "true",
 *   <code>request.getRemoteUser()</code> will return the username of the 
 *   currently logged-in CAS user.  (Optional.  If nothing is specified, 
 *   this defaults to false.)</li>
 * </ul>
 *
 * <p>The logged-in username is set in the session attribute defined by
 * the value of <code>CAS_FILTER_USER</code> and may be accessed from within
 * your application either by setting <code>wrapRequest</code> and calling
 * <code>request.getRemoteUser()</code>, or by calling
 * <code>session.getAttribute(CASFilter.CAS_FILTER_USER)</code>.</p>
 *
 * @author Shawn Bayern
 */

/**
 * @version 1.2
 * Last modification date : 04/10/2005
 * Modifications :
 * 30.09.2004 - Thomas Bellembois - adding the Principal wrapper
 * 04/10/2005 - Thomas Bellembois - adding the cascading functionnality
 */
public class CASFilter implements Filter {

	
	private String filterName;
	
    /** Session attribute in which the username is stored */
    public final static String CAS_FILTER_USER =
        "edu.yale.its.tp.cas.client.filter.user";
    public final static String FILTER_USED_FOR_THIS_SESSION = "edu.yale.its.tp.cas.client.filter.filterUsedForThisSession";	
    
    //*********************************************************************
    // Configuration state
    private static final String SELECTED_FILTER = "org.esupportail.filter.authenticationRouter.selectedFilter";
    private String casLogin,
        casValidate,
        casAuthorizedProxy,
        casServiceUrl,
        casRenew,
        casServerName;
    private boolean wrapRequest;
   
	// logger
	private static final Log log = LogFactory.getLog(CASFilter.class);
       
    
    //*********************************************************************
    // Initialization 

    public void init(FilterConfig config) throws ServletException {
        casLogin =
            config.getInitParameter(
                "edu.yale.its.tp.cas.client.filter.loginUrl");
        casValidate =
            config.getInitParameter(
                "edu.yale.its.tp.cas.client.filter.validateUrl");
        casServiceUrl =
            config.getInitParameter(
                "edu.yale.its.tp.cas.client.filter.serviceUrl");
        casAuthorizedProxy =
            config.getInitParameter(
                "edu.yale.its.tp.cas.client.filter.authorizedProxy");
        casRenew =
            config.getInitParameter("edu.yale.its.tp.cas.client.filter.renew");
        casServerName =
            config.getInitParameter(
                "edu.yale.its.tp.cas.client.filter.serverName");
        wrapRequest = Boolean.valueOf(config.getInitParameter(
				"edu.yale.its.tp.cas.client.filter.wrapRequest")).booleanValue();
        filterName = config.getFilterName().toUpperCase();
    }// init

    //*********************************************************************
    // Filter processing

    public void doFilter(
        ServletRequest request,
        ServletResponse response,
        FilterChain fc)
        throws ServletException, IOException {

    	
    	log.info("Entering CASFilter "+filterName);
    	
    	
        // make sure we've got an HTTP request
        if (!(request instanceof HttpServletRequest)
            || !(response instanceof HttpServletResponse))
            throw new ServletException(getClass().getName()+".doFilter() - CASFilter protects only HTTP resources");
		

        ////////////////////////////////////////////////////////////////////////////////////////////////////////
		// checking that the selectedFilter session variable is set to CAS (by the AuthenticationRouter filter)
		//
		HttpSession session = ((HttpServletRequest) request).getSession();
//		if (session == null || session.getAttribute(SELECTED_FILTER) == null || !session.getAttribute(SELECTED_FILTER).equals("CAS")){
//			log(LOG_DEBUG, "No session or session attribut SELECTED_FILTER not set to CAS");
//			log.info("Leaving CASFilter...");
//			fc.doFilter(request, response);
//			return;
//		}// if
//		else{
//			log(LOG_DEBUG, "Session attribut SELECTED_FILTER set to CAS");
//		}// else
		// Cascading modifications
		if (session == null || session.getAttribute(SELECTED_FILTER)==null){
			log.debug("No session attribute");
			log.info("Leaving CASFilter "+filterName);
			fc.doFilter(request, response);
			return;
		}// if (session == null || session.getAttribute(SELECTED_FILTER)==null)
		else if (session.getAttribute(SELECTED_FILTER)!=null) {
			String sessionAttributes = (String)session.getAttribute(SELECTED_FILTER);
			log.debug("sessionAttributes="+sessionAttributes);
			if (sessionAttributes.indexOf(filterName) == -1) {
				log.debug("Session attribute SELECTED_FILTER not set to "+filterName);
				log.info("Leaving CASFilter "+filterName);
				fc.doFilter(request, response);
				return;
			}// if (sessionAttributes.indexOf("CAS") == -1)
			else{
			log.debug("Session attribute SELECTED_FILTER set to "+filterName);
			}// else
		}// else if (session.getAttribute(SELECTED_FILTER)!=null)
		// !Cascading modifications
		
		// Wrapping the request if desired
	    if(wrapRequest) {
		    request = new CASFilterRequestWrapper((HttpServletRequest)request);
		}// if
        
	    
        // if our attribute's already present, don't do anything
//        if (session != null && session.getAttribute(CAS_FILTER_USER) != null) {
//        	log.debug("Session in progress");
//			log.info("Leaving CASFilter "+filterName);
//            fc.doFilter(request, response);
//            return;
//        }
        if(session.getAttribute(FILTER_USED_FOR_THIS_SESSION)!=null && session.getAttribute(FILTER_USED_FOR_THIS_SESSION).equals(filterName)){
			log.debug("Session in progress");
			log.info("Leaving CASFilter "+filterName);
			fc.doFilter(request, response);
			return;
		}//if
        
        // otherwise, we need to authenticate via CAS
        String ticket = request.getParameter("ticket");

        // no ticket?  abort request processing and redirect
        if (ticket == null || ticket.equals("")) {
            if (casLogin == null) {
                throw new ServletException(
                    "When CASFilter protects pages that do not receive a 'ticket' "
                        + "parameter, it needs a edu.yale.its.tp.cas.client.filter.loginUrl "
                        + "filter parameter");
            }
            ((HttpServletResponse)response).sendRedirect(
                casLogin
                    + "?service="
                    + getService((HttpServletRequest)request)
                    + ((casRenew != null && !casRenew.equals(""))
                        ? "&renew=" + casRenew
                        : ""));

            // abort chain
            return;
        }

        // Yay, ticket!  Validate it.
        String user = getAuthenticatedUser((HttpServletRequest)request);
        if (user == null)
            throw new ServletException("Unexpected CAS authentication error");

        // Store the authenticated user in the session
        if (session != null) // probably unncessary
            session.setAttribute(CAS_FILTER_USER, user);
        	session.setAttribute(FILTER_USED_FOR_THIS_SESSION, filterName);
        // continue processing the request
        log.info("Leaving CASFilter "+filterName);
        fc.doFilter(request, response);
        return;
    }

    //*********************************************************************
    // Destruction

    public void destroy() {
    }

    //*********************************************************************
    // Utility methods

    /**
     * Converts a ticket parameter to a username, taking into account an
     * optionally configured trusted proxy in the tier immediately in front
     * of us.
     */
    private String getAuthenticatedUser(HttpServletRequest request)
        throws ServletException {
        ProxyTicketValidator pv = null;
        try {
            pv = new ProxyTicketValidator();
            pv.setCasValidateUrl(casValidate);
            pv.setServiceTicket(request.getParameter("ticket"));
            pv.setService(getService(request));
	    pv.setRenew(Boolean.valueOf(casRenew).booleanValue());
            pv.validate();
            if (!pv.isAuthenticationSuccesful())
                throw new ServletException(
                    "CAS authentication error: " + pv.getErrorCode() + ": " + pv.getErrorMessage());
            if (pv.getProxyList().size() != 0) {
                // ticket was proxied
                if (casAuthorizedProxy == null) {
                    throw new ServletException("this page does not accept proxied tickets");
                } else {
                    boolean authorized = false;
                    String proxy = (String)pv.getProxyList().get(0);
                    StringTokenizer casProxies =
                        new StringTokenizer(casAuthorizedProxy);
                    while (casProxies.hasMoreTokens()) {
                        if (proxy.equals(casProxies.nextToken())) {
                            authorized = true;
                            break;
                        }
                    }
                    if (!authorized) {
                        throw new ServletException(
                            "unauthorized top-level proxy: '"
                                + pv.getProxyList().get(0)
                                + "'");
                    }
                }
            }
            return pv.getUser();
        } catch (SAXException ex) {
            String xmlResponse = "";
            if (pv != null)
                xmlResponse = pv.getResponse();
            throw new ServletException(ex + " " + xmlResponse);
        } catch (ParserConfigurationException ex) {
            throw new ServletException(ex);
        } catch (IOException ex) {
            throw new ServletException(ex);
        }
    }

    /**
     * Returns either the configured service or figures it out for the current
     * request.  The returned service is URL-encoded.
     */
    private String getService(HttpServletRequest request)
        throws ServletException {
        // ensure we have a server name or service name
        if (casServerName == null && casServiceUrl == null)
            throw new ServletException(
                "need one of the following configuration "
                    + "parameters: edu.yale.its.tp.cas.client.filter.serviceUrl or "
                    + "edu.yale.its.tp.cas.client.filter.serverName");

        // use the given string if it's provided
        if (casServiceUrl != null)
			try {
				return URLEncoder.encode(casServiceUrl, "UTF-8");
			} catch (UnsupportedEncodingException e) {
				throw new ServletException("Unsupported encoding");
			}
		else
            // otherwise, return our best guess at the service
            return Util.getService(request, casServerName);
    }
}
