/**
 * @author tbellemb
 * @version 1.3
 * creation date : 2004 09 30
 * last modification date : 2006 01 06
 * modifications :
 * 2005 02 01 - Thomas Bellembois - modifying the scope parameters
 * 2005 10 04 - Thomas Bellembois - adding the cascading functionnality, adding the authentication router dependency configuration
 * 2006 01 06 - Thomas Bellembois - modifying the scope names
 */
package org.esupportail.filter.LDAPFilter;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.Hashtable;

import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
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 org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class LDAPFilter implements Filter{
	
	private String filterName;
	private boolean useAuthenticationRouter;
	
	private String connectionUrl;
	private String alternateUrl;
	private String bindType;
	private String fastBindUserPattern;
	private String searchBindConnectionName;
	private String searchBindConnectionPassword;
	private String searchBindUserBase;
	private String searchBindUserSubtree;
	private String searchBindUserSearch;
	
	// Const. for LDAP
	public static final String SCOPE_SUBTREE_LEVEL = "SUBTREE_SCOPE";
	public static final String SCOPE_ONE_LEVEL = "ONELEVEL_SCOPE";
	public static final String SCOPE_OBJECT_LEVEL ="OBJECT_SCOPE";
	
	private static final String SELECTED_FILTER = "org.esupportail.filter.authenticationRouter.selectedFilter";
	private static final String USE_AUTHENTICATION_ROUTER = "org.esupportail.filter.LDAPFilter.useAuthenticationRouter";
	private static final String AUTHENTICATION_TYPE = "simple";
	// LDAP directory URL's
	private static final String CONNECTION_URL = "org.esupportail.filter.LDAPFilter.connectionURL";
	private static final String ALTERNATE_URL = "org.esupportail.filter.LDAPFilter.alternateURL";
	// Bind type = FASTBIND | SEARCHBIND
	private static final String BIND_TYPE = "org.esupportail.filter.LDAPFilter.bindType";
	// ex : uid={0},ou=people,dc=univ,dc=fr
	private static final String FASTBIND_USERPATTERN = "org.esupportail.filter.LDAPFilter.fastBindUserPattern";
	// if the searchBind need and authentication
	private static final String SEARCHBIND_CONNECTIONNAME = "org.esupportail.filter.LDAPFilter.searchBindConnectionName";
	private static final String SEARCHBIND_CONNECTIONPASSWORD = "org.esupportail.filter.LDAPFilter.searchBindConnectionPassword";
	// BaseDN for the LDAP search
	private static final String SEARCHBIND_USERBASE = "org.esupportail.filter.LDAPFilter.searchBindBaseDN";
	// Scope for the LDAP search -  SCOPE_SUBTREE_LEVEL | SCOPE_ONE_LEVEL | SCOPE_OBJECT_LEVEL
	private static final String SEARCHBIND_USERSUBTREE = "org.esupportail.filter.LDAPFilter.searchBindScope";
	// String to create an LDAP RDN thanks to the connection name
	// ex : uid={0}
	private static final String SEARCHBIND_USERSEARCH = "org.esupportail.filter.LDAPFilter.searchBindFilter";
	
	public static final String LDAP_FILTER_USER= "org.esupportail.filter.LDAPFilter.user";
	public final static String FILTER_USED_FOR_THIS_SESSION = "org.esupportail.filter.LDAPFilter.filterUsedForThisSession";	
	
	// Const. for auth
	private final String AUTH_TYPE = "Basic";
	private final String AUTH_REALM = "ESUP WebDAV Server";
	
	private final String MICROSOFT_MINIREDIR = "microsoft-webdav-miniredir";
	
	// logger
	private static final Log log = LogFactory.getLog(LDAPFilter.class);
	
	
	public void init(FilterConfig config) throws ServletException {
		filterName = config.getFilterName().toUpperCase();
		connectionUrl = config.getInitParameter(CONNECTION_URL);
		alternateUrl = config.getInitParameter(ALTERNATE_URL);
		bindType = config.getInitParameter(BIND_TYPE);
		fastBindUserPattern = config.getInitParameter(FASTBIND_USERPATTERN);
		searchBindConnectionName = config.getInitParameter(SEARCHBIND_CONNECTIONNAME);
		searchBindConnectionPassword = config.getInitParameter(SEARCHBIND_CONNECTIONPASSWORD);
		searchBindUserBase = config.getInitParameter(SEARCHBIND_USERBASE);
		searchBindUserSubtree = config.getInitParameter(SEARCHBIND_USERSUBTREE);
		searchBindUserSearch = config.getInitParameter(SEARCHBIND_USERSEARCH);
		useAuthenticationRouter = Boolean.valueOf(config.getInitParameter(USE_AUTHENTICATION_ROUTER)).booleanValue();
	}// init
	
	
	/*
	 * Make the String lenght multiple of for FOR A BASE64 ENCODING or DECODING
	 */
	private static String makeMultipleOf4(String s){
		StringBuffer result = new StringBuffer(s);
		int stringLength = s.length();
		if(stringLength%4!=0){
			for(int i=0; i<(4-(stringLength%4)); i++){
				result.append("=");
			}// for
		}// if
		return result.toString();
	}// makeMultipleOf4
	
	
	/*
	 * Replace the string in the pattern
	 * ex : user=toto pattern=uid={0},ou=people,dc=univ,dc=fr
	 *      >return uid=toto,ou=people,dc=univ,dc=fr
	 */
	private static String replaceStringInPattern(String string, String pattern) {
		Object[] arguments = {string};
		return MessageFormat.format(pattern, arguments);
	}// replaceStringInPattern
	
	
	
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
		
		
		log.info("Entering LDAPFilter "+filterName);
		
		
		//////////////////////////////////////
		// make sure we've got an HTTP request
		//
		if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse))
			throw new ServletException(getClass().getName()+".doFilter() - LDAPFilter protects only HTTP resources");
		
		
		////////////////////////////////////////////////////////////////////////////////////////////////////////
		// checking that the selectedFilter session variable is set to LDAP (by the AuthenticationRouter filter)
		//
		HttpSession session = ((HttpServletRequest) request).getSession();
