package com.mjwilcox;

import java.util.Hashtable;
import java.util.Enumeration;


import javax.naming.*;
import javax.naming.directory.*;
import javax.naming.event.*;
import javax.naming.ldap.*;

import org.apache.tomcat.core.*;
import org.apache.tomcat.util.*;
import org.apache.tomcat.util.xml.*;
import org.apache.tomcat.request.*;

import java.io.*;
import java.net.*;
import java.util.*;
import javax.servlet.http.*;
import org.xml.sax.*;

public class ldapAuthCheck extends SimpleRealm {
//public class ldapAuthCheck extends org.apache.tomcat.request.SecurityCheck {
//public class SecurityCheck {

    // user variables
    private String ldapHost = ""; // name of LDAP server 
	private int ldapPort = 389;  // port the server runs on
	private String searchBase = ""; // search base for the DIT
	private String managerDN = "";  // entry to bind as for initial search DN
	private String managerPW = "";  // entry to bind as for initial search password
	
	//application variables
	
	// initial context implementation 
    private static String INITCTX = "com.sun.jndi.ldap.LdapCtxFactory";
	//for now users can't change the scope of the search 
	private static int scope = SearchControls.SUBTREE_SCOPE; // scope of search
	
    private Hashtable userCache = new Hashtable(); //keep a cahce of names and DNs
	private org.apache.tomcat.core.Context ourCTX = null; // use for debugging/logging
	private Hashtable userRoles = new Hashtable(); //keep track of the roles a user plays
	private Hashtable env = null; // store LDAP configuration information
	 

	/** default constructor */
	public void ldapAuthCheck()
	{
	}
	
	//Object getAttribute(String name)
	public void contextInit(org.apache.tomcat.core.Context ctx) throws TomcatException
	{
	  //temporary
	    ldapHost = "localhost"; // name of LDAP server 
	    ldapPort = 389;  // port the server runs on
	    searchBase = "o=airius.com"; // search base for the DIT
	  
	  ourCTX = ctx;
      super.contextInit(ctx);
	}
	
	/** set the host name of the LDAP server */
	public void setHost (String host)
	{
        ldapHost = host;
    }
  
    /** get the host name of the LDAP server */
    public String getHost ()
    {
       return ldapHost;
    } 
	
	/** set the port of the LDAP server */
    public void setPort (int port)
	{
       ldapPort = port;
    }
	
	/** get the port of the LDAP server */
	public int getPort ()
	{
        return ldapPort;
    }
		
    /** set the LDAP search base */
	public void setSearchbase (String base)
    {
        searchBase = base;
    }
  
    /** get the LDAP search base */
    public String getSearchbase ()
    {
        return searchBase;
    }
  
    /** set the connection 'manager' DN */
    public void setManagerDN (String dn)
    {
	    managerDN = dn;
	}
	
	/** get the connection 'manager' DN */
	public String getManagerDN ()
	{
	    return managerDN;
	}
	
	/** set the connection 'manager' password */
	public void setManagerPW (String password)
	{
	    managerPW = password;
	}
	
	/** get the connection 'manager' password */
	public String getManagerPW ()
	{
	    return managerPW;
	}
	
	/** inherited from SecurityCheck*/
	protected boolean checkPassword( String user, String pass ) {
	//if( memoryRealm != null ) return memoryRealm.checkPassword( user, pass );
          
	 ourCTX.log("checkPassword is "+user);
	 ourCTX.log("checkPassword true/false is a "+ LDAPauthenticate(user,pass));
	 return LDAPauthenticate(user, pass);
	//return false;
    }

	
    protected boolean userInRole( String user, String role ) {
	//if( memoryRealm != null ) return memoryRealm.userInRole( user, role );
	//return false;
	  ourCTX.log("role is " + LDAPuserInRole(user,role));
	  return LDAPuserInRole(user,role);
	 //return true;
    }
	
	
	//we use this to simply populate our environment
	public int authenticate( Request req, Response response )
    {
	   org.apache.tomcat.core.Context ctx=req.getContext();
	   env = getEnv(ctx);
	   //ctx.log("in authenticate LDAP host is "+ ctx.getInitParameter("ldaphost"));
	   //ctx.log("check is "+check);
	   //check++;
	   return super.authenticate(req,response);
	}
	  
	   
	  
