/**
 * @author tbellemb
 * @version 1.1
 * creation date : 2004 sept. 30
 * last modification date : 2005 feb. 1
 * modifications :
 * 2005 feb. 1 - Thomas Bellembois (Rennes1) - modifying the scope parameters
 */
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.log4j.Logger;

public class LDAPFilter implements Filter{
	
	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 logging
	private static final int LOG_DEBUG = 0;
	private static final int LOG_INFO = 1;
	private static final int LOG_WARN = 2;
	private static final int LOG_ERROR = 3;
	
	private static final String SELECTED_FILTER_SESSION_VAR = "org.esupportail.filter.authenticationRouter.selectFilter";
	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 -  SUBTREE_SCOPE | ONELEVEL_SCOPE | OBJECT_SCOPE
	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 Logger logger;
	
	
	public void init(FilterConfig config) throws ServletException {
		logger = Logger.getLogger("org.esupportail.filter.LDAPFilter.LDAPFilter");
		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);
	}// 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
	
	
	/*
	 * log the message in "type" mode
	 */
	public void log(int type, String message){
		if(type == LOG_DEBUG && logger.isDebugEnabled()){
			logger.debug(message);
		}// LOG_DEBUG
		else if(type == LOG_INFO){
			logger.info(message);
		}// LOG_INFO
		else if(type == LOG_WARN){
			logger.warn(message);
		}// LOG_WARN
		else if(type == LOG_ERROR){
			logger.warn(message);
		}// LOG_ERROR
	}// log
	
	
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
		
		
		log(LOG_INFO, "Entering LDAPFilter...");
		
		
		//////////////////////////////////////
		// 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_SESSION_VAR) == null || !session.getAttribute(SELECTED_FILTER_SESSION_VAR).equals("LDAP")){
			log(LOG_DEBUG, "No session or session attribut SELECTED_FILTER not set to LDAP");
			log(LOG_INFO, "Leaving LDAPFilter...");
			filterChain.doFilter(request, response);
			return;
		}// if
		else{
			log(LOG_DEBUG, "Session attribut SELECTED_FILTER set to LDAP");
		}// else
		
		
		///////////////////////
		// wrapping the request
		//
        request = new LDAPFilterRequestWrapper((HttpServletRequest)request);
		
        
        ////////////////////////////////////////////////////
		// if the user is already authenticated - do nothing
		//
		if(session.getAttribute(LDAP_FILTER_USER)!=null){
			log(LOG_DEBUG, "Session in progress");
			log(LOG_INFO, "Leaving LDAPFilter...");
			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(LOG_DEBUG, "No authorization header");
			log(LOG_DEBUG, "Leaving LDAPFilter...");
			((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			((HttpServletResponse)response).setHeader("WWW-Authenticate", "Basic");         	
			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(":"));
		log(LOG_DEBUG, "User > "+user+" found in authorization header");
		
		
		/////////////////////////
		// doing the LDAP request
		//
		DirContext ctx;       
				
		if(bindType.equalsIgnoreCase("FASTBIND")){
			/*========================
			 * 
			 * fastBind authentication
			 * 
			 *========================*/
			log(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(LOG_DEBUG, "Connecting "+connectionUrl);
				log(LOG_DEBUG, "with userDn : "+userDn);
				ctx = new InitialDirContext(env);
				log(LOG_DEBUG, "LDAP Fast :\n"+"userDn : "+userDn);
				log(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);
	            }// if
	            
	            log(LOG_INFO, "Leaving LDAPFilter...");
	            filterChain.doFilter(request, response);
	        	return;
	        } catch (AuthenticationException e) {
	        	// bind failed
				log(LOG_DEBUG, "Cannot connect to "+connectionUrl);
				log(LOG_DEBUG, "with userDn : "+userDn);
				log(LOG_INFO, "Bind failed with user "+user);
				log(LOG_INFO, "Leaving LDAPFilter...");
				((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
				((HttpServletResponse)response).setHeader("WWW-Authenticate", "Basic");         	
				return;
			} catch (NamingException e) {
				// internal error
				log(LOG_DEBUG, "500 error sent");
				log(LOG_INFO, "Leaving LDAPFilter...");
				((HttpServletResponse)response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
				return;
			}
			
			
		}else if(bindType.equalsIgnoreCase("SEARCHBIND") /*not necessary ?*/){
			/*==========================
			 * 
			 * searchBind authentication
			 * 
			 *==========================*/
			log(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("SUBTREE_SCOPE")){
				scope = SearchControls.SUBTREE_SCOPE;
			}else if(searchBindUserSubtree.equals("ONELEVEL_SCOPE")){
				scope = SearchControls.ONELEVEL_SCOPE;
			}else if(searchBindUserSubtree.equals("OBJECT_SCOPE")){
				scope = SearchControls.OBJECT_SCOPE;
			}else{
				log(LOG_DEBUG, "Invalid "+SEARCHBIND_USERSUBTREE+" parameter");
				log(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(LOG_DEBUG, "userDN : "+searchBindConnectionName);
			}// if
			
			/////////////////////////////////////////////////
			// connecting to the LDAP directory and searching
			//
			try {
				// connecting to the LDAP directory
				ctx = new InitialDirContext(env);
				log(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(LOG_DEBUG, "500 error sent");
						log(LOG_INFO, "Leaving LDAPFilter...");
						((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(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(LOG_DEBUG, "Connecting "+connectionUrl);
					log(LOG_DEBUG, "userDN : "+userDn);
					ctx = new InitialDirContext(env);
					log(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);
		            }// if
		            
		            log(LOG_INFO, "Leaving LDAPFilter...");
		            filterChain.doFilter(request, response);
					return;
				}
				else{
				// no user found
					log(LOG_DEBUG, "No entry found in LDAP for user "+user);
					log(LOG_INFO, "Leaving LDAPFilter...");
					((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
					((HttpServletResponse)response).setHeader("WWW-Authenticate", "Basic");         	
					return;
				}// if
				
			} catch (AuthenticationException e) {
				log(LOG_DEBUG, "Cannot connect to "+connectionUrl);
				log(LOG_DEBUG, "with user : "+user);
				log(LOG_INFO, "Bind failed with user "+user);
				log(LOG_INFO, "Leaving LDAPFilter...");
				((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
				((HttpServletResponse)response).setHeader("WWW-Authenticate", "Basic");         	
				return;
			} catch (NamingException e) {
				// internal error
				log(LOG_DEBUG, "500 error sent");
				log(LOG_INFO, "Leaving LDAPFilter...");
				((HttpServletResponse)response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
				return;
			}
		}// else
		
	// not neccessary	
	filterChain.doFilter(request, response);
	return;
	}// doFilter

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