//		if (session == null || session.getAttribute(SELECTED_FILTER) == null || !session.getAttribute(SELECTED_FILTER).equals("LDAP")){
//			log(LOG_DEBUG, "No session or session attribut SELECTED_FILTER not set to LDAP");
//			log.info("Leaving LDAPFilter...");
//			filterChain.doFilter(request, response);
//			return;
//		}// if
//		else{
//			log(LOG_DEBUG, "Session attribut SELECTED_FILTER set to LDAP");
//		}// else
		// Cascading modifications
		if (useAuthenticationRouter) {
			log.debug("Use AuthenticationRouter");
			if (session == null || session.getAttribute(SELECTED_FILTER)==null){
				log.debug("No session attribute");
				log.info("Leaving LDAPFilter "+filterName);
				filterChain.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 LDAPFilter "+filterName);
					filterChain.doFilter(request, response);
					return;
				}// if (sessionAttributes.indexOf("LDAP") == -1)
				else{
					log.debug( "Session attribute SELECTED_FILTER set to "+filterName);
				}// else
			}// else if (session.getAttribute(SELECTED_FILTER)!=null)
		}// if (useAuthenticationRouter)
		else {
			log.debug( "Do not use AuthenticationRouter");
		}// else (useAuthenticationRouter)
		// !Cascading modifications
		
		
		///////////////////////
		// wrapping the request
		//
        request = new LDAPFilterRequestWrapper((HttpServletRequest)request);
		
        
        ////////////////////////////////////////////////////
		// if the user is already authenticated - do nothing
		//