    /** setup LDAP configuration */
	private Hashtable getEnv(org.apache.tomcat.core.Context ctx)
	{
	    //get data from XML file
		try
		{
		    ldapHost = ctx.getInitParameter("com.mjwilcox.ldapAuthCheck.ldaphost");
		    Integer tempPort = new Integer( ctx.getInitParameter("com.mjwilcox.ldapAuthCheck.ldapport"));
	        ourCTX.log("getEnv ldap port is "+ tempPort);
		    ldapPort = tempPort.intValue();
  	        ourCTX.log("in getEnv ldapPort is " + ldapPort);
			searchBase = ctx.getInitParameter("com.mjwilcox.ldapAuthCheck.searchbase");
			managerDN = ctx.getInitParameter("com.mjwilcox.ldapAuthCheck.managerdn");
			managerPW = ctx.getInitParameter("com.mjwilcox.ldapAuthCheck.managerpw");
			// if null, set to anonymous connection
		    if ((managerDN == null) || (managerPW == null))
		    {
		        managerDN = "";
		        managerPW = "";
		    }
		 		
	
	        Hashtable env = new Hashtable();
		    //Specify which class to use for our JNDI provider
		    env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, INITCTX);
		    //specify which host and port
		    StringBuffer jndiHost = new StringBuffer (); //hold JNDI host url
		    jndiHost.append("ldap://");
		    jndiHost.append(ldapHost);
		    jndiHost.append(":");
		    jndiHost.append(ldapPort);
		    env.put(javax.naming.Context.PROVIDER_URL,jndiHost.toString()); 
		
		    //Security Information
		    env.put(javax.naming.Context.SECURITY_AUTHENTICATION,"simple");
		    env.put(javax.naming.Context.SECURITY_PRINCIPAL,managerDN);
		    env.put(javax.naming.Context.SECURITY_CREDENTIALS,managerPW);
		    return env;
		}
		catch (NullPointerException ne)
		{
		   ctx.log("getEnv had a null pointer exception "+ne.toString());
		}
		catch (NumberFormatException nfe)
		{
		   ctx.log("getEnv had a number format exception "+nfe.toString());
	   	}
		