//		if(session.getAttribute(LDAP_FILTER_USER)!=null){
//			log.debug( "Session in progress");
//			log.info("Leaving LDAPFilter "+filterName);
//			filterChain.doFilter(request, response);
//			return;
//		}// if
		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 LDAPFilter "+filterName);
			filterChain.doFilter(request, response);
			return;
		}//if
		
		/////////////////////////////////////
		// getting the header "authorization"
		//
		String header = ((HttpServletRequest)request).getHeader("authorization");
		
		
		/////////////////////////////////////////////////////////////////////////////////////////
		// if there is no "authorization" header then send a 401 error and ask for authentication
		//
		if(header==null || header.equals("")){
			log.debug( "No authorization header");
			log.debug( "Leaving LDAPFilter "+filterName);
			((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			((HttpServletResponse)response).setHeader("WWW-Authenticate", AUTH_TYPE+" realm=\""+AUTH_REALM+"\"");        	
			return;
		}// if
		
		
		////////////////////////////////////////////////////////////////////////////////
		// processing the header to get authentication informations (login and password)
		//
		// removing the authentication type (and spaces) from the header
		String encodedAuthorization = header.substring(AUTHENTICATION_TYPE.length()).trim();
		// decoding the authorization (base64 encoded)
		String decodedAuthorization = Base64CoDec.decode(makeMultipleOf4(encodedAuthorization));
		// getting the password
		String password = decodedAuthorization.substring(decodedAuthorization.indexOf(":")+1);
		// getting the user (to save in session variable)
		String user = decodedAuthorization.substring(0, decodedAuthorization.indexOf(":"));
		
		
		////////////////////////////////////////////////////////////////////////////////
		// empty password are not allowed
		//
		if(password==null || password.equals("")){
			log.debug( "Empty password are not allowed");
			log.debug( "Leaving LDAPFilter "+filterName);
			((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			((HttpServletResponse)response).setHeader("WWW-Authenticate", AUTH_TYPE+" realm=\""+AUTH_REALM+"\"");        	
			return;
		}// if
		
		
		//////////////////////////////////////////////////////////////////////////////////////////////
		// when the agent is MICROSOFT_MINIREDIR then the user is host\\user, we need to remove host\\
		//
		String request_agent = (((HttpServletRequest)request).getHeader("user-agent")).toLowerCase(); 
		if (StringUtils.contains(request_agent, MICROSOFT_MINIREDIR)) {
			log.debug( "agent="+MICROSOFT_MINIREDIR);
			log.debug( "user="+user+" rebuilding string...");
			if (StringUtils.contains(user, "\\")) user = StringUtils.substringAfterLast(user, "\\");
		}// if (StringUtils.contains(request_agent, MICROSOFT_MINIREDIR)) 
		// ESUP
		
		log.debug( "user > "+user);
		
		
		log.debug( "User > "+user+" found in authorization header");
		
		
		/////////////////////////
		// doing the LDAP request
		//
		DirContext ctx;       
				
		if(bindType.equalsIgnoreCase("FASTBIND")){
			/*========================
			 * 
			 * fastBind authentication
			 * 
			 *========================*/
			log.debug( "FASTBIND");
			
			// building the distinguished name
			String userDn = replaceStringInPattern(user, fastBindUserPattern);
			
			try {
				////////////////////////////
				// initializing LDAP request
				//
				Hashtable env = new Hashtable();
				env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
				env.put(Context.PROVIDER_URL, connectionUrl+" "+alternateUrl);					// LDAP URL
				env.put(Context.SECURITY_AUTHENTICATION, AUTHENTICATION_TYPE);	// authentication type
				env.put(Context.SECURITY_PRINCIPAL, userDn);					// user distinguished name
				env.put(Context.SECURITY_CREDENTIALS, password.getBytes());		// user password
				
				///////////////////////////////////////////////
				// connecting to the LDAP directory and binding
				//
				log.debug( "Connecting "+connectionUrl);
				log.debug( "with userDn : "+userDn);
				ctx = new InitialDirContext(env);
				log.debug( "LDAP Fast :\n"+"userDn : "+userDn);
				log.info("Bind successfull with user "+user);
				// ...if the bind fails an AuthenticationException is thrown
				
				// bind successfull
				// store the authenticated user in the session
	            if (session != null){ // probably unncessary
	                session.setAttribute(LDAP_FILTER_USER, user);
	                session.setAttribute(FILTER_USED_FOR_THIS_SESSION, filterName);
	            }// if
	            
	            log.info("Leaving LDAPFilter "+filterName);
	            filterChain.doFilter(request, response);
	        	return;
	        } catch (AuthenticationException e) {
	        	// bind failed
				log.debug( "Cannot connect to "+connectionUrl);
				log.debug( "with userDn : "+userDn);
				log.info("Bind failed with user "+user);
				log.info("Leaving LDAPFilter "+filterName);
				((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
				((HttpServletResponse)response).setHeader("WWW-Authenticate", AUTH_TYPE+" realm=\""+AUTH_REALM+"\"");       	
				return;
			} catch (NamingException e) {
				// internal error
				log.debug( "500 error sent");
				log.info("Leaving LDAPFilter "+filterName);
				((HttpServletResponse)response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
				return;
			}
			
			
		}else if(bindType.equalsIgnoreCase("SEARCHBIND") /*not necessary ?*/){
			/*==========================
			 * 
			 * searchBind authentication
			 * 
			 *==========================*/
			log.debug( "SEARCHBIND");
			
			NamingEnumeration ldapSearchResult; // result of the (future) LDAP search
			String userDn = new String(); // the (future) distinguished name
			String searchBindUserSearchBuilt = new String(); // temporary var. containing searchBindUserSearch=userToFind ex : uid=toto
			
			////////////////////////////
			// initializing LDAP request
			//
			// getting the scope parameter
			int scope = 0;
			if(searchBindUserSubtree.equals(SCOPE_SUBTREE_LEVEL)){
				scope = SearchControls.SUBTREE_SCOPE;
			}else if(searchBindUserSubtree.equals(SCOPE_ONE_LEVEL)){
				scope = SearchControls.ONELEVEL_SCOPE;
			}else if(searchBindUserSubtree.equals(SCOPE_OBJECT_LEVEL)){
				scope = SearchControls.OBJECT_SCOPE;
			}else{
				log.debug( "Invalid "+SEARCHBIND_USERSUBTREE+" parameter");
				log.debug( "500 error sent");
				((HttpServletResponse)response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
				return;
			}// if
			
			SearchControls searchControls = new SearchControls();
			// init. scope parameter
			searchControls.setSearchScope(scope);
			// building filter parameter
			searchBindUserSearchBuilt = replaceStringInPattern(user, searchBindUserSearch);
			
			Hashtable env = new Hashtable();
			env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
			// init. URL parameter
			env.put(Context.PROVIDER_URL, connectionUrl+" "+alternateUrl);
			// init. authentication type parameter
			env.put(Context.SECURITY_AUTHENTICATION, AUTHENTICATION_TYPE);
			
			// if the search request need an authentification - setting the principal and credential parameters
			if(searchBindConnectionName!=null && !searchBindConnectionName.equals("")){
				env.put(Context.SECURITY_PRINCIPAL, searchBindConnectionName);
				env.put(Context.SECURITY_CREDENTIALS, searchBindConnectionPassword.getBytes());
				log.debug( "userDN : "+searchBindConnectionName);
			}// if
			
			/////////////////////////////////////////////////
			// connecting to the LDAP directory and searching
			//
			try {
				// connecting to the LDAP directory
				ctx = new InitialDirContext(env);
				log.debug( "LDAP Search :\n"+"baseDN : "+searchBindUserBase+"\nfilter : "+searchBindUserSearchBuilt+"\n scope : "+searchBindUserSubtree);
				// searching the user
				ldapSearchResult = ctx.search(searchBindUserBase, searchBindUserSearchBuilt, searchControls);
				
				// processing the response - if more than 1 response then send an answer
				int counter = 0;
				boolean ldapEntryFound = false;
				while(ldapSearchResult!=null && ldapSearchResult.hasMore()){
					if(counter>1){
						// more than 1 entry with the given user in the LDAP directory - send a 500 error
						log.debug( "500 error sent");
						log.info("Leaving LDAPFilter "+filterName);
						((HttpServletResponse)response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
						return;
					}// if
					counter++;
					ldapEntryFound = true;
					SearchResult searchResult = (SearchResult)ldapSearchResult.next();
					userDn = (searchResult.getName())+","+searchBindUserBase;
				}// while
				
				// if ONLY one user found then trying to bind with his distinguished name and password
				if(ldapEntryFound){
					log.debug( "userDn found : "+userDn);
					env = new Hashtable();
					env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
					env.put(Context.PROVIDER_URL, connectionUrl+" "+alternateUrl);
					env.put(Context.SECURITY_AUTHENTICATION, AUTHENTICATION_TYPE);
					env.put(Context.SECURITY_PRINCIPAL, userDn);
					env.put(Context.SECURITY_CREDENTIALS, password.getBytes());
					
					// connecting to the LDAP directory
					log.debug( "Connecting "+connectionUrl);
					log.debug( "userDN : "+userDn);
					ctx = new InitialDirContext(env);
					log.info("Bind successfull with user "+user);
					// ...if the bind fails an AuthenticationException is thrown
					
					// bind successfull
					// store the authenticated user in the session
		            if (session != null){ // probably unncessary
		                session.setAttribute(LDAP_FILTER_USER, user);
		                session.setAttribute(FILTER_USED_FOR_THIS_SESSION, filterName);
		            }// if
		            
		            log.info("Leaving LDAPFilter "+filterName);
		            filterChain.doFilter(request, response);
					return;
				}
				else{
				// no user found
					log.debug( "No entry found in LDAP for user "+user);
					log.info("Leaving LDAPFilter "+filterName);
					((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
					((HttpServletResponse)response).setHeader("WWW-Authenticate", AUTH_TYPE+" realm=\""+AUTH_REALM+"\"");         	
					return;
				}// if
				
			} catch (AuthenticationException e) {
				log.debug( "Cannot connect to "+connectionUrl);
				log.debug( "with user : "+user);
				log.info("Bind failed with user "+user);
				log.info("Leaving LDAPFilter "+filterName);
				((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
				((HttpServletResponse)response).setHeader("WWW-Authenticate", AUTH_TYPE+" realm=\""+AUTH_REALM+"\"");         	
				return;
			} catch (NamingException e) {
				// internal error
				log.debug( "500 error sent");
				log.info("Leaving LDAPFilter "+filterName);
				((HttpServletResponse)response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
				return;
			}
		}// else
		
	// not neccessary	
	filterChain.doFilter(request, response);
	return;
	}// doFilter

	
	public void destroy() {
	}// destroy
	
}// LDAPFilter