		return null;
		   
	}

	 
	    	  
	/** performs simple LDAP authentication */
	private boolean LDAPauthenticate (String username, String password)
	{
 	     ourCTX.log("in LDAPAuthenticate");
	      
		if ((env == null) || (password == null) || (password.equals("")))
		{
		   return false;
		}
		
		try
		{
            //Get a reference to a directory context
		    DirContext ctx = new InitialDirContext(env);
		
		    /*
		    retrieve the user's entry
		    assume their userid is stored in the uid attribute 
		    we also limit the returned attributes to just their uid 
		    because we don't really need anything retrieved from the LDAP server so 
		    we spare the extra overhead
		    */
		
		    StringBuffer filterBuffer = new StringBuffer ("uid=");
			filterBuffer.append(username);
			
		    String[] attrIDs = {"uid"};
  		    SearchControls ctls = new SearchControls();
            ctls.setReturningAttributes(attrIDs);       // return no attrs
	        ctls.setSearchScope(scope); // search object only
		
		    // Search for objects with those matching attributes
            NamingEnumeration results = ctx.search(searchBase, filterBuffer.toString(),ctls);
			ourCTX.log("filter is "+filterBuffer.toString());
			ourCTX.log("base is "+searchBase);
			StringBuffer dnBuffer = new StringBuffer (); //hold DN of results
		    
		    
		    while (results != null && results.hasMore())
		    {
                SearchResult sr = (SearchResult) results.next();

                dnBuffer.append(sr.getName());
				dnBuffer.append(",");
				dnBuffer.append(searchBase);
				
                //System.out.println("Distinguished Name is "+dnBuffer.toString());
				userCache.put(username,dnBuffer.toString());
				
				//now attempt to rebind to the server as the user
				ctx.removeFromEnvironment(javax.naming.Context.SECURITY_PRINCIPAL);
				ctx.removeFromEnvironment(javax.naming.Context.SECURITY_CREDENTIALS);
				ctx.addToEnvironment(javax.naming.Context.SECURITY_PRINCIPAL,dnBuffer.toString());
				ctx.addToEnvironment(javax.naming.Context.SECURITY_CREDENTIALS,password);
				
				//attempt an operation to see if we are still authenticated
				// Set up search controls
	            ctls = new SearchControls();
	            ctls.setReturningAttributes(new String[0]);       // return no attrs
	            ctls.setSearchScope(SearchControls.OBJECT_SCOPE); // search object only
				
				//JNDI compare, least resource intensive operation
				//only testing if our authentication succeeds or fails
				NamingEnumeration authResults = ctx.search(dnBuffer.toString(),"(uid=mark)",ctls);
								
				// should set a boolean to return at the end
				// make sure we null out all of our variables used in this method
				// should set them up first so that we can re use them
				return true;
				
            }
		}
		catch (NamingException ne)
		{
		   ourCTX.log( "naming exception error: " + ne.toString());
		   return false;
		}

		 return false;
	}
	
	/** check to see if user is in proper role which in our case is an LDAP group.
	    Right now this is very simple. The string name must match the CN attribute of a group.
		We only handle members of groupOfMembers and groupOfUniquemembers. No inherited groups or dynamic 
		groups at this time. */
	private boolean LDAPuserInRole (String username, String groupname)
	{
	    ourCTX.log("LDAPuserInRole "+username + " " + groupname);
		String userDN = (String) userCache.get(username);
		if (userDN == null) return false;
		
	    //need a routine to grab connection
		//Hashtable for environmental information
		Hashtable env = new Hashtable();
		
		StringBuffer jndiHost = new StringBuffer (); //hold JNDI host url
 	    
		//Specify which class to use for our JNDI provider
		env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, INITCTX);

		
		//specify which host and port
		jndiHost.append("ldap://");
		jndiHost.append(ldapHost);
		jndiHost.append(":");
		jndiHost.append(ldapPort);
		env.put(javax.naming.Context.PROVIDER_URL,jndiHost.toString()); 
		
		//Security Information
		env.put(javax.naming.Context.SECURITY_AUTHENTICATION,"simple");
		env.put(javax.naming.Context.SECURITY_PRINCIPAL,managerDN);
		env.put(javax.naming.Context.SECURITY_CREDENTIALS,managerPW);

		try
		{
            //Get a reference to a directory context
		    DirContext ctx = new InitialDirContext(env);
		
		    //look for group and member at the same time
		    StringBuffer filterBuffer = new StringBuffer ("(&(cn=");
			filterBuffer.append(groupname);
			filterBuffer.append(")(|(uniquemember=");
			filterBuffer.append(userDN);
			filterBuffer.append(")(member=");
			filterBuffer.append(userDN);
			filterBuffer.append(")))");
			
		    String[] attrIDs = {"uniquemember"};
  		    SearchControls ctls = new SearchControls();
            ctls.setReturningAttributes(attrIDs);       
	        ctls.setSearchScope(scope); 
		
		    // Search for objects with those matching attributes
            NamingEnumeration results = ctx.search(searchBase, filterBuffer.toString(),ctls);
			
			StringBuffer dnBuffer = new StringBuffer (); //hold DN of results
		   
		   
		    if (results != null && results.hasMore())
			{
	       	   return true;
			}								
		}
		catch (NamingException ne)
		{
		   ourCTX.log( "inRole naming exception error: " + ne.toString());
		}

		 return false;
	}
	
	/** get a Vector of the roles a user can play. In LDAP this would be the groups they are a 
	member of.
	   We only check for membership in traditional groups like groupOfUniquemember and groufOfMember, 
	   but not dynamic groups.
	   
	   If we wanted to support dynamic gruops, we'll likely need to write our own requestSecurityProvider
	   something that I'll leave for later.
	   */

	private Vector getGroups(String username)
	{
	    ourCTX.log("get Roles");
		Vector groups = new Vector();
		String userDN = (String) userCache.get(username);
		
		if (userDN == null) return null;
		
		if (env == null)
		{
		   return null;
		}
	  

		try
		{
            //Get a reference to a directory context
		    DirContext ctx = new InitialDirContext(env);
		
		    //look for group and member at the same time
		    StringBuffer filterBuffer = new StringBuffer("(|(uniquemember=");
			filterBuffer.append(userDN);
			filterBuffer.append(")(member=");
			filterBuffer.append(userDN);
			filterBuffer.append("))");
			
		    String[] attrIDs = {"cn"};
  		    SearchControls ctls = new SearchControls();
            ctls.setReturningAttributes(attrIDs);       
	        ctls.setSearchScope(scope); 
		
		    // Search for objects with those matching attributes
            NamingEnumeration results = ctx.search(searchBase, filterBuffer.toString(),ctls);
			
			StringBuffer dnBuffer = new StringBuffer (); //hold DN of results
		   
    	    while (results != null && results.hasMore())
		    {
                SearchResult sr = (SearchResult) results.next();

            //    dnBuffer.append(sr.getName());
			//	dnBuffer.append(",");
			//	dnBuffer.append(searchBase);
			
                Attributes attrs = sr.getAttributes();
	            for (NamingEnumeration ne = attrs.getAll();ne.hasMoreElements();)
	            {
	                Attribute attr = (Attribute)ne.next();
					String attrID = attr.getID();
					
					if (attrID.equals("cn"))
					{
					    continue; // to the next attribute
					}
					
					for (Enumeration vals = attr.getAll();vals.hasMoreElements();)
	                {
	                    groups.addElement(vals.nextElement());
					}
				}
			}
			
			return groups;
		
		}
		catch (NamingException ne)
		{
		   ourCTX.log( "getRoles naming exception error: " + ne.toString());
	    }
		 return null;
	}
	
	
	// test-drive function
	public static void main (String args[])
	{
	    ldapAuthCheck la = new ldapAuthCheck();
		la.setHost("ldap.acme.com");
		la.setPort(389);
		la.setSearchbase("ou=people,dc=acme,dc=com");
		System.out.println( "authentication is " + la.LDAPauthenticate("user","password") );
	}
		
}
	
